13 Commits
Author SHA1 Message Date
aleleba 7a1577b9d7 Phase 6.4.25c: correct flex.appendChild -- live verification shows it works
Live probing against Penpot 2.16.2 (repeated, on a dedicated scratch page,
cleaned up afterwards) shows board.flex.appendChild(shape) is a real
function, distinct from board.appendChild, arity 1, throws nothing, and
preserves visual insertion order -- unlike board.appendChild, which still
inserts at the front as documented. The prior "broken" claim traces to the
MCP server's own high_level_overview() text (CRITICAL / BROKEN / NEVER use),
never verified with a live throw, and possibly confused with a real doc bug
that mislabels the grid 3-arg example as board.flex.

Removes flex.appendChild from the lint's FORBIDDEN patterns and from the
gate 5 forbidden-behavior veto, updates PENPOT_API_VERIFIED.md with the
verified facts, and drops the now-obsolete pattern from the replay filters
in 05_build_dataset.py and 07_build_lora2_mix.py.
2026-08-04 17:40:16 +00:00
aleleba c7b1311928 Phase 6.4.25b: gate5 -- declare THRESHOLDS for the generalization probe prompts
gp-01..gp-04 (ferreteria, gimnasio, tienda de mascotas, panaderia) had no THRESHOLDS entry,
so the probe hard-failed before running a single prompt. Each entry mirrors the closest of
the 10 official gate5 prompts by structure/difficulty, so the probe's pass/fail criteria stay
comparable to the official gate even though it is explicitly a separate, additional check.
2026-08-04 06:19:15 +00:00
aleleba cc252d70f5 Phase 6.4.25b: gate5 -- allow explicit ack of known train/prompt overlap (GATE5_KNOWN_OVERLAP_IDS)
The 4 uploadMediaUrl reinforcement seeds added in the corrective round (134-137) share a
6-gram with g5-03-card-producto ('foto real traida de https picsum'). v2b-bf16 is already
trained on that text -- editing the seed corpus now cannot retroactively decontaminate it.
This lets the disjoint check acknowledge that specific, already-baked-in overlap explicitly
(never silently) instead of hard-aborting gate5 full mode entirely.
2026-08-04 05:22:32 +00:00
aleleba 959404823a Phase 6.4.25b: parametrize gate5 prompts path, add generalization probe
GATE5_PROMPTS_PATH env var lets the same 17-metric harness (veto system,
MCP retry/page handling) run against a different prompts file. Unset
behavior is unchanged -- the official gate 5 (data/gate5_prompts.jsonl,
10 frozen prompts) stays untouched.

data/gate5_generalization_probe.jsonl: 4 prompts in rubros confirmed
absent from the 138-seed corpus, the 10 gate5 prompts, and the 60-prompt
holdout -- ferreteria, gimnasio, tienda de mascotas, panaderia. Swapped
out "veterinaria" (originally planned) after finding seed 83 is a
substantial design-repair seed for a vet clinic; "tienda de mascotas"
verified clean instead. This is an additional check, separate from the
official gate 5, meant to test whether the correction generalizes across
business types rather than overfitting to the pizzeria case.
2026-08-03 22:53:07 +00:00
aleleba 3919b6baff Phase 6.3.17: close the production baseline with three repetitions
Ten prompts measured three times against vllm-qwen36, all with the same
23-metric harness. Three earlier result files are kept but excluded from
the consolidation and named for why: one used 14% of the server's
instructions block, two predate the audit-root fix and report zero shapes
by construction.

The consolidated numbers correct the previous report, which was wrong.
Production does not fail to draw. It creates structure and text - up to 40
shapes and 24 texts on the ambiguous-brief prompt - and then fails to
colour any of it. Distinct fill colours run 0 to 1 across all thirty
measurements.

Mean score per repetition: 6.79, 8.25, 0.00. No prompt reaches 60 in any
repetition.

Reporting the veto rate separately from the score turned out to matter more
than expected, so it is now the headline number alongside it. Of thirty
measurements, 21 violate the forbidden-API veto and 15 use nothing but
Penpot's three default colours; only 6 are veto-free. The score alone
collapses those into a zero that cannot distinguish "broke an API rule"
from "made an ugly design", and the two need different fixes.

