Skip to content

PSTricks semantic layer + conformance harness against real PSTricks - #26

Merged
pyramation merged 22 commits into
mainfrom
feat/pstricks-semantic-layer-and-conformance
Aug 16, 2026
Merged

PSTricks semantic layer + conformance harness against real PSTricks#26
pyramation merged 22 commits into
mainfrom
feat/pstricks-semantic-layer-and-conformance

Conversation

@pyramation

Copy link
Copy Markdown
Collaborator

Two commits: the semantic-layer rewrite, and a harness that checks it against genuine PSTricks.

Semantic layer

Parser rebuilt around an ordered AST with diagnostics and source-order rendering. Command coverage roughly doubles — pscustom, psgrid, psdots, psellipse, psbezier, pscurve, psecurve, psccurve, pswedge, multido — plus text macros, header environments, and a new expression parser in @latex2js/utils for algebraic plot bodies.

Adds a Vite playground with a headless render page, a Playwright suite over every corpus example, and CI running both.

pnpm test is green across all 10 projects.

Conformance harness (tools/pstricks-conformance/)

Renders the same source twice — LaTeX2JS and real PSTricks in pyramation/pstricks-latex — and ranks pairs by disagreement. Ground truth is the spec made visual.

  • render-examples.mjs — splits the 24 example files into 57 pictures, renders each. 53/57 succeed in ~30s.
  • fuzz-corpus.mjs — 336 systematic cases: every command × style axis, plus draw-order and degenerate-input probes. 334/336 render.
  • compare.mjs — scores pairs on ink / colour histogram / layout occupancy.

The score is deliberately a triage ordering, not a gate — SVG and Ghostscript output cannot match pixel for pixel.

Why this is needed: the existing corpus cannot measure coverage. \psline appears 256 times across all examples while every command added here appears once, and \psecurve appears zero times despite being implemented.

What the harness already found

