# Testigo

**Testigo puts a migrated app next to its legacy system and leaves a record of what changed.**

- **The actas, live:** https://maxiroblesfx-commits.github.io/testigo/ (click a screenshot to enlarge it)
- **Demo video (2:29):** https://youtu.be/0vkE4oXe5ss

For every record, it captures the screens of both systems twice: when the record opens, and after it is saved without touching anything. Muse Spark reads every screen. Testigo then compares field by field under fixed rules and writes an *acta*, a short report. Each value in the acta links to the screenshot where it can be seen.

Testigo does not replace a migration's tests. It always asks the same question, the same way, and keeps the evidence.

## Why

> I migrated a real HR system with an AI agent: the one that runs a company's payroll. It was a huge amount of work, and we lost weeks. Sometimes we were looking in the wrong place; once we spent a week going down a path that wasn't the right one. Part of it was my own inexperience, but many errors were there the whole time and got lost in all that work. Testigo comes from that: a fixed question, asked the same way every time, about whether what the old system had is still there in the new one. It sorts out the basics so you don't make silly mistakes. The complex cases still need a person to look at them.
>
> — Maxi, author

No data, screens or code from that case appear in this repository, in API calls or in the video. All demos use an invented membership registry.

## See it

Open [`index.html`](index.html): the three actas, each with a one-line verdict, the rows to review with a line saying what to compare, and the four screenshots of every field in a before/after grid. Click a screenshot to enlarge it.

## How it works

1. **Record.** Each record gets four screenshots: legacy and new, on open and after an untouched save.
2. **Read.** Muse Spark 1.3 reads every screenshot twice with the same sealed prompt. It returns every label/value pair it sees; the prompt carries no field list. The prompt asks that an empty field stay empty and never become `0`.
3. **Pair labels.** A map says which legacy field matches which new one, for example *Saldo pendiente → Deuda acumulada*. Spark proposes the map from the two screens of the same record. A pair is kept only if 8 requests agree; otherwise it stays pending for a person. On our apps it proposed 12 of 12 pairs correctly for both migrations ([`20261103-testigo-mapa-spark/`](20261103-testigo-mapa-spark/RESULTADO.md)). The actas still use the map Claude wrote earlier from Spark's readings; they were not regenerated from Spark's proposal, which has the same content. No person has confirmed the map. The test checks Spark's proposals on our own material: the evaluation compares them with a list of the 12 reference labels, and Spark was not given that list. It does not show that Spark can discover every field of a system that has no inventory.
4. **Compare.** The rules are fixed before reading:
   - Dates and amounts are compared by value, using the format each screen declares in its header, thousands separator included.
   - Empty vs zero, leading zeros, case and accents are never normalized.
   - If the two readings disagree, the row stays **pending**: Testigo does not pick one.
5. **Acta.** A one-line conclusion comes first, then the rows to review with links to their screenshots. All other rows are collapsed.

## Use of Meta models

- **Muse Spark 1.3** is the only reader. To write the acta, Testigo never reads the legacy database: it works from what the system shows, which is what you have when the legacy code cannot be touched. The evaluation of Testigo itself does use the test apps' data and answer key, to check the acta against the truth.
- **Muse Spark 1.3** also proposes the label map, from the old and new screens of the same record (see *What was tested*).
- **Muse Code** performed the migration under audit. It ran twice on the same legacy app:
  - Run 1 could not execute its tests.
  - Run 2 passed its 41 tests, and an untouched save changed no data.
- **SAM 3.1** was tried for drawing each field on the screenshot. Its boxes matched 215 of 240 cells but none of the 20 checkboxes, so the acta does not depend on it. The overlap cutoff behind "matched" was chosen after seeing the results.

## What was tested

Everything below uses our own apps and data, and the same team built the evaluation. It is better at refuting than at validating.

| Test | Result |
|---|---|
| **Milestone 1:** does Spark read without turning empty into zero? 20 screenshots, two passes: 40 Spark requests (plus 20 to SAM 3.1) | 0 empty-read-as-zero. Abstained on both hidden or blurred fields in both passes (4 of 4 readings). 474 of 480 cells exact. |
| **Milestone 1:** a seeded defect (saving in the web turns three empty fields into `0`) | Flagged 3 of 3 in both passes. 0 false equalities. 1 false alarm caused by the thousands separator. 2 rows pending because the two readings disagreed. |
| **Milestone 2:** Muse Code's migration and the repaired web, 32 requests | 0 changes, 0 false equalities, 0 false alarms. 3 and 2 rows pending because the two readings disagreed. |
| **Label map:** Spark proposes which old field matches which new one, from both screens. 16 requests, rule preregistered and amended before the responses were opened | 12 of 12 pairs right for the repaired web and for Muse Code's app. Every request got 12 of 12. A decoy (*Cuota mensual* vs *Plan mutual*) never fooled it. Easy case: 12 fields, and Muse's app keeps the old labels. After the evaluation, Codex found that a response naming the same old label twice could still count as a pair; the rule now leaves it pending. None of the 16 responses does that, and the result did not change. |
| **Comparison:** a general agent (Claude Opus 5.5, no Testigo) given the same 16 screenshots | Found the 3 changes, with no false alarms, and said the web was not ready. **Testigo did not find more.** |
| **Author sessions with the acta,** about 10 minutes each | First: reached no conclusion and could not find the cited screenshots, so the page was redesigned. Second: found the screenshot but could not name the seeded defect, and found the tasks unclear. The cards were then rebuilt with before/after columns and a line saying what to compare. Third: said the repaired web changed no data and found the screenshot to check. By then the author already knew the seeded defect, and one task was again unclear. These were three sessions by the author; nobody outside the team took part. Records in `20261103-testigo-hito2/evaluacion/`. |

