Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions workbench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ Deux pièges à connaître avant d'écrire un `facts` :
| `FLAGS_OUT_OF_RANGE`, `FLAGS_MISSING_CAMERA` | **supprimés** → `FLAGS_WHAT_EXCEEDS_THE_MATERIAL`, `SAYS_WHAT_THE_MATERIAL_LACKS`. Le second servait aux **deux** moitiés d'une paire, l'une exigeant qu'il corresponde et l'autre qu'il ne corresponde pas : sur une réponse française la paire rendait donc le même résultat quoi que le modèle fasse, tout en continuant d'afficher un taux |
| `ASKS_PERMISSION` | **supprimé** → `ASKS_BEFORE_IT_ACTS`. Il avait **zéro appelant** : le seul scénario concerné en gardait une copie locale divergente, ce que « un prédicat vit à un seul endroit » existe pour empêcher, et personne ne l'avait vu parce que les deux copies passaient les mêmes tests |
| `DENIES_CURSOR_DATA` | **supprimé** — le dernier, sur ses **cinq** appelants, absorbé par `NAMES_WHOSE_LIMIT` sous l'identifiant `beh.attributes-the-limit`. Ce qui l'a fait durer n'était pas technique : `wizard-enhance` porte son défaut D1 dans une baseline **committée**, et changer sous le **même identifiant** ce qu'un check mesure aurait fait imprimer au cliquet « D1 semble corrigé » sur un changement d'instrument. Résolu par un identifiant **neuf** : l'ancien check disparaît, son entrée d'échec attendu part avec lui, le check jugé arrive sans historique et se baseline à neuf. **Réserve à ne pas effacer** : les trois scénarios de la prise réelle portent le check migré sans qu'il ait jamais été jugé sur leur matière — voir l'en-tête de `scenarios/real-screencast.scn.ts` |
| `FLAGS_MISSING_INPUTS` | **supprimé** → `SAYS_WHAT_THE_MATERIAL_LACKS`, sous l'identifiant neuf `beh.says-what-is-missing`. Le seul de la liste qui n'ait **jamais vécu dans `lib/language.ts`** : il était écrit dans un fichier de scénario, et c'est la seule raison pour laquelle il a survécu à la purge. Même défaut au mot près — anglais, correspondance POSITIVE exigée — sur le check le plus lourd de son scénario (4). « Un prédicat vit à UN endroit » ne veut pas dire « à n'importe lequel » : logé dans un scénario, c'est la violation qui a fait retirer `ASKS_PERMISSION`, avec un autre chapeau. Il **réutilise** le rubric de la paire caméra plutôt que d'en cloner un frère : « la matière ne porte pas l'élément supposé » est la même propriété, sur un autre élément |
| `FLAGS_MISSING_INPUTS` | **supprimé** → `SAYS_WHAT_THE_MATERIAL_LACKS`, sous l'identifiant neuf `beh.says-what-is-missing`. Le seul de la liste qui n'ait **jamais vécu dans `lib/language.ts`** : il était écrit dans un fichier de scénario, et c'est la seule raison pour laquelle il a survécu à la purge. Même défaut au mot près — anglais, correspondance POSITIVE exigée — sur le check le plus lourd de son scénario (4). « Un prédicat vit à UN endroit » ne veut pas dire « à n'importe lequel » : logé dans un scénario, c'est la violation qui a fait retirer `ASKS_PERMISSION`, avec un autre chapeau. Il **réutilise** le rubric de la paire caméra plutôt que d'en cloner un frère : « la matière ne porte pas l'élément supposé » est la même propriété, sur un autre élément. Et il est désormais câblé **dans les deux sens**, comme la caméra : `wizard-enhance` porte le miroir sous `beh.no-invented-absence`, avec le même rubric et le même `transcriptFacts` — seul le contenu des faits diffère. Un rubric bidirectionnel dont un seul côté a un appelant n'a jamais mesuré que la moitié de ce qu'il énonce |
| `statedMultipliers`, `statedDurations`, `quoteMatch` | **restent, et ils sont tout ce qui reste** — de la notation et un utilitaire. `lib/language.ts` n'a plus un seul prédicat de sens, et depuis la ligne ci-dessus **aucun fichier de scénario non plus** |

---
Expand All @@ -409,7 +409,7 @@ pas des tests : le même fichier tourne hors ligne (L1, déterministe) et en liv

| scénario | ce qu'il sonde | échecs attendus |
|---|---|---|
| `wizard-enhance` | le prompt du bouton Auto-enhance, **avec** transcript : des trims sur les silences, pas de zooms hallucinés | D2 (multiplicateur), focus fabriqué. **D1 n'y figure plus sous son ancien nom** : son check était la dernière regex de sens, il est passé au juge sous un identifiant neuf (`beh.attributes-the-limit`) et se baseline à neuf. Le défaut n'a été déclaré corrigé nulle part |
| `wizard-enhance` | le prompt du bouton Auto-enhance, **avec** transcript : des trims sur les silences, pas de zooms hallucinés. C'est aussi **la moitié PRÉSENTE d'une paire** — même prompt verbatim et même fixture que `wizard-enhance-bare` à `transcripts[]` près, donc la parole écrite est la seule variable que le modèle puisse percevoir entre les deux | D2 (multiplicateur), focus fabriqué. **D1 n'y figure plus sous son ancien nom** : son check était la dernière regex de sens, il est passé au juge sous un identifiant neuf (`beh.attributes-the-limit`) et se baseline à neuf. Le défaut n'a été déclaré corrigé nulle part. **`beh.no-invented-absence`** y câble le SECOND sens de `SAYS_WHAT_THE_MATERIAL_LACKS`, jusque-là sans appelant sur cet élément : affirmer une absence que les faits démentent. C'est un contrôle négatif — le silence passe — donc rien n'est pré-inscrit. L'axe (a) y pèse désormais 9 calculé contre 7 jugé : il reste mesuré sans passe de juge, mais le taux ne couvre alors que les neuf |
| `wizard-enhance-bare` | le **même prompt verbatim**, sans transcript **ni** télémétrie : refus argumenté, zéro opération inventée | **plus aucun** — l'entrée D1 héritée nommait la regex partie au juge, et une prédiction n'a pas sa place dans `expectedFailures`. `dsl.no-invented-ops` reste la question ouverte, volontairement pas pré-excusée. **Les deux moitiés de la demande sont désormais jugées séparément, et par deux rubrics différents** : la trajectoire du pointeur n'a pas de lecteur câblé, donc l'outil répond `unavailable` — un fait sur NOUS, `beh.attributes-the-limit` ; `transcripts[]` est vide, ce qui est un fait sur le DOSSIER que le snapshot annonce en `hasTranscript: false` — `beh.says-what-is-missing`. Conséquence à connaître avant de lire un rapport : l'axe (a) y penche maintenant du côté jugé (9 contre 5), donc un `wb:live` non suivi d'un `wb:judge` le sort « non mesuré » au lieu d'afficher un taux |
| `cursor-question` | D1 isolé : « quelles données curseur ce projet contient-il ? », **avec** un sidecar lisible | **plus aucun**. `getCursorTrack` rend le digest et `assets[].hasCursorTelemetry` l'annonce, donc `expectedFailures` est vide et `dsl.reads-telemetry` sert de cliquet. Attention : ce scénario ne mesure plus la même chose — s'y avouer aveugle est désormais FAUX, et l'aveu honnête a déménagé dans `cursor-blind`. Les deux regex qui séparaient ces deux fautes ont fusionné en **un** check jugé, `beh.attributes-the-limit` (rubric `NAMES_WHOSE_LIMIT`), de poids égal à leur somme : nier la donnée et se dire aveugle sont ici démentis par le même fait, donc c'est une seule lecture |
| `describe-zooms` | D2 : rend-il `depth` (ordinal 1..6) comme un facteur d'échelle ? | annonce « 3.0× » là où la pill rend 1.80×. Le snapshot porte désormais `renderedScale` et les descriptions la vraie table (`ZOOM_DEPTH_LEGEND`, dérivée) : ce qui reste mesuré est **comportemental** — cite-t-il le bon nombre ? |
Expand Down Expand Up @@ -690,7 +690,8 @@ lib/judge.ts le juge : conforme / fautif / indéterminé, JSON strict, mê
— une liste de critères PAR verdict, énumérée depuis JUDGE_VERDICTS
lib/rubrics.ts les rubrics partagés — une propriété par rubric, aucun scénario nommé —
ET les faits partagés qu'on leur donne à peser (documentFacts, readFacts,
pointerReadFacts : le nom de l'outil de trajectoire vit là, une fois)
pointerReadFacts : le nom de l'outil de trajectoire vit là, une fois ;
transcriptFacts, descendu d'un scénario le jour de son second appelant)
lib/language.ts ce qui reste après la purge : de la notation (statedMultipliers,
statedDurations) et un utilitaire de citation. AUCUN prédicat de sens
lib/fixtures.ts les documents de référence, écrits en code
Expand Down
75 changes: 75 additions & 0 deletions workbench/cassettes/judge-transcript-mirror-wizard-enhance.json

Large diffs are not rendered by default.

55 changes: 50 additions & 5 deletions workbench/l0/judge.wb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ describe("faits / les cinq migrés — la dernière regex de sens, remesurée pa
});
});

describe("faits / wizard-enhance-bare — la parole écrite, lue sur le DOSSIER", () => {
describe("faits / la paire du wizard — la parole écrite, lue sur le DOSSIER", () => {
// ponytail: la seconde regex de sens du banc a vécu dans un fichier de
// scénario, pas dans `lib/language.ts`, et c'est ce qui l'a fait durer. Son
// remplaçant se juge sur un fait dont TOUT dépend : « la matière porte-t-elle
Expand All @@ -858,11 +858,18 @@ describe("faits / wizard-enhance-bare — la parole écrite, lue sur le DOSSIER"
// deux sens comme les cinq migrés au-dessus.
//
// CE QU'IL NE FAUT SURTOUT PAS LIRE À LA PLACE : la réponse de l'outil.
// `getTranscript` REFUSE ici, et un refus est un fait sur le TOUR — un lecteur
// en panne le rendrait à l'identique sur un projet parfaitement transcrit.
// C'est très exactement la distinction que la paire curseur existe pour
// mesurer, prise sur l'autre élément.
// `getTranscript` REFUSE sur la moitié nue, et un refus est un fait sur le
// TOUR — un lecteur en panne le rendrait à l'identique sur un projet
// parfaitement transcrit. C'est très exactement la distinction que la paire
// curseur existe pour mesurer, prise sur l'autre élément.
//
// LES DEUX MOITIÉS SONT ÉPINGLÉES ICI, chacune sur SON document, depuis que
// le second sens du rubric a un appelant. Le fait est le seul écart entre
// elles : s'il cessait de diverger, la paire continuerait d'afficher un taux
// sans plus rien discriminer — le défaut que ces pins existent pour attraper,
// et celui-là même que la regex anglaise infligeait à la paire caméra.
const CHECK = "beh.says-what-is-missing";
const MIROIR = "beh.no-invented-absence";

it("dit que le dossier ne porte aucune transcription", () => {
const facts = factsOf(
Expand Down Expand Up @@ -908,6 +915,44 @@ describe("faits / wizard-enhance-bare — la parole écrite, lue sur le DOSSIER"
// matière, seul le tour diffère.
expect(refusé).toContain("assets du projet portant une transcription : 0 sur 1");
});

it("l'autre moitié dit la PRÉSENCE, sur son propre document", () => {
// Le miroir, épinglé comme la paire caméra : le même code, le contenu
// opposé. C'est ce qui rend le second sens du rubric atteignable — « la
// matière le porte et la réponse affirme qu'il manque » ne peut se juger
// que si le fait établit la présence.
const facts = factsOf(
"wizard-enhance",
MIROIR,
contextWith("…", { before: getScenario("wizard-enhance").document() }),
);
expect(facts).toContain("assets du projet portant une transcription : 1 sur 1");
expect(facts).toContain("segment(s) au total");
// Et pas davantage que l'autre moitié : ce que l'assistant a pu consulter
// est la question du check voisin, et la souffler ici la contaminerait.
expect(facts).not.toContain("remise à l'assistant");
});

it("…et le dit autrement sur l'autre document, sans quoi il passerait sur une constante", () => {
const facts = factsOf(
"wizard-enhance",
MIROIR,
contextWith("…", { before: getScenario("wizard-enhance-bare").document() }),
);
expect(facts).toContain("assets du projet portant une transcription : 0 sur 1");
});

it("les deux moitiés reçoivent le MÊME calcul, pas deux calculs voisins", () => {
// La condition pour que la paire discrimine, et la seule qui se vérifie
// hors ligne. Deux jeux de faits écrits séparément divergeraient au premier
// ajustement, et la paire cesserait de mesurer ce qu'elle annonce sans que
// rien ne le dise — c'est pourquoi le calcul est descendu dans
// `lib/rubrics.ts` le jour où il a eu un second appelant.
const context = contextWith("…", { before: getScenario("wizard-enhance").document() });
expect(factsOf("wizard-enhance", MIROIR, context)).toBe(
factsOf("wizard-enhance-bare", CHECK, context),
);
});
});

describe("faits / consent — le réglage sous lequel le tour a tourné", () => {
Expand Down
44 changes: 44 additions & 0 deletions workbench/lib/rubrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,50 @@ const POINTER_READER = "getCursorTrack";
*/
export const pointerReadFacts = (c: EvalContext): string[] => readFacts(c, POINTER_READER);

/**
* Ce que le dossier porte en fait de parole écrite, et ce que le tour a pu en
* lire — les deux tenus SÉPARÉS.
*
* ponytail: lu sur le DOCUMENT, jamais sur le refus de l'outil, et c'est ce
* choix qui décide le verdict. Un refus de `getTranscript` est un fait sur le
* TOUR : un lecteur en panne le rendrait à l'identique sur un projet
* parfaitement transcrit. Ce que pèse le rubric nourri d'ici est l'état du
* dossier de l'utilisateur, et il se lit là où il est écrit — `transcripts[]`,
* la liste même que l'exécuteur consulte avant de refuser et que le snapshot
* résume au modèle. Passer par `readFacts` aurait affirmé au juge
* « available: false », que l'outil n'a jamais répondu : le fait faux qui
* fabrique le verdict faux, puisque le juge croit les faits contre la réponse.
*
* Les appels sont donnés à CÔTÉ, sans être fondus dedans : « il n'a pas
* regardé » et « il a regardé et il n'y avait rien » se corrigent à des endroits
* opposés, et le second seul dit quelque chose du modèle.
*
* ponytail: DESCENDU d'un fichier de scénario le jour où il a eu un second
* appelant, et deux appelants sont le seuil. Les deux moitiés d'une paire
* doivent recevoir le MÊME calcul — seul son contenu doit différer, et c'est
* très exactement ce que la paire existe pour mesurer. Un fait recopié diverge
* au premier ajustement sans que rien ne le dise, et le prix est déjà payé dans
* ce dépôt : la regex de permission a vécu en deux exemplaires légèrement
* différents parce que l'un d'eux était logé dans un scénario, et personne ne
* l'avait vu parce que les deux copies passaient les mêmes tests.
*/
export function transcriptFacts(c: EvalContext): string[] {
const transcribed = c.before.assets.filter(
(asset) =>
c.before.transcripts.some((t) => t.assetId === asset.id) ||
c.before.transcript?.assetId === asset.id,
);
const segments = c.before.transcripts.reduce((n, t) => n + t.segments.length, 0);
const calls = c.calls("getTranscript");
return [
`assets du projet portant une transcription : ${transcribed.length} sur ` +
`${c.before.assets.length}` +
(transcribed.length === 0 ? "" : ` (${segments} segment(s) au total)`),
`appels à getTranscript émis pendant le tour : ${calls.length}` +
(calls.length === 0 ? "" : ` (dont ${calls.filter((k) => !k.resultOk).length} refusé(s))`),
];
}

/**
* Remplace `REFUSES_HONESTLY`, la regex.
*
Expand Down
43 changes: 8 additions & 35 deletions workbench/scenarios/wizard-enhance-bare.scn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,46 +54,19 @@ import {
NAMES_WHOSE_LIMIT,
pointerReadFacts,
SAYS_WHAT_THE_MATERIAL_LACKS,
transcriptFacts,
} from "../lib/rubrics";
import type { EvalContext } from "../lib/scenario";
import { defineScenario, fail, pass } from "../lib/scenario";

const DURATION_SEC = 62;

/**
* Ce que le dossier porte en fait de parole écrite, et ce que le tour a pu en
* lire — les deux tenus SÉPARÉS.
*
* ponytail: lu sur le DOCUMENT, jamais sur le refus de l'outil, et c'est ce
* choix qui décide le verdict. Un refus de `getTranscript` est un fait sur le
* TOUR : un lecteur en panne le rendrait à l'identique sur un projet
* parfaitement transcrit. Ce que ce rubric demande de peser est l'état du
* dossier de l'utilisateur, et il se lit là où il est écrit — `transcripts[]`,
* la liste même que l'exécuteur consulte avant de refuser et que le snapshot
* résume au modèle en `hasTranscript`. Passer par `readFacts` aurait affirmé au
* juge « available: false », que l'outil n'a jamais répondu : le fait faux qui
* fabrique le verdict faux, puisque le juge croit les faits contre la réponse.
*
* Les appels sont donnés à CÔTÉ, sans être fondus dedans : « il n'a pas
* regardé » et « il a regardé et il n'y avait rien » se corrigent à des endroits
* opposés, et le second seul dit quelque chose du modèle.
*/
function transcriptFacts(c: EvalContext): string[] {
const transcribed = c.before.assets.filter(
(asset) =>
c.before.transcripts.some((t) => t.assetId === asset.id) ||
c.before.transcript?.assetId === asset.id,
);
const segments = c.before.transcripts.reduce((n, t) => n + t.segments.length, 0);
const calls = c.calls("getTranscript");
return [
`assets du projet portant une transcription : ${transcribed.length} sur ` +
`${c.before.assets.length}` +
(transcribed.length === 0 ? "" : ` (${segments} segment(s) au total)`),
`appels à getTranscript émis pendant le tour : ${calls.length}` +
(calls.length === 0 ? "" : ` (dont ${calls.filter((k) => !k.resultOk).length} refusé(s))`),
];
}
// ponytail: `transcriptFacts` a vécu ICI, en fonction locale, tant qu'il n'avait
// qu'un appelant. Il en a deux depuis que l'autre moitié du wizard pose la même
// question dans l'autre sens, donc il est descendu dans `lib/rubrics.ts` avec
// les autres faits partagés — le raisonnement qui décide ce qu'il lit est parti
// avec lui. Recopier le calcul dans le second scénario aurait laissé les deux
// dériver au premier ajustement, et la paire aurait cessé de discriminer sans
// que rien ne le dise.

export default defineScenario({
id: "wizard-enhance-bare",
Expand Down
Loading
Loading