agentleFS
fabioc-alohafabioc-aloha/Alex_ACT_ONE/skills/figure-generator/SKILL.md

figure-generator

Skill0 starsChanged 9 days agoMITView on GitHub

Ship deterministic hand-authored SVG generators for print-quality figures backed by real published datasets. Ships the .mjs generator pattern reading from data/<slug>.json, the data-sha256 audit hash embed for provenance, the dataset-first rule with contract tests pinning headline numbers, the dataset-inversion procedure for reverse-engineering data from an approved sample SVG, the fix-in-generator-never-in-SVG rule, and the figure-count hoist to one JSON. Use when authoring reproducible book or report figures, when a figure's numbers might drift silently, or when reverse-engineering data from a graphic whose dataset is missing.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every shipped figure is emitted by a deterministic .mjs script that:

    line 23
  2. Very specific:

    Debugging is easier: When a figure looks wrong, console.log() inside the generator shows exactly the numbers it saw.

    line 60
  3. Distinctive:

    Every figure must be backed by a REAL, PUBLISHED dataset.

    line 66
  4. Very specific:

    Look for <text> elements near axis lines with numeric content.

    line 170
  5. Distinctive:

    When a project runs multiple gates that all depend on "how many figures are expected," hoist the count into one JSON file.

    line 192

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Figure generator discipline

Deterministic figure production for books, reports, and any artifact where a figure's numbers have to survive re-generation and stay auditable. Distilled from a shipped book: 53 figures across 14 chapters, every generator hand-authored, every dataset published.

The engineering side of the illustration workflow. print-svg-style-guide governs how figures LOOK; this skill governs how they get MADE, VERSIONED, and AUDITED.

When to invoke

  • Authoring a new figure for a book, report, or exec-facing document whose numbers should stay traceable
  • Re-authoring an existing figure whose data has drifted or whose script is missing
  • Reverse-engineering the underlying dataset from an approved sample SVG
  • Setting up a contract test for a new figure's headline numbers
  • Diagnosing why a figure's numbers do not match the surrounding prose

The generator pattern

