content/auto-draft
needs attentionviewing older commit5c57414 · incrementalPR #325reviewed 2026-07-20 22:29 UTC2H · 7M · 10L · 8I- Purpose
- Autonomous content draft for the GDMTO vs GDMTH comparison blog post — a primary keyword cluster target (informational: tarifa GDMTH, GDMTO vs GDMTH).
- Goal
- Publish a complete, factually-grounded GDMTO vs GDMTH guide with interactive SVG charts backed by real CFE tariff rates, verified by unit tests.
- Sub-goals
- SG-1: MDX content — full article with hero chart, load profiles chart, demand chart, FAQ structured data
- SG-2: SVG charts in charts.tsx — GdmtoVsGdmthDecisionChart, LoadProfilesChart, GdmtoVsGdmthCostChart, DemandaFacturableSolarChart
- SG-3: Real CFE tariff rates (VdM Centro, jun-2026) grounding the cost comparison
- SG-4: Pure math module (charts.tariffs.ts) extracted for testability + unit tests (charts.calculations.test.ts)
- SG-5: llms.txt updated with API + blog guide links for GEO
- What
- This incremental pass clears remaining loop-review nits: (1) MDX prose polished — em-dashes replaced with parentheses, multi-bullet paragraphs condensed; (2) llms.txt gets /developers and Guías destacadas section; (3) charts.tariffs.ts extracted as a new pure module with RATES/PROFILES/bill functions; (4) charts.calculations.test.ts adds unit tests locking the published peso figures; (5) charts.tsx visual fixes — decision chart height (caption clipping), cost segment colors, immutable stacked bars, winner pill, LoadProfilePanel component extraction, band overflow fix.
- Why
- Lock the math (tests prevent silent arithmetic drift that would mislead readers), fix visual regressions from previous passes (clipped captions, grey energía segment, band overflow), and improve GEO signal in llms.txt.
- Areas
- apps/web/content/blog+178−0apps/web/src/components/blog+440−2apps/web/public/llms.txt+6−0apps/web/src/app/(marketing)/blog+2−2
- Blast
- 7 files, +627/−6 lines. All changes confined to apps/web — blog content, blog chart components, and llms.txt. No domain logic, no API, no infrastructure.
Findings · 27
correctness2
dfCap test provides no regression protection against formula change
apps/web/src/components/blog/charts.calculations.test.ts:24
Same root cause as the tests lens finding — tautological assertion. Low correctness impact since RATES.days is a const, but flagged for completeness.
GDMTH capacidad-wiring test is indirect
apps/web/src/components/blog/charts.calculations.test.ts:52
`gh.capacidad < go.capacidad` doesn't structurally assert `gh.capacidad === dmaxPunta(concentrado) * RATES.gdmth.cap`. A regression that uses dmaxMonth for GDMTH capacidad could still pass if the resulting charge remained lower.
security1
No security concerns
All SVG is server-rendered static. No dynamic user data, no innerHTML, no network calls. Pass.
conventions9
File-level docstring explains WHAT, not WHY
apps/web/src/components/blog/charts.tariffs.ts:1
The opening JSDoc block describes what the module contains rather than why it was split. The only non-obvious WHY worth keeping is the testability rationale ('importing the JSX module stalls vitest on transform'). Trim to that one line.
Second docstring is a content summary of RATES
apps/web/src/components/blog/charts.tariffs.ts:8
The JSDoc above RATES describes what the constant contains — readable from the const itself. The only worth-keeping fact is source + effective date. Reduce to a single inline comment.
PROFILES docstring restates data shape
apps/web/src/components/blog/charts.tariffs.ts:18
Most of the PROFILES JSDoc ('two 24 h load curves, same kWh, different shape') is readable from the const. The ToU block boundaries `base [0,6), punta [18,22)` are non-obvious and worth one line — the rest can be removed.
Test file opening docstring describes what tests do
apps/web/src/components/blog/charts.calculations.test.ts:1
The block comment explains what the functions produce and that tests 'lock the maths' — both visible from the test names. Keep only the last sentence ('Expected values recomputed from RATES by hand') which justifies the hardcoded expected values.
REF_KW inline comment is a 'what', not a 'why'
apps/web/src/components/blog/charts.tariffs.ts:16
'GDMTH eligibility threshold, drawn as a reference line' restates what the variable name already conveys. Remove.
blockKWh and dfCap JSDoc comments describe the obvious
apps/web/src/components/blog/charts.tariffs.ts:22
'Monthly kWh split by ToU block, from the curve.' and 'Demanda-facturable cap: Energía ÷ (FC · 24 · días).' restate what the function names and signatures already communicate. Remove.
Winner pill comment is the one justified WHY in this diff
apps/web/src/components/blog/charts.tsx
The comment explaining full opacity ('dimming the loser turned its dark-violet energía segment grey — indistinguishable from cargo fijo') documents a non-obvious visual failure mode. Keep it.
Height change comment is acceptable
apps/web/src/components/blog/charts.tsx:682
'room for the threshold captions at y≈298 (was 300 → descenders clipped)' explains a magic number and prior failure mode. Justified.
LP_* constant extraction is appropriate
apps/web/src/components/blog/charts.tsx:857
Pure configuration with no closure dependency — hoisting reduces re-allocation. Noted as info since improvement lens flags the namespace aspect separately.
tests7
dfCap test is tautological — verifies the formula against itself
apps/web/src/components/blog/charts.calculations.test.ts:34
The test computes `90000 / (fc * 24 * 30)` inline — the same expression the implementation uses. It passes even if the formula is wrong. Replace with a concrete expected value computed offline, e.g. `expect(dfCap(90000, 0.55)).toBeCloseTo(227.27, 1)`.
dfCap cap-binding branch is never exercised
apps/web/src/components/blog/charts.calculations.test.ts:39
Both profiles have peak demand well below dfCap, so `Math.min(dmaxMonth, dfCap(...))` always returns dmaxMonth. The branch where dfCap is the binding constraint (high-load-factor site) is a real code path in both bill functions but is never tested. A curve like `Array.from({length:24}, () => 250)` (180 000 kWh/mo, dfCap ≈ 454 kW < 250 false — actually 454 > 250 so still dmaxMonth — try 350 kW flat) would trigger it.
GDMTH distribución demand basis has no direct assertion
apps/web/src/components/blog/charts.calculations.test.ts:56
The test confirms `gh.capacidad < go.capacidad` (punta vs month peak) but never asserts that GDMTH's distribución uses dmaxMonth. If both charges were accidentally wired to dmaxPunta the bill totals change (covered by the snapshot), but the structural rule is not an explicit invariant.
dmaxMonth is exported but not directly tested
apps/web/src/components/blog/charts.calculations.test.ts:2
dmaxMonth is exercised only indirectly through bill functions. A direct assertion like `expect(dmaxMonth(concentrado)).toBe(206)` would make the intent explicit.
Bill totals tested with Math.round — sub-peso drift undetected
apps/web/src/components/blog/charts.calculations.test.ts:41
Rounding to integer allows ±0.49 peso drift per assertion. `toBeCloseTo(expected, 1)` on the raw float would tighten the guard without changing test intent.
2.0–2.7 $/kWh range bounds not sourced
apps/web/src/components/blog/charts.calculations.test.ts:65
The range sanity guard is reasonable but bounds aren't derived from any CFE reference. If rates update, the test would silently pass even with implausible values. Consider annotating the source.
Tests cover only the two canonical PROFILES
apps/web/src/components/blog/charts.calculations.test.ts:10
No edge-case curves tested (all-zeros, single spike, dfCap-triggering high-LF curve). Acceptable scope for an article-claims test suite, but the dfCap branch gap (medium finding) could be closed with a synthetic fixture.
improvement4
Import from charts.tariffs placed at bottom of module
apps/web/src/components/blog/charts.tsx:733
`import { RATES, REF_KW, PROFILES, billGdmth, billGdmto, type Profile } from "./charts.tariffs"` appears after hundreds of lines of existing code. ES imports are hoisted so it works, but bottom-of-file placement violates universal convention and confuses readers scanning the file header for dependencies. Move to the top.
reduce stacking adds complexity without correctness benefit
apps/web/src/components/blog/charts.tsx:813
The `SEG.reduce(...)` builds an intermediate array then immediately maps it — two passes, extra allocation, non-null assertion, nested spread. The original `let cum = 0` was scoped safely per bar render. In a server-rendered SVG this is trivial overhead, but the added complexity is not justified by the stated immutability goal.
LP_* module-level constants serve only LoadProfilesChart
apps/web/src/components/blog/charts.tsx:857
Six LP_* constants are module-scoped but used by a single component. The LP_ prefix is a naming workaround for the lack of lexical scoping. They could live inside or directly above the component.
LoadProfilePanel key omission on root g is intentional and correct
apps/web/src/components/blog/charts.tsx:881
Old closure returned <g key={p.name}>; named component returns <g> without key. Since LoadProfilePanel is called directly (not in .map), no key is needed — this is the correct fix.
seo4
Verify /developers route is live before shipping
apps/web/public/llms.txt:4
The new llms.txt entry links https://batuenergy.com/developers. Confirm the route is live and server-rendered; a 404 in llms.txt signals errors to AI crawlers.
Prose condensation removes minor keyword signal
apps/web/content/blog/gdmto-vs-gdmth.mdx
Removing the em-dash parenthetical '—lo que casi todos vienen a buscar—' slightly weakens explicit search-intent matching in the intro. The bold on 'cuál te conviene' partially compensates. Minimal SEO impact.
Guías destacadas section improves GEO extractability
apps/web/public/llms.txt:12
Adding labeled blog URLs for primary-keyword topics (GDMTO vs GDMTH, cómo leer recibo CFE, tarifa GDMTH) directly targets the informational cluster in CLAUDE.md. Good GEO signal.
Blog URLs in llms.txt use canonical apex host
apps/web/public/llms.txt:13
All new URLs use https://batuenergy.com/blog/* — no www, no trailing slash inconsistency. Consistent with canonical host convention.
History · 14 commits
- c27ea57needs attentionincremental3H · 5M · 4L2026-08-04 17:46
- ff69645needs attentionincremental0H · 6M · 8L2026-07-28 18:21
- 0257d74safeincremental0H · 0M · 0L2026-07-20 23:00
- 0795a9dsafeincremental0H · 0M · 2L2026-07-20 22:42
- 5c57414needs attentionincremental2H · 7M · 10L2026-07-20 22:29current
- 5a57a8fneeds attentionincremental0H · 5M · 8L2026-07-20 17:14
- cc9056eneeds attentionfull1H · 3M · 9L2026-07-20 16:40
- 8b79d33needs attentionincremental5H · 7M · 11L2026-07-14 18:10
- f0c337cneeds attentionincremental1H · 1M · 5L2026-07-14 17:15
- 5c9de97needs attentionincremental0H · 5M · 7L2026-07-14 17:02
- 92cce08needs attentionincremental1H · 4M · 3L2026-07-14 02:00
- b7dd610needs attentionincremental0H · 6M · 7L2026-07-14 01:43
- 8c43554needs attentionfull0H · 2M · 1L2026-07-14 01:15
- 2149391blockedfull8H · 6M · 7L2026-07-10 17:30