That also explains why single measurements looked stable: the score is
pinned at zero by the veto, not by model consistency. All the real variance
sits in the prompts that do not veto, where scores swing 20 to 35 points
between identical runs. So the repetitions matter more for the trained
model, which should land in that non-veto regime, than for this baseline.
The eval window at the end has to budget for three repetitions on that side
too, or the comparison is asymmetric.
2026-07-30 21:01:59 +00:00
aleleba 869b00c924 Phase 6.3.17: make gate 5 measure all ten prompts in one resilient run
The Penpot MCP keeps going down and each window where it works is
expensive - three of the user's windows were spent on runs that aborted
partway. The run is now built to finish rather than to be correct about
why it stopped.

One prompt failing no longer ends the run. Failures are collected and
retried on a later pass, up to three passes, with a clean handshake and a
probe between them. Even an unexpected exception is caught per prompt,
because one bug in one prompt must not take down the other nine. The
opening probe no longer aborts either: a plugin that does not answer now
may answer on pass two, and aborting there throws away the whole window
for a transient state.

Two time budgets bound it: four minutes per prompt, checked before every
turn of the agent loop, and forty-five minutes of wall clock. Both are env
vars. A hang used to mean waiting forever; now it costs one prompt and the
run continues.

Results are written after every prompt, so a crash costs at most the
prompt in flight, and the JSON is only marked complete when all ten have a
score.

The run also inventories the accumulated gate5 pages at the end and writes
the list to disk. It does not delete them: the pages this run created are
already emptied after their PNG is exported, but the ones from earlier runs
are in the user's own file, so that is offered rather than assumed.
2026-07-30 20:27:46 +00:00
aleleba ad62277f14 Phase 6.3: stop the gate depending on penpot.root, and stop blaming the plugin for its own bugs
The setup returned penpot.root.id and the run aborted when it came back as
an empty string on the second prompt of a batch. penpot.root is the root of
the ACTIVE page, and after createPage plus openPage it need not have caught
up yet - a race the page-emptying code introduced, since that leaves the
emptied page active. The value was never useful anyway: every page shares
the same root id, so it identified nothing. Setup now returns only pageId,
which is unique and stable, and both the audit and the cleanup use it.

The diagnostic message is the second half of the same mistake. It printed
"PLUGIN DEGRADADO, ask the user to reload the browser" whenever any prompt
went unmeasured, regardless of why - so it said that for a failure that was
entirely the gate's own. A message that sends the user to reload their
browser when the bug is mine costs both of us time. It now classifies on
the error text: task timeouts and transport drops point at the plugin,
anything else points at the gate and says so explicitly.

Also closes two evaluation leaks the gate's own pre-flight caught, both in
the seeds rather than the gate prompts, since the prompts have to stay as a
real user would write them:
- A seed shared the 6-gram "la home de una escuela de" with gate prompt 9.
  My first fix was overwritten by a subagent still writing the file, which
  is why it reappeared.
- A seed used the same business as gate prompt 9 - a music school - without
  sharing any 6-gram. Shingles cannot see that: two texts describe the same
  business without sharing words. Training on the domain we then evaluate
  inflates the result invisibly. So the gate now also checks that no seed
  uses any of the gate's business nouns, listed explicitly.

The training mix was rebuilt: it had been assembled before the 20
ambiguous-brief seeds existed, so training on it would not have used the
corpus that was audited. The ambiguous-brief class gets its own mix portion
rather than being folded into design, because diluted across 76 design
seeds it would be at the mercy of a ratio, and that is the class the user
named as the main painpoint. 125 seeds, 168 unique payloads, 446 distinct
user prompts, 901 train and 99 eval.
2026-07-30 20:07:40 +00:00
aleleba 387b237ee5 Phase 6.3.17: run gate 5 in batches and stop burning prompts on a degraded plugin
Measured across three consecutive runs: the Penpot plugin reliably handles
5 or 6 heavy prompts and then degrades to 30-second timeouts on createPage,
always with the same shape - the first few work, the rest fail in setup
without exception. That is not random flakiness.

Prompts now run in batches of 4 with a clean handshake between batches,
giving the server a recovery point before the deterioration sets in. A
setup failure no longer burns the prompt as measured-with-score-None: it
goes on a pending list, the batch stops, and the run exits 3 with the exact
GATE5_ONLY line to resume. Insisting past the first timeout only spends
pages and dirties the JSON, since once the plugin starts timing out the
rest fail identically.