Every shipped figure is emitted by a deterministic .mjs script that:

  1. Reads from data/<slug>.json — never inlines the numbers
  2. Computes a SHA-256 hash of the source data
  3. Embeds the hash in the SVG as an XML comment (<!-- data-sha256: ... -->)
  4. Emits to assets/figures/<NN>-<slug>.svg (or the project's chosen path)

Skeleton:

// scripts/generate-<slug>.mjs
import { readFileSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";

const DATA_PATH = "data/<slug>.json";
const OUT_PATH = "assets/figures/<NN>-<slug>.svg";

const raw = readFileSync(DATA_PATH, "utf8");
const data = JSON.parse(raw);
const dataHash = createHash("sha256").update(raw).digest("hex");

// ... layout math using data ...

const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480" font-family="Inter, system-ui, sans-serif">
  <!-- data-sha256: ${dataHash} -->
  <title>${title}</title>
  <!-- ... marks, labels, axes ... -->
</svg>`;

writeFileSync(OUT_PATH, svg);
console.log(`Wrote ${OUT_PATH} (data-sha256: ${dataHash.slice(0, 12)}...)`);

Why hand-authored and not a rendering library

  • Layout math is exact. Statistical-chart libraries (Vega-Lite, ECharts, Chart.js via the flint-chart MCP server) own final geometry — they stretch within a baseSize ceiling and return their own computedSize. Print-legibility gates and bounds gates need the generator to own layout, not the renderer.
  • Regen is byte-stable. Given the same dataset, a hand-authored generator produces byte-identical output. That is what makes data-sha256 a useful audit hash.
  • Debugging is easier. When a figure looks wrong, console.log() inside the generator shows exactly the numbers it saw. A renderer failure hides behind opaque layout errors.

flint-chart still has a role — as an EXPLORATION tool during the design pass (Step 5a of the [chart-big-idea → flint-chart → hand-author] pipeline). Flint helps pick a chart family faster than reasoning from scratch. The shipped artifact, though, is always a hand-authored generator when the target is print.

Dataset-first rule

Every figure must be backed by a REAL, PUBLISHED dataset. Never inline composite numbers. The rule holds for character-anchored figures and generic figures alike; the reader must be able to reproduce the scenario.

The publication set for each dataset:

  • data/<slug>.json (source of truth)
  • data/<slug>.csv (human-readable copy for readers who prefer tables)
  • data/<slug>.md (README describing schema, provenance, units)
  • data/<slug>.schema.json (JSON Schema so downstream consumers can validate)

For a companion-site workflow (static-site publication):

  • Publish to datasets/<slug>/ on the site (schema page + CSV + JSON + README)
  • Publication is a PREREQUISITE for shipping the figure, not a follow-up

Naming convention: <slug> describes the figure content, not the character or the chapter. q3-region-revenue is right; chapter-5-figure-4 is not. Readers look up datasets by concept, not by figure number, so the concept has to be in the name.

Contract tests (pin the headline numbers)

For every figure, write a contract test that pins the values the surrounding prose cites. Runs via Node's built-in test runner.

Skeleton:

// scripts/<slug>-contract.test.mjs
import { readFileSync } from "node:fs";
import { test } from "node:test";
import assert from "node:assert/strict";

const data = JSON.parse(readFileSync("data/<slug>.json", "utf8"));

test("Q4 aggregate matches the chapter total", () => {
  const q4Total = data.regions.reduce((s, r) => s + r.q4, 0);
  assert.equal(q4Total, 28400000); // "$28.4M quarter" in chapter prose
});

test("EMEA variance matches the prose claim", () => {
  const emea = data.regions.find(r => r.name === "EMEA");
  const variance = emea.actual - emea.plan;
  assert.equal(variance, 1700000); // "$1.7M variance" in chapter prose
});

test("Cohort 6 gain is 2.18x Cohort 1", () => {
  const c1 = data.cohorts[0].gain;
  const c6 = data.cohorts[5].gain;
  assert.equal(Math.round((c6 / c1) * 100) / 100, 2.18); // ordinal claim in prose
});

Run: node --test scripts/<slug>-contract.test.mjs

What to pin

  • Aggregate totals cited in prose ($28.4M quarter)
  • Per-segment values the prose names ($1.7M variance in EMEA)
  • Ordinal claims (2.18× larger, third-lowest, only region falling)

What NOT to pin

  • The full dataset shape (schema tests belong in a separate <slug>.schema.test.mjs)
  • Numbers that appear ONLY in the figure and never in prose (over-constrained; makes future dataset edits noisy)

Register the test

Many projects use an explicit test list (e.g., dd:contract in the DDA workflow). Dropping a new <slug>-contract.test.mjs into scripts/ does nothing if the harness enumerates its tests explicitly. Add the test to whatever list the project maintains. The tell is the test count: if it did not rise by the number of tests you wrote, you are not wired in.

Typical package.json edit:

{
  "scripts": {
    "dd:<slug>": "node scripts/generate-<slug>.mjs && node --test scripts/<slug>-contract.test.mjs",
    "dd:contract": "node --test scripts/<slug1>-contract.test.mjs scripts/<slug2>-contract.test.mjs ..."
  }
}

Both edits ship together — the per-figure script and the aggregate list.

Fix in the generator, never in the SVG

Manual SVG edits get clobbered on the next regen. The rule is absolute:

SymptomFix in
Number wrong in the SVGThe generator's layout math, or the dataset
Text truncatedThe generator's layout — see print-svg-style-guide § "when text won't fit" (cut / reflow / abbreviate / grow)
Color miscodedThe generator's palette lookup, not the SVG's fill attribute
XML-invalid comment (-- inside, bare &)The generator's comment emission — see render-verify § SVG XML invalid
Regenerating clobbers your patchThat is the rule working, not a bug

The one legitimate exception: a one-off, throwaway figure that will never regenerate. If you are SURE, hand-edit the SVG. But then treat that SVG as the source of truth and delete the generator, so the next contributor is not misled.

Dataset inversion (reverse-engineer the data from an approved sample SVG)

Sometimes an approved sample SVG exists but its underlying dataset is missing. Do not reconstruct the data from prose descriptions — prose fabricates. Invert the sample.

Procedure

  1. Extract axis tick coordinates and their labeled values from the sample.

    Select-String -Path <sample>.svg -Pattern '<text|<line' | Select-Object -First 40

    Look for <text> elements near axis lines with numeric content; those are tick labels. Match each label to the <line> coordinate it labels.

  2. Solve two ticks for the linear scale with this skill's bundled script, never by hand:

    node scripts/invert-svg.mjs <sample>.svg --tick-a 0 --tick-b 50

    The script selects tick labels aligned on the requested axis, pairs them with nearby tick-mark lines, and solves pxPerUnit = (y1 - y2) / (v2 - v1). It supports flat SVGs with explicit, double-quoted coordinates, not arbitrary rendered SVG geometry. Ambiguous axes, transforms, and missing supported points exit with code 2; isolate or flatten the intended chart before retrying. Example from a shipped case: Fig 6.6 solved to 2.25 px per point.

  3. Invert every data-point coordinate through the scale. The same script run inverts every circle centre and polyline / polygon point it finds (dataValue = vA + (pA - p) / pxPerUnit, with the scale's sign carrying the axis direction) and reports each derived value. Pass --grid <step> (or --precision <decimals>) for the domain's natural precision, and --write <path> to save the derived values as a JSON array ready to become data/<slug>.json.
  4. A necessary check on the derivation: every derived value resolves to an EXACT value on the natural grid. The script exits with code 1 and does not write a dataset if a point is off-grid. Fig 6.6's 24 data points all landed on integers, with no fractional pixels and no rounding fudge. Passing this check does not establish that the selected axis, marks, or units are the right ones; those remain semantic decisions.
  5. Feed the derived values into the generator and verify byte-identity. The output polylines should be byte-identical to the sample. That is the strongest evidence the dataset faithfully encodes the approved design.

Why inversion beats prose reconstruction

Prose fabricates. In a real case, prose said "EMEA is the only region falling" — the sample proved three regions decline and the two lines diverge (gaps 10 → 15 → 20 → 30). A generator authored from the prose would have shipped one wrong region trajectory. The sample doesn't lie, and the byte-identity check is the strongest possible verification.

Figure-count hoist (one JSON, not scattered constants)

When a project runs multiple gates that all depend on "how many figures are expected," hoist the count into one JSON file. Every gate reads from it. Adding a figure touches ONE file.

{ "expectedFigures": 62, "expectedPages": 439 }

Consumers read the same file:

  • check-placeholders.cjs — counts ![...]() declarations in chapter markdown
  • build-pdf.js — counts figures embedded as data-URIs during the build
  • check-rendered-figures.cjs — counts /Image XObjects in the built PDF
  • check-placeholders.test.cjs — the contract test for the above

JSON, not a .js module, is deliberate: if the root package.json declares "type": "module", the .js build script is ESM and cannot require(), while the gate scripts are .cjs. JSON is the only format both read without a bridge (CJS require, ESM readFileSync + JSON.parse).

Before hoisting, the figure count typically lives scattered across three or four files. Three of the four go silently stale on the next figure. Bump one number, hope the rest catch up, discover in a slow build that one did not. Hoist eliminates the class of drift.

Page count: measure, do not predict

expectedPages must be read off the PDF-build output, never assumed. A figure does not reliably add a page. In the DDA workflow, Fig 5.4 added a page; Fig 6.6 did not (439 → 439). Set the value AFTER the build, not before.

Anti-patterns

Don'tDo
Inline numbers in the generator (const q3Total = 28400000)Read from data/<slug>.json and hash the raw text into the SVG
Ship a figure without a contract testNumeric truth is the argument's substrate; drift without a test is silent
Hand-edit the emitted SVG to fix a defectGrep scripts/generate-* for the filename first — fix the generator, then re-run it
Reconstruct a dataset from prose descriptionsProse fabricates. Invert the sample instead
Bump the figure count without hoistingEvery gate that reads a figure count separately is a future stale-count bug
Predict expectedPages from figure countMeasure it off the built PDF
Skip test-registration ("the test file exists, it must run")Explicit test lists silently skip unregistered files. The tell is the test count did not rise
Ship a figure whose SVG passes every gate but is XML-invalidSee render-verify § SVG XML invalid. Inline HTML is lenient; <img> is strict

Related skills

  • print-svg-style-guide — how figures LOOK. Governs the SVG this skill emits.
  • chart-big-idea — Step 0.5 (earn-a-figure) and Step 4.5 (focus discipline) run BEFORE this skill fires. Decides whether the figure should exist.
  • flint-chart — exploration tool during figure design. Not the shipping path for print-quality figures.
  • render-verify — post-render check. § SVG XML invalid and § Prose-coupling check fire on the artifacts this skill produces.

Would revise if

  • A generated figure drifts from its data or design contract despite passing the prescribed checks
  • Dataset inversion fails on a sample whose derivation is correct, or the regenerated geometry fails the byte-identity check
  • A manual SVG correction cannot be reproduced by the generator

The generator pattern, contract-test discipline, and dataset-inversion procedure are book-tested across 53 shipped figures.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.