**What this shows:** a general agent asked the right question finds this defect. Testigo does not detect better. What it adds is:
- a fixed question, repeated identically on every run;
- readings that refuse to choose when they disagree;
- an acta where every value is tied to its screenshot.

## Limits

- It covers only what was recorded and is visible. An acta with no differences does not mean the migration is correct.
- A field missing from both screens does not appear in the acta.
- No person has confirmed the label map. Spark's proposal matches the apps' field ids, but those are our own apps with 12 fields; on a real system someone has to confirm it.
- Navigation was automated. In Muse Code's app, the save was the same PUT its page sends, not a click.
- In real use, screenshots go to the API. Handling of personal data is not solved.
- No one outside the team has used Testigo.

## Built during the event

The event kicked off on 26 October 2026 at 14:00 (UTC-3). Everything in this repository was built between 1 and 4 November 2026.

| Folder | What |
|---|---|
| `20261102-testigo-hito1/` | The web test app, the legacy app's data and screenshots, the sealed reading prompt, the request runner, 60 readings, the evaluation and the acta. |
| `20261103-testigo-hito2/` | Muse Code's two runs, the captures, 32 readings, two actas, the comparison with a general agent and the author session. |
| `20261103-testigo-mapa-spark/` | Spark proposing the label map from both screens: preregistration, 16 requests, result and evaluation against the apps' field ids. |
| `index.html`, `visor/` | The acta page, built from the evaluators' output. |
| `video/` | Slides for the demo video (`diapositivas.html`; arrow keys to move). Counts and the seeded rows come from `visor/actas.js`. |

**Before the kickoff** there was planning only: the idea and the application to the hackathon. No code existed. Testigo was built by one participant, with the AI assistants listed below.

**AI assistants:**
- **Claude Code (Opus 5.5 and Sonnet 5.5)** wrote the reading prompt and schema, the label maps, the evaluators, the actas, the acta page, the label-map test, the video slides and this README. It prepared and checked the Muse Code runs; the migration itself was written by Muse Code.
- **Codex** (with subagents it coordinated) wrote the milestone 1 synthetic test apps, the screenshot capture helper and the API runner (`20261102-testigo-hito1/web/`, `20261102-testigo-hito1/grabar/` and `20261102-testigo-hito1/lectura-hito1/harness/`), prepared and ran the milestone 1 readings, and independently reviewed and reproduced the evaluations from a clean clone. It audited the repository before publication and found a bug in the label-map rule, fixed before that test was evaluated, and later a second one, described under *What was tested*. The source of the original Tk/SQLite legacy app, with the seeded defect and the web branches, is not published here. The version given to Muse Code, derived from it without the defect or the web branches, is in `20261103-testigo-hito2/entrada/`. The original's sealed answer key, seal and synthetic databases are, in `20261102-testigo-hito1/lectura-hito1/clave-y-bases/`.
- **Maxi** directed the work, authorized every Meta call, and ran the milestone 2 readings with their own key.

## Reproduce

The evaluations need Node 24 and no network:

From the repository root:

```
node 20261102-testigo-hito1/evaluacion/evaluar.mjs
node 20261103-testigo-hito2/evaluacion/evaluar.mjs
node visor/construir.mjs
node 20261103-testigo-mapa-spark/proponer.mjs evaluar
node 20261103-testigo-mapa-spark/probar-evaluar.mjs
```

The third command rebuilds `visor/actas.js`, the data behind `index.html`. The fourth evaluates Spark's 16 real label-map responses and rewrites `20261103-testigo-mapa-spark/RESULTADOS.json`. The fifth tests the label-map rule with invented answers.

The readings were made with `20261102-testigo-hito1/lectura-hito1/harness/` (milestone 1) and `20261103-testigo-hito2/lectura/harness/runner.mjs` (milestone 2). Both take the key from the `MODEL_API_KEY` environment variable, never retry, and stop if they cannot price a request.

Six sealed run files (`plan.json`, a capture receipt, two configs) had absolute paths with the author's user folder. On 3 November that folder was replaced with `%USERPROFILE%` and nothing else was changed. Because of that edit, those files no longer match the hashes recorded when they ran (`plan.sha256`, receipts, `RUN-LOCK.json`). `EDITED-PATHS.md` lists each file with its original hash. The published history carries only the edited versions; the author keeps the originals privately. The evaluations still give the same results. One receipt (`20261103-testigo-hito2/lectura/captura-reparada/receipt.json`) also holds an `ownerToken`: a local identifier the capture helper uses to guard its temporary browser profile. It is not an API key or a cookie and gives access to nothing. It is left for the same reason.

**API cost:** these are estimates from the usage receipts, not invoices.
- Milestone 1 readings (Spark and SAM 3.1): USD 0.32.
- Milestone 2 readings: USD 0.20.
- Label map proposal: USD 0.11.
- Both Muse Code runs: about USD 0.93.

## License

MIT, see [`LICENSE`](LICENSE). It covers the code and the invented data and screenshots in this repository.