On the 30-second timeout the user asked to raise: it is the MCP server's
own limit on a plugin task, not a client timeout, so it cannot be raised
from here. What can be done is not to approach it. generateStyle with
includeChildren plus generateMarkup serialise the whole subtree and are by
far the most expensive part of the audit, so above 400 nodes they are
skipped and rendersOk becomes not-applicable rather than risking the entire
audit - and with it the prompt's measurement - timing out. That required
fixing the scoring too: a metric whose VALUE is None is now not-applicable,
like one whose threshold is None. Counting "could not measure" as a failure
would have penalised exactly the large designs the gate is meant to reward.
2026-07-30 19:39:49 +00:00
aleleba f63ff6830d Phase 6.3.17: fix the audit root, add fill metrics, and stop the gate degrading the file
Three defects, the first found by inspecting the user's Penpot file live
while the gate reported something else.

The audit resolved the page root with penpotUtils.findShapeById(rootId).
Every new Penpot page shares the same root frame id,
00000000-0000-0000-0000-000000000000, and findShapeById searches globally,
so that lookup always returned the root of the FIRST page in the file - the
user's empty one - rather than the page the gate had just created. That is
why the baseline reported shapeCount 0 on all ten prompts while the file
actually held 28 shapes and 20 texts. It resolves by pageId now, which is
unique.

With that fixed the baseline reproduces the reported symptom exactly rather
than something worse: the button gets 4 shapes, all grey; the navbar gets
10 shapes and 6 real texts with a single distinct fill colour. Production
does create structure and text. What it never manages is to apply colour -
it creates the shapes in a call that works, then sets fills in a later call
using Figma syntax, that call throws, and the shapes keep the #B1B2B5
default. So "creates grey boxes" was accurate and "creates nothing" was my
measurement error.

Two metrics now capture that directly, since neither distinct-colour counts
nor placeholder-grey counts see it - #FFFFFF and #000000 are not mid greys,
so a design where every fill is a default passes both. explicitFillShare is
the share of filled shapes whose colour is not one of Penpot's three
defaults, thresholded by difficulty (0.80 high, 0.65 medium, 0.50 low,
since a small artefact's structural neutrals weigh heavily in a ratio). And
onlyDefaultColors is a veto: true when the whole palette is those three.
White and black stay legitimate when chosen - the distinction is
co-presence, not the hex. They are only suspicious when they are all there
is; alongside chosen brand colours they also count as neutrals in
paletteStructured.

resumen() crashed adding None scores from failed prompts. Beyond the crash,
an unmeasured prompt must not average in as a zero: "could not measure" and
"the model did it badly" are different, and averaging them would have made
a mid-run plugin outage look like cheap quality. They are reported
separately and any missing prompt makes the verdict invalid, because a
baseline with 5 of 10 measured is not a baseline.

The gate now empties its own page after exporting the PNG. Across three
runs the plugin reliably handled 5 or 6 heavy prompts and then degraded to
30-second timeouts on createPage - that is not random flakiness, it tracks
the file growing by one page per prompt, so the gate was manufacturing its
own failure. The evidence that matters is the PNG plus the JSON metrics,
not the live page. A page is kept only when its PNG failed, so there is
something to inspect. Exit code 3 now distinguishes "plugin degraded
mid-run, reload it and resume these ids" from "plugin not connected".
2026-07-30 19:35:42 +00:00
aleleba 8094183940 Phase 6.3.17: fix a harness fidelity bug and add a vibrancy metric
The first baseline measured production with only 2290 of the 16392
characters of the server's instructions block - 14%. The missing 86% is
exactly the API grounding: Core Shape Properties and Methods, Layout
Systems, Text Elements, and The penpot and penpotUtils Objects, which is
where insertChild, resize(), the layouts and penpotUtils are documented.

That was worth catching, because the discrepancy had a visible signature:
the measurement said production creates nothing, while the user's real
Claude Code session produced grey boxes, i.e. shapes greater than zero.
When a harness and reality disagree, the harness is the first suspect. In
phase 5 a low max_tokens manufactured an apparent regression the same way.

The gate now injects the full document, minus the trailing "You have hereby
read the Penpot High-Level Overview" line, which is framing of the tool
response rather than part of the instructions block and would otherwise
tell the model it had already read something.

The finding survives the fix. Across the five prompts measured cleanly
under the corrected condition, shapeCount is still zero on every one. So
the API invention is not an artefact of withholding documentation from the
model - it happens with the documentation present.

Also adds the vibrancy requirement the user raised as first-class scope:
given an ambiguous brief the model must choose and justify a palette rather
than ask or fall back to defaults. Neither distinctFillColors nor
placeholderGreys distinguishes a vibrant palette from a muted but
technically non-grey one, so four metrics are added: chromaticFills,
meanChromaticSaturation, paletteStructured (a dominant brand hue, an accent
at least 30 degrees away, and neutrals), and finalMessageListsHex, because
a palette chosen in silence cannot be adjusted by the user.

The saturation floor of 45 is derived, not asserted: measured over the 325
non-neutral fills of this phase's hand-authored corpus, median HSL
saturation is 75, p25 is 48 and p10 is 35. A floor of 45 sits just under
the first quartile and is cleared by 79% of those fills, so it is a floor
the target behaviour already clears rather than an aspiration. The
lightness band of 15 to 85 excludes near-blacks and near-whites, which can
compute as highly saturated while reading as neutral.

Gate prompt 6 becomes the user's literal failing sentence, and two
ambiguous-brief prompts are added. One of them had to be re-domained after
the disjointness check found it shared a 6-gram with a seed - the check
fails on a single shared shingle, which is what makes it useful.

Results so far are partial: prompts 1-5 measured cleanly, 6 has a timed-out
audit and 7-10 hit the MCP outage, so those get re-measured. Both runs are
kept, the 14% one renamed to record what it was.
2026-07-30 18:38:24 +00:00
aleleba 5f0ddd962c Phase 6.3.17: measure the gate 5 baseline against production
Ran the full agent loop against vllm-qwen36 on port 8000 (read-only HTTP)
with the Penpot plugin live, before asking for any downtime. Without this
file "it improved" would be a claim rather than a measurement.

Result over the 8 graded prompts: mean score 14.9, zero prompts at or above
60, veto violated on 2 of 8, 66% of execute_code calls raised, and 7 of 8
prompts burned all 14 turns without producing a final message.

The plan predicted production would score near zero on distinct colours and
style richness while producing a high shape count - grey boxes. The shape
count is also zero. On a fresh page it creates nothing at all, so the
failure sits upstream of the grey boxes: the model invents a Figma-shaped
API wholesale and every call throws. From the captured turns:

  penpot.currentPage()                     is a property, not a function
  penpot.createRectangle(page, 200, 56)    takes no arguments
  penpot.createText(page, ...)             takes one, the text
  penpot.getPageById(...)                  lives on penpotUtils
  fills   = [{type:'solid', color:{r,g,b,a}}]   is {fillColor, fillOpacity}
  shadows = [{type:'drop', x, y, blur, ...}]    is {style, offsetX, offsetY}

It then spends the remaining turns querying penpot_api_info without
recovering. So the reported symptom understated it.

Two robustness fixes the run itself forced, both after losing a completed
run to them:
- A ConnectionError does not just drop the request, it can drop the MCP
  session, so retrying the same tools/call against a dead session fails
  identically every time - which is exactly what the first attempt showed,
  four retries and four identical ConnectionErrors. The client now redoes
  the handshake before retrying, and that recovered two drops in this run.
- Results are written after every prompt. The first attempt died on prompt
  4 and lost the three already measured, which is the expensive data
  precisely because it requires production to be up.
2026-07-30 18:14:03 +00:00
aleleba c65d309719 Phase 6.4: make the gates fail when they cannot verify something
A code review found seven ways these gates could pass green with something
actually wrong. All are the same family: a missing value was treated as OK.
The rule now written into all three files is that absent is not OK, absent
is "could not verify", and that either fails or is reported as an explicit
SKIP - it never slips through as green.

30_eval_suite.py:
- A bucket with no baseline of its own fell back to the global 0.2750 and
  printed it in a column headed "baseline", as if it were that bucket's
  number. Measured against the real eval.jsonl buckets: negativos going
  from 0.12 to 0.33 is a real +0.21 regression, but the computed delta was
  +0.055 and it PASSED; manejo_errores sitting unchanged at 0.42 produced
  a fabricated +0.145 FAIL that would have discarded a healthy candidate
  mid-downtime. Now such buckets print SKIP and the verdict reports how
  many went unverified.
- "VEREDICTO: FAIL" exited 0, so a runbook chaining the gate into
  quantization would have carried on to write 24 GB. Now exits 1.
- A typo in BASELINE_BUCKET_LOSSES silently matched nothing; now aborts.
- The penpot exemption is labelled honestly: those 11 rows are pre-existing
  LoRA #1 tool-calling, not new capability, so gate 1 has no regression
  coverage there and the log says so.

20_merge_lora.py dry-run (merge path untouched, verified by AST diff):
- adapter_config.get("use_rslora", False) meant a missing key passed AND
  the log printed use_rslora=False, asserting it had checked something that
  was never there. A different PEFT version omitting a key was enough.
- lora_bias was not checked at all, only bias. They are different fields:
  lora_bias puts a bias inside lora_B, which W + scaling * (B @ A) ignores.
- The 620 keys were printed but never asserted, so an adapter with extra
  tensors printed "310 + 310 = 930" and passed.
- rank_pattern/alpha_pattern were not checked. They set r per module, so
  scaling is not uniformly alpha/r while both the dry-run and the merge
  apply a single 2.0 to all 310 tensors.
- A missing family was invisible: swap linear_attn for 150 mlp.gate targets
  and the total is still 310, no norm is zero because the family is simply
  gone, and it passed. Now presence and per-family counts are asserted,
  derived from the real adapter: linear_attn 150, shared_expert 120,
  attention_qkvo 40, otros 0.
Verified against seven synthetic adapters plus the real phase 3 one; only
the correct adapter passes.

21_quantize_nvfp4.py (recipe and oneshot untouched): the calibration cache
now carries a provenance.json recording the training file's sha256, the
recipe numbers and the bucket distribution, and loading aborts on mismatch.
This is the phase's number one risk and it had no mechanical defence: the
phase 5 cache on disk has exactly 512 rows, the same as the v2 recipe, so
the only existing check could not tell them apart and reusing it would have
calibrated with zero design data and washed out the new capability
silently. Verified: that cache now aborts.

gate 5: retry transport failures against the Penpot MCP, which drops
connections mid-call intermittently (seen before in phase 4's gate 4).
Without it a blip on prompt 6 of 8 kills a whole run and reads like a model
failure. PluginNotConnected is deliberately not retried - that is a real
state of the world. Also unwrap the {"result":..., "log":...} envelope the
server wraps execute_code returns in; the gate was reading keys off the
outer object and rejecting a valid page setup.
2026-07-30 17:37:46 +00:00
aleleba 8ea4572edd Phase 6.3: add gate 5, design quality in Penpot
Unlike gates 2-4 this one needs a real agent loop - model, tool call, live
MCP, result, up to 14 turns - because design quality only exists after the
code executes. It talks to vLLM over the OpenAI API and to the Penpot MCP
over HTTP (initialize, notifications/initialized, tools/list, tools/call),
handling both application/json and text/event-stream responses. Endpoints
and credentials come from env with no defaults and are never printed or
stored; requests errors are reduced to the exception type because the
requests message embeds the URL.

Eight graded prompts, a fresh page per prompt named gate5/<tag>/<id>/<ts>,
and the gate never deletes anything. The 17 metrics are computed by an
audit payload the gate injects, not the model. placeholderGreys and
forbidden behaviour are veto metrics: any hit scores that prompt 0.

Two things worth calling out.

The forbidden-pattern regexes are imported from 07_lint_penpot_code.py
rather than duplicated, and the gate runs those same regexes over its own
setup and audit payloads at startup - a gate that violated the API it is
grading would be measuring its own bug.

The holdout mode had a silent failure that is exactly the kind this phase
exists to catch: with the endpoint down, all 60 generations failed, each
entered the denominator with zero forbidden patterns found, and the gate
reported 0% forbidden API and APPROVED. Since that number is the fallback
trigger, a false pass there would have launched the quantization run.
Request errors are now counted separately, never enter the denominator,
and block approval outright.

Known issue, resolved separately: gate prompt 6 is the exact production
failure ("hazme una landing page de una pizzería con colores vibrantes"),
and the flagship B6 seed was written to the same wording. Shingle overlap
measures 20%, under the 34% threshold, but the seed prompt is a literal
substring of the gate prompt - the threshold is too loose for prompts this
short. The seed's domain gets changed rather than the gate's, so gate 5
measures transfer instead of memorisation; the real pizzeria prompt still
runs in the human acceptance test, which is the criterion that decides.
2026-07-30 17:18:01 +00:00