Not fixed in this PR — recorded here so they are tracked.

  1. Interactive re-render discards draw order. psgraph.ts pspicture()/userEvent removes and re-appends .userline/.psplot, so they jump to the top layer and reorder among themselves on first pointer move. The initial draw respects source order; the interactive path does not. A diagram is correct until the pointer moves over it.
  2. \pswedge draws concave arcs. 17-pie renders as a petal/star rather than a filled pie. Source is pure PSTricks, no shims.
  3. Only fillstyle=solid is implemented, and the fallbacks disagreehlines becomes a solid fill on pspolygon/psarc but no fill on psellipse/pswedge.
  4. \section renders as literal source text, and the proof environment emits an undefined \qed.
  5. Starred forms (\psframe*) honour fillcolor; PSTricks fills with linecolor and ignores it.
  6. A malformed macro copy-pasted into 3 corpus files\userline...(2,2)(sin(x)}{-y}, where ( should be {. LaTeX rejects it; LaTeX2JS parses it silently.

Dialect divergences needing a decision

LaTeX2JS accepts input PSTricks rejects: pow(a,b), infix bodies without algebraic=true, variable plot bounds ({alpha-3}), a bare algebraic key, plotpoints=1 (PSTricks requires at least 2), implicit multiplication (0.5x^3). Each is keep-the-extension or conform.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf

pyramation and others added 14 commits August 15, 2026 15:59
Rewrites the parser around an ordered AST with diagnostics and source-order
rendering, and roughly doubles the PSTricks surface.

Commands added: pscustom, psgrid, psdots, psellipse, psbezier, pscurve,
psecurve, psccurve, pswedge, multido. Text macros and header environments
gain textbf/textit/texttt/underline, itemize/description, and the theorem
family. A new expression parser in @latex2js/utils evaluates algebraic plot
bodies (x^2, implicit multiplication, ternaries).

Adds a Vite playground with a headless-friendly render page, a Playwright
suite covering every corpus example plus interaction, and a CI workflow that
runs both and uploads the rendered gallery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Renders the same source through LaTeX2JS and through genuine PSTricks in
Docker, then ranks the pairs by disagreement, so conformance questions are
answered by comparison rather than by reading code.

render-examples splits the project's own examples into individual pictures
(57 from 24 files) and rasterizes each via latex/dvips/ps2pdf/gs; fuzz-corpus
generates 336 systematic cases covering every command crossed with each style
axis plus draw-order and degenerate-input probes; compare scores pairs on ink
coverage, colour histogram and layout occupancy, weighted worst-first.

The score is deliberately heuristic: SVG in a browser and Ghostscript output
cannot match pixel for pixel, so it orders where to look and never gates.

LaTeX2JS-only macros have no upstream equivalent, so they are rewritten to
their static state with plot variables pinned and the bindings recorded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
…th a tombstone

parseTextExpression matched the pristine line while matchrepl replaced inside
the accumulated contents, so any macro whose argument an earlier transform had
already rewritten silently failed to replace. \section{Cauchy--Schwarz} was
left as literal source text once -- had become an en dash; the same held for
\subsection, \textbf, \textit and \footnote over dashes and quotes. Matching
the accumulated value fixes the whole class.

The proof environment emitted $\qed$, and MathJax defines no such macro, so
every proof ended in a visible "Undefined control sequence" box. It now emits
the open square amsthm uses, set flush right.

Both defects rendered visibly wrong while the suite stayed green, so they are
covered by tests over the interaction that produced them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
The Y transform inverts the axis, so PSTricks' counter-clockwise sweep is
SVG's sweep-flag 0. Both psarc and pswedge passed 1, which traces the
complementary arc: every wedge bowed inward, turning 17-pie's five slices
into a star and leaving the circle unfilled.

Sweep span is now normalised into [0, 2pi), so an end angle preceding the
start takes the long way round as PSTricks does, and the large-arc flag
follows the real span rather than an absolute difference. A full turn cannot
be one SVG arc — start and end coincide — so it is emitted as two half-turns.

Sweep direction is invisible to data-extraction tests, so these assert the
emitted path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Every renderer decided its own fill, in three mutually inconsistent ways:
fillstyle=hlines became a solid fill on pspolygon and psarc, and no fill at
all on psellipse, pswedge and pscurve, so the same document rendered
differently depending on which shape drew it.

All twelve sites now route through resolveFill. solid and none behave exactly
as before; hlines, vlines and crosshatch gain a real rendering as SVG
patterns honouring hatchwidth, hatchsep, hatchangle and hatchcolor, with the
starred forms laying their lines over the fill colour. A style that is still
unimplemented resolves to no fill everywhere rather than guessing solid on
some shapes.

hasFill separates the shape decision — whether a path must be closed before it
can be filled — from the paint itself, so hatched shapes are built as fillable
regions too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
The initial draw walked env.elements in document order, but the pointer
handler removed the .userline and .psplot elements and appended them again,
iterating the type-grouped plot map. Re-appending put them at the end of the
SVG — above every later shape — and regrouped them by command type, so a
diagram that was correct on load silently reordered itself the first time the
pointer crossed it.

Drawing now happens once, into a layer group that the handler replaces
wholesale, so the interactive path uses exactly the same source-order walk as
the initial one. Interactive elements recompute against the pointer through
resolveData; everything else redraws from the data the parser produced.

Order after an event is only observable by dispatching one, so the regression
test asserts it there rather than on the initial render alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
\userline takes its head and tail expressions as brace groups, but the
lightblue vector was written (sin(x)}{-y} with an opening parenthesis where a
brace belongs. LaTeX2JS parses it without complaint, so it went unnoticed; real
LaTeX rejects it outright, and it was the only thing keeping four example
pictures from compiling under PSTricks.

The same line appears inside a verbatim block on the site, so the typo was
being published as documentation for how \userline is written.

With this corrected, all 57 example pictures rasterize against real PSTricks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
The splitter took pspicture blocks verbatim, dropping any \psset above them.
The corpus routinely sets the unit once for several pictures, so those
references rendered at the wrong scale — and under standalone cropping that
produced a plausible-looking image rather than an obvious failure. 16-curves
came out looking rotated, which reads as a renderer bug until you check the
document that was actually compiled.

Each block now carries the settings in scope where it appeared, and the
manifest records them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Colour options were handed to the browser verbatim, so `fillcolor=gray!40`
arrived as an unusable fill value. An unparsable fill is not ignored — it
falls back to black — and example 10's light grey plane rendered as a solid
black shape covering the vectors and labels drawn over it.

parseOptions now resolves the colour-valued keys through resolveColor, which
implements xcolor's mix semantics: `base!N` against white, `base!N!other`
against a named colour, chained left to right. Anything without a mix term is
returned untouched, so plain colour names keep exactly the CSS meaning they
already had. An unknown name, a bad percentage, or an unknown second operand
returns the input rather than guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
\uservariable{alpha}(0.1,0){x} states the initial cursor position in its
coordinate argument, so alpha starts at 0.1. The harness ignored that and
pinned every such variable to 1, producing a faithful reference of a
different diagram — example 09's tangent line was drawn at a slope the source
never asks for, which reads as a plot bug rather than a harness one.

The expression is now evaluated at the declared position, falling back to the
old pin only when it is not numeric.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
…ranking

Slider chrome, whole-document typesetting, and unbound plot variables produce
differences no renderer change can close. Ranked alongside everything else
they sat at the bottom looking like the worst defects, which is exactly where
attention goes first.

Those pairs are now listed with the reason they cannot be judged on equal
terms, and the ranking covers only pairs that can. The README records the
divergences that are deliberate — starred shapes honouring fillcolor, document
numbering, CSS colour names — so they are not re-discovered as bugs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
EnvContent matches Command before Line, and a command's tail stops at the
next command, so `\item First with \textbf{bold} text` arrives as two command
nodes from one source line. Walking them individually rendered each as its own
line, breaking every list item at its first macro — the same text outside a
list was unaffected, which is why it read as a list-styling quirk.

Text environments now rejoin nodes that share a source line before the text
passes run. An empty Line closes the line being built, or is a paragraph break
when there is nothing to close, so blank-line handling is unchanged. pspicture
keeps the per-node walk, since it depends on receiving commands separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
psaxes parsed `ticks` and `labels` and then discarded both, so ticks=none
still drew ticks and no axis could ever carry a number — the reference renders
number every tick. Both options are now carried onto the data and select which
axes get marks and numbers.

Two errors surfaced while adding the labels. Ticks stepped from the end of the
axis rather than the origin, so an axis spanning -3.5 to 3.5 was marked at
half-integers; they now step outward from the origin and land on whole
multiples. And because Y inverts the axis, a vertical span arrives with its
ends reversed, which made the y loop's condition false immediately — no y tick
has ever been drawn.

SVGSelection.text narrowed on SVGTextElement, a constructor jsdom does not
expose as a global, so the first axis label threw a ReferenceError. Every
Element carries textContent, so no narrowing is needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Ghostscript crops to the PostScript bounding box while the browser fills a
fixed viewport, so ink as a fraction of the whole canvas mostly reported how
differently the two pad their output. Simple pictures scored worst purely for
sitting in more whitespace.

Ink is now measured against the drawing's own bounding box, the same frame the
layout signal already uses. The ranking now separates on what the renderers
actually draw: the two lowest are the starred-fill divergence, and their gap is
entirely colour, with ink and layout at 92% and above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
@pyramation

Copy link
Copy Markdown
Collaborator Author

Fixes applied since the PR opened

Nine defects found and fixed by comparing against real PSTricks. All 177 tests green; every fix carries a regression test over the interaction that produced it, because each of these rendered visibly wrong while the suite stayed green.

Rendering

  • \pswedge and \psarc swept the wrong way. Y inverts the axis, so PSTricks' counter-clockwise sweep is SVG sweep-flag 0; both passed 1 and traced the complementary arc. The pie chart rendered as a five-pointed star. Sweep span is now normalised into [0, 2π) so a reversed end angle takes the long way round, and a full turn is emitted as two half-turns.
  • Draw order was discarded on interaction. The pointer handler removed .userline/.psplot and re-appended them, putting them above every later shape and regrouping by command type. A diagram was correct until the pointer first crossed it. Drawing now happens into a layer the handler replaces wholesale, so the interactive path uses the same source-order walk as the initial one.
  • Only fillstyle=solid existed, and the fallbacks disagreedhlines became a solid fill on pspolygon/psarc and no fill on psellipse/pswedge. All twelve sites route through one resolver; hlines, vlines and crosshatch now render as real SVG patterns honouring hatchwidth/hatchsep/hatchangle/hatchcolor.
  • psaxes never drew tick labels, and ticks/labels were parsed then discarded, so ticks=none still drew ticks. Adding labels exposed two more: ticks stepped from the end of the axis (marking an axis spanning -3.5 to 3.5 at half-integers), and because Y inverts, the y loop's condition was false immediately — no y tick had ever been drawn.
  • xcolor tints were passed through raw. fillcolor=gray!40 reached the browser unusable, and an unparsable fill falls back to black — example 10's light grey plane rendered as a solid black shape covering the vectors drawn over it.

Text

  • \section rendered as literal source. Not unimplemented — silently skipped. Text transforms matched the pristine line but replaced inside the already-mutated contents, so once -- became an en dash the replace was a no-op. This was a whole class: \subsection, \textbf, \textit, \footnote over dashes or quotes all failed the same way.
  • Every proof ended in a MathJax error box. \end{proof} emitted the literal string $\qed$, and MathJax defines no \qed. Now the open square amsthm uses, set flush right.
  • List items broke at their first macro. EnvContent matches Command before Line, so one source line arrived as several nodes. The same text outside a list was fine, which is why it read as a styling quirk.
  • SVGSelection.text threw a ReferenceError in any DOM not exposing SVGTextElement as a global, jsdom included.

Corpus

  • A malformed \userline...(2,2)(sin(x)}{-y}( where a brace belongs — copied into three files, including a verbatim block publishing it as documentation. It was the only thing keeping four pictures from compiling; all 57 now rasterize.

Harness corrections (it was wrong twice, in ways that looked like renderer bugs)

  • The splitter dropped \psset above a picture, so references rendered at the wrong scale — 16-curves came out looking rotated.
  • \uservariable{alpha}(0.1,0){x} declares its starting value; the harness pinned every such variable to 1, producing a faithful reference of a different diagram.
  • Ink is now measured against the drawing's bounding box rather than the canvas, since the two renderers pad completely differently.

Where it stands

18 pairs ranked, 3 listed separately as structurally incomparable (slider chrome, whole-document typesetting). The two lowest ranked are the starred-fill divergence\psframe*[fillcolor=...] fills with fillcolor here and with linecolor in PSTricks — where the lenient reading plainly matches author intent (the bar chart wants blue bars). Their gap is entirely colour; ink and layout sit at 92% and above. Left unchanged and documented, along with document numbering and CSS colour names, in tools/pstricks-conformance/README.md.

🤖 Generated with Claude Code

pyramation and others added 6 commits August 15, 2026 17:35
…lling by default

Endpoints were transformed from `r*cos(theta)` alone, placing them as though
every arc were centred on the picture origin. A pie at (0,0) looked correct,
which is why this survived — the same wedge anywhere else collapsed into a
spike reaching back to the origin. The centre is now added before the
transform, so an arc is the same shape wherever it sits.

psarc and pswedge also defaulted to fillstyle=solid with a black fillcolor, so
an unstarred \psarc drew a solid black wedge instead of the open curve
PSTricks draws. PSTricks fills nothing unless a fillstyle is given or the
starred form is used; both now default to none. No existing example relies on
the old default — every pswedge and the one psarc in the corpus either sets a
fillstyle or is starred.

Both surfaced from stress cases placing arcs away from the origin, which the
example corpus never did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Nine stress cases covering combinations the example corpus never had: arcs at
every angle configuration including reversed and full turns, arcs placed away
from the origin, every fill style side by side, deep interleaved layering,
xcolor tint ramps, and five axis configurations. Two of the bugs fixed in this
branch were found by these and by nothing else.

before-after.mjs pairs each example as it rendered before a change, as it
renders now, and what PSTricks draws for the same source, ordered by how much
moved. Where the canvases match it compares per pixel — an ink-position
signature cannot see a recolour, so a plane turning from black to light grey
registered as no change at all; where the drawing changed size it falls back to
the coarse signature and says so rather than reporting a meaningless
percentage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
…s arrowhead

A PSTricks grid is three things: fine subdivision lines, a heavier line on each
unit, and optional coordinate numbers. Only the unit lines were drawn, in
`linecolor` — which `gridcolor` could not override, so setting it did nothing.
subgriddiv, subgridcolor, subgridwidth, gridcolor and gridwidth are now
honoured, with the subdivisions drawn first so the unit lines sit over them.

Grid numbers are opt-in rather than on by default. PSTricks draws them outside
the grid on an unbounded page; an SVG is sized to the picture's declared
bounds, so on a grid reaching the edge they would land outside the viewport and
be clipped. When asked for they are clamped inside so they always show.

psaxes also gives an arrowed end of an axis to its arrowhead: verified against
PSTricks, where an arrow suppresses both the tick and its number at that end
while the un-arrowed end keeps both. Only a tick coincident with the tip is
dropped, so one that merely falls short of it survives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
A blank source line becomes a <br>, and a heading carries its own margins, so
a break next to one produced two gaps where the author asked for one. Runs of
breaks also survived verbatim — the parser snapshot pinned nine consecutive
<br> between two paragraphs — which spread a short document over roughly half
again the height it needs.

Breaks adjacent to a block element are dropped, a run collapses to one, and
trailing breaks are trimmed. Spacing is left to the elements' own margins,
where it can be controlled from CSS. pspicture and verbatim keep their lines
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
pnpm/action-setup refuses to install when the version is named both in its own
`version` input and in package.json's `packageManager`, so every run has failed
during setup — before installing a single dependency. Dropping the input leaves
`packageManager` as the one place the version is declared, which is also what
keeps local and CI runs on the same pnpm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Adds a third job that renders every example with genuine PSTricks and scores it
against the LaTeX2JS output, publishing the comparison as an artifact. Only the
deterministic half gates: a picture that stops compiling under PSTricks fails
the job, while the similarity score is a heuristic between two rasterizers and
is published for review rather than enforced. The runner is amd64, so the image
runs natively there instead of emulated.

Also: superseded runs on a branch are cancelled rather than queued behind each
other, jobs carry timeouts, Playwright browsers are cached against their exact
version so an upgrade misses the cache instead of restoring browsers the new
Playwright refuses, and the e2e job publishes one browsable gallery page beside
the loose PNGs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
@pyramation

Copy link
Copy Markdown
Collaborator Author

CI was failing on every run — fixed, then extended

Root cause: pnpm/action-setup refuses to install when the version is named both in its own version: input and in package.json's packageManager. Every run died in setup, before installing a single dependency — which is why they all failed in ~7 seconds. Dropping the input leaves packageManager as the single declaration, which is also what keeps local and CI on the same pnpm.

Both original jobs now pass in ~80s.

Third job: PSTricks conformance

Renders every example with genuine PSTricks in pyramation/pstricks-latex, renders the same sources with LaTeX2JS through Playwright, and scores them. The runner is amd64, so the image runs natively there rather than emulated as it does on Apple silicon.

What gates and what doesn't: a picture that stops compiling under PSTricks fails the job — that's deterministic and a definite regression. The similarity score is a heuristic between two different rasterizers and is published, not enforced; SVG and Ghostscript output can never match pixel for pixel, so a threshold there would only produce noise.

158s total, and the breakdown says where it goes:

step time
Pull the PSTricks image 67s
Install Playwright browsers 35s
Render 66 pictures with real PSTricks 13s
Render 33 with LaTeX2JS 19s
Score them 1s

The image pull is the bulk. If that becomes annoying it's worth a paths filter so the job only runs when the renderer or corpus changes — happy to add it, but I'd rather see it run on everything first.

Operational fixes

  • Superseded runs are cancelled instead of queueing. This branch had eight failed runs stacked up today.
  • Job timeouts so a hang costs minutes rather than the default six hours.
  • Playwright browsers cached against their exact version, so an upgrade misses the cache rather than restoring browsers the new Playwright then refuses.
  • e2e publishes one browsable gallery page beside the loose PNGs — a self-contained HTML file with every rendering inlined, which is far easier to review from an artifact download than 33 separate images.

Artifacts per run

  • example-renderings (2.5 MB) — every LaTeX2JS render plus gallery.html
  • pstricks-conformance (0.8 MB) — the side-by-side comparison plus every PSTricks reference

🤖 Generated with Claude Code

pyramation and others added 2 commits August 15, 2026 22:10
The conformance job pulls a multi-gigabyte TeX image and takes over two
minutes, which is wasted on a change that cannot alter what gets drawn. A
preceding job diffs against the base commit and decides.

The package list is the dependency closure of the html5 renderer rather than a
guess: html5 pulls in latex2js, pstricks, utils and macros; latex2js and
pstricks both pull in settings; settings pulls in utils. settings and macros
were missing from the first draft of the list, which would have skipped the job
on a change that can move a rendering. The lockfile and root manifest are
included too, since a dependency bump can shift output without a source edit.

The filter fails open: an unknown or unreachable base commit, a failed diff, or
a manual run all take the job. Skipping a correctness check on a bad guess is
the expensive mistake; a wasted run is only slow. It also prints the file list
and the reason it decided, so a surprising skip can be read straight from the
log rather than reproduced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
Records what the conformance job gates on and what it only publishes, the
artifacts each run produces, and the dependency closure the paths filter is
derived from — including the warning to widen it when a package gains an edge
into that closure, since the failure mode is a job that silently stops covering
something.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBYcp3Go9naDWgFThChaDf
@pyramation
pyramation merged commit e83f14e into main Aug 16, 2026
4 checks passed
@pyramation

Copy link
Copy Markdown
Collaborator Author

Filed #27 with a full plan for the dialect work and the divergences found while building the conformance harness.

The short version: LaTeX2JS accepts a dialect of PSTricks that is partly a deliberate extension and partly accident, and nothing currently distinguishes the two. #27 proposes a \psset{dialect=latex2js} key so a document declares which language it is written in, fixes the accidents underneath it, and records the rest as decisions.

Three things from it are strictly fixes with no design question attached, and could land before any dialect work:

  • X/Y coerce NaN to 0, so a broken plot renders as a flat line on the axis instead of failing. This is the mechanism behind most of what this PR fixed.
  • arrowscale (28 uses), plotstyle (3) and showorigin (6) are parsed and ignored. plotstyle=dots is why the tangent-point dots are missing from every picture in graph.tex while the PSTricks reference shows them.
  • Pi is undefined — the table has pi, PI and π but not pst-plot's own spelling.

Nothing in #27 blocks this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant