1
0
mirror of https://github.com/alexandrev/xslt-lab.git synced 2026-09-13 08:43:16 +00:00

44 Commits

Author SHA1 Message Date
claude-code 4e1f3ffdac Fix: give the backend room to run, and stop transforming half-typed expressions
The backend container runs three JVMs — Saxon 12, Saxon 9.6 and Xalan — whose
-Xmx values add up to 512 MB, inside a container limited to 256Mi. The limit was
half the heap the JVMs believed they had, so the kernel was killing the
container: OOMKilled twice a day on the 16th and 17th of August, five times a
day on the 18th and 19th, and 81 users in 24 hours got "transform service
unavailable" instead of a result. Before each kill the serial collector spends
hours in continuous full GC, which is why three of the five pods sat pinned at
exactly 0.5 CPU — their limit — around the clock, and why XSLT 2.0's p95 was
5.5s against 0.7s for the other two engines.

Those limits were never in the chart to begin with: the backend deployment has
never rendered a resources block, values.resources.backend was declared and
unused, and the live numbers came from a manual kubectl patch that an ArgoCD
sync would have silently dropped. So the chart now owns them, with room for
what actually runs in there. The CPU request moves 10m -> 250m as well: against
real usage of 500m the HPA read utilisation in the thousands of percent and the
deployment sat permanently at maxReplicas, unable to signal anything. And the
backend gets minReplicas 2, for the reason the frontend already learnt.

The other half is the mid-keystroke problem again, one level down. Gating the
automatic run on well-formedness took the error rate from 54% to 24%, but a
stylesheet whose XPath is half-typed is perfectly good XML, so it still went to
the backend and still came back as a compile error. A day of logs is mostly
that, keystroke by keystroke: 'current()/..[@N' then '[@Na' then '[@Name', and
"Required attribute 'select' is missing" 65 times on a single line. So the gate
now also declines to send an expression that is provably unfinished — a
required attribute that is absent, an empty value, a quote or bracket still
open, a trailing token that cannot end an expression. Facts about the text, not
guesses at intent: there are twice as many tests for what it must leave alone
as for what it catches, including every template the app itself ships. It is
skipped above 64KB, where a document was pasted rather than typed, and as
before it only holds back the automatic run — "Run it anyway" is always there.

Finally, the error classifier learns the rest of what was sitting in "other":
Xalan's "Syntax error in '<expr>'" is the stylesheet's XPath, not the input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HnUwBeXv6PCTEzRpFoMSU
2026-08-19 19:30:02 +00:00
claude-code 9e81475957 Fix: create the fiddle table with explicit DDL, not AutoMigrate
GORM's migrator crashes with "insufficient arguments" when it introspects
this composite-primary-key table if the table already exists — which is
exactly what happened on the first production rollout (my local end-to-end
test had created the table moments earlier), crashlooping the backend until
a rollout undo restored service.

CREATE TABLE IF NOT EXISTS is idempotent and has no such moods. A failure
now degrades to fiddles-off like every other storage failure, instead of
taking /transform down with it. Verified against all three paths: existing
table, fresh database, and restart over a self-created table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-14 11:55:20 +00:00
claude-code 40b81728d6 Competitive batch: Saxon 12.9, 2.0 tracing, saved fiddles, multi-engine story
From the August competitive analysis, the items that close real gaps:

- Saxon HE 12.5 -> 12.9 (the closest competitor already ships 12.9). The
  trace/hotspot instrumentation is reflection-based, so it was verified
  against the new jar: variables and execution counts come back intact.

- Tracing now works for XSLT 2.0. The trace infrastructure needs Saxon 10+
  APIs and cannot load inside the Saxon 9 daemon, so traced 2.0 requests run
  on Saxon 12 in backwards-compatible mode instead of silently returning an
  empty trace — a real trace with a small, labeled semantic difference.
  Untraced 2.0 runs still use Saxon 9. XSLT 1.0 now says outright that XSLTC
  has no tracing hook rather than pretending.

- Saved fiddles: POST /fiddle stores the workspace under a 7-char id with an
  append-only revision history; ?f=ID loads it (optionally &r=N). Available
  whenever DATABASE_URL is configured — unlike /history this is
  unauthenticated, because a fiddle is a thing you share with someone who has
  no account. An unreachable database degrades to "fiddles off" instead of
  crashlooping: the chart has shipped a placeholder databaseUrl for a long
  time, and opening it eagerly would have taken production down.

- The home page now tells the truth about engines: 1.0 on XSLTC, 2.0 on
  Saxon 9, 3.0 on Saxon 12.9 — three real processors was already the
  architecture, it just was never marketed.

- Four posts aimed at where the demand data points: the FreeFormatter
  shutdown audience, Chrome's XSLT removal (v158, Nov 2026), Peppol/EN-16931
  Schematron validation and ISO 20022 camt transformations. Every example was
  executed against the real backend and the published output matches.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-14 11:39:00 +00:00
claude-code 053a7d95e5 Fix: stop rejecting valid enterprise stylesheets, and classify the rest of "other"
Three things the error review turned up.

Secure processing caps an XPath expression at 100 operators, so the JDK was
refusing perfectly valid stylesheets with JAXP0801002 — a limit this service
imposes, not a mistake by the author. The XSLT 1.0 daemon now raises the op,
group and total limits instead of switching secure processing off, which also
guards external entity access; a runaway expression is still bounded by the
10s transform timeout, and any -D on the command line still wins.

That error is now classified as "backend" rather than "stylesheet": it belongs
with the bug candidates, not with user error, or the dashboard hides it.

The remaining unclassified errors were mostly classifiable all along, and the
patterns come straight from the production logs: multiply-defined and
undefined variables, format-number picture strings and missing Java extension
methods are the author's stylesheet; a stray <?xml ...?> declaration is the
input document.

Finally, calls to Java extension methods now say so: the playground runs stock
Saxon HE, so a class from the author's own project or their ESB runtime will
never resolve, and the raw "Cannot find external method" gives no clue why.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-08-03 09:43:31 +00:00
claude-code 3925ed9e77 Feat: report how often each construct actually ran, alongside the trace
Static rules can only guess at what is slow; the trace knows. The listener now
also counts template, apply-templates, for-each, for-each-group, call-template
and function executions, and reports the top 25 with the trace, so "this
template ran 4,312 times" is a fact rather than a heuristic.

The counting is kept strictly apart from the variable reporting it sits next
to. That deque pairs enter/leave and stays balanced only because both handlers
filter on isVariable() identically: pushing non-variable frames onto it would
make a variable's leave pop someone else's frame and silently attribute the
wrong value in the UI. countHotspot touches its own map and nothing else, and
handleEnter/handleLeave are unchanged.

Two things this needed along the way:

- Saxon never calls TraceListener.close() before the daemon reads the trace
  buffer, so relying on it dropped every hot spot. attachTraceListener now
  returns a flush the daemon runs once the transform is done.
- getConstructType() is null on these objects in Saxon 12 — which is why
  isVariable() already falls back to class names — so constructs are
  recognised the same way.

Verified end to end against a real transform: the variables still come back
intact (6 entries with their values) and a template applied to five elements
is reported as five executions.

Note for later: only SaxonDaemon attaches the listener, so tracing — and now
profiling — is XSLT 3.0 only. That predates this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-28 16:01:38 +00:00
claude-code 4747468182 Test: assert the intended no-source behaviour instead of the removed default
TestPickSourceXMLReturnsDefaultWhenNoXML still expected the <root/> fallback
that 2bd3e94 deliberately removed so Saxon can invoke xsl:initial-template.
The test had been failing ever since; update it to assert an empty source.
This makes the backend suite green again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 07:30:07 +00:00
claude-code bb2bc4c741 Backend: classify "no source document" as input_xml instead of other
Saxon phrases the missing-input error as "an initial template or an initial
function" (spaces), so the hyphenated initial-template check never matched and
it fell through to class="other" — it was the 6th most frequent error in the
Loki triage (11x) and reproduced in production after deploying the counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-25 07:28:48 +00:00
claude-code 62b3761908 Add error metrics and contextual feedback 2026-07-24 15:35:56 +00:00
claude-code 4724a44a7d Backend: classify XSLT 1.0 (JAXP/Xalan) errors + frontend version-upgrade hint
Triage of transform_error logs (Loki) showed ~500 failures piling into
class="other" — all of them user errors from the XSLT 1.0 processor, which
classifyTransformError didn't recognise (it only knew Saxon phrasing).

Backend:
- classifyTransformError now recognises JAXP/Xalan (XSLT 1.0) messages:
  XML well-formedness failures -> input_xml; undefined-function / misplaced-
  element / illegal-attribute / type errors -> stylesheet. Adds containsAny
  helper and a table-driven test covering Saxon and Xalan phrasings.

Frontend:
- detectVersionUpgradeHint(error, version): when a transform fails because a
  newer-version function is called (e.g. current-date()/tokenize() in 1.0, or
  map:/array: in 2.0), the error box shows "<fn>() is an XSLT 2.0 feature —
  switch to XSLT 2.0" with a one-click button that bumps the tab version.
- Requires the call shape to fire, so plain words don't false-positive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KZntQUfw463NW4ftgSjWw5
2026-07-24 14:30:54 +00:00
claude-code 01a4715efc Perf: halve LCP (4.1s -> 2.0s) + surface Saxon vs round-trip timing
LCP was gated on the backend: the largest element on the page is a
CodeMirror line in the *result* pane, which could not exist until the
2s debounce plus the POST /transform round-trip had completed.

- Seed the welcome example's known Saxon output into the result pane on
  first visit so it paints on mount. The real transform still runs and
  overwrites it; duration/serverMs stay null so no timing is claimed
  until the real run lands.
- Drop the static xml-formatter import that was silently defeating the
  existing dynamic import (Vite warned about this).
- Load lint, autocompletion, hover docs and the ~50KB completions table
  after mount via requestIdleCallback; the editor reconfigures when they
  arrive and degrades gracefully if they fail.

Critical index chunk 100KB -> 56KB (28KB -> 17.7KB gzipped);
xsltCompletions split into its own deferred 39.7KB chunk.

Measured over 3 runs, mobile emulation, 4x CPU throttle, 1.6Mbps:
LCP 4064ms -> 2032ms median, CLS 0.033 -> 0.036, FCP unchanged.

Also includes the in-progress timing work: the success box now shows the
client round-trip alongside server-side Saxon time, and Saxon
compilation failures without a code prefix classify as "stylesheet".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDk5guu51FjFxQMjgrrekW
2026-07-20 13:55:51 +00:00
claude-code 22c5209939 Backend: structured error logging with one-click repro URLs + Grafana dashboard
Every failed transformation now emits a single JSON line to stdout
("event":"transform_error") that promtail already ships to Loki:

- class: input_xml (user data) / stylesheet (user XSLT) / backend (bug
  candidates) — lets review focus on real defects
- error_code extracted from the Saxon message (XPST0017, FORX0002…)
- xslt_b64 / params_b64: full payloads (capped at 48KB/field)
- repro_url: share-format URL (?xslt=&xml=&version=) — one click opens the
  playground with the exact failing case
- New "XSLT Playground — Errors" Grafana dashboard (errors by code, by
  class, recent log lines) added to the chart's dashboard ConfigMap

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-07-03 08:36:04 +00:00
claude-code 1692ae4360 Fix: source XML with &amp; entities corrupted by html.UnescapeString
pickSourceXML always ran html.UnescapeString on the chosen source document,
turning valid "&amp;" into a bare "&" and breaking XML parsing across all
engines (e.g. the default welcome example failed with "The entity name must
immediately follow the '&'"). The same corruption was already fixed for the
parameters path in e2fdbfd but the source path was missed.

Only unescape when the value is HTML-encoded (starts with "&lt;"); leave
already well-formed XML untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgybX3QbAWVzb9ZnroCA79
2026-06-30 14:54:25 +00:00
alexandrev-tibco 2b2a16ed43 Metrics: add Prometheus instrumentation + Grafana dashboard
Instrument the Go backend with Prometheus metrics (HTTP request rate/
latency/in-flight and per-version transformation outcomes, Saxon duration,
payload size, trace usage), exposed on a dedicated internal port 9100 so
they are never routed through the public ingress. Add an nginx exporter
sidecar to the frontend. Ship ServiceMonitors (release=kube-prom-stack)
and a Grafana dashboard ConfigMap via the Helm chart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 15:28:46 +02:00
alexandrev-tibco e2fdbfd296 Fix: html.UnescapeString corrupts XML params with &amp; entities
When an XML parameter value starts with '<', html.UnescapeString was
incorrectly called, converting &amp; → & and breaking well-formed
XML that contained entity references. Only apply HTML decoding when
the value starts with '&lt;' (HTML-encoded XML sent from a form field).

Fixes: 'The entity name must immediately follow the & in entity reference'
error reported when XML input contains & characters escaped as &amp;.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 13:42:51 +02:00
alexandrev-tibco 25bb1dce07 Add xsl:result-document support and blog article
- SaxonDaemon (Saxon 12): capture secondary outputs via setResultDocumentHandler
- Saxon2Daemon (Saxon 9.6): capture secondary outputs via OutputURIResolver
- Go backend: propagate secondaryResults field in TransformResponse
- Frontend: secondary results panel with collapsible items and copy button
- Blog: xslt-result-document.md covering basic syntax, split-by-category,
  multi-format output, serialization, XSLT 3.0 streaming, and limitations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 16:23:42 +02:00
alexandrev-tibco ac409af741 Fix Saxon2Daemon compilation against Saxon 9.6
Saxon 9.6 lacks ErrorReporter/XmlProcessingError (introduced in Saxon 10),
so Saxon2Daemon is now self-contained using JAXP ErrorListener. Runner.java
is excluded from the Saxon 9.6 build step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 13:00:06 +02:00
alexandrev-tibco e78b71cdbb Use Saxon HE 9.6.0.7 for XSLT 2.0 daemon (last true XSLT 2.0 processor)
Saxon 9.8+ supports XSLT 3.0, so Saxon 10 had the same validation gap
as Saxon 12. Saxon 9.6 is the last release focused on XSLT 2.0; it does
not process xsl:mode or other XSLT 3.0 declarations, making it a proper
XSLT 2.0 validator. Downloaded from SourceForge at build time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 12:55:08 +02:00
alexandrev-tibco 7733fcfc6d Fix: use Saxon HE 10.9 for XSLT 2.0 daemon (9.9.x not on Maven Central)
Saxon 9.9.x is not available on Maven Central; Saxon 10.9 is the closest
alternative (last pre-12 major release, stricter 2.0 semantics than 12).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:25:22 +02:00
alexandrev-tibco 49c9281910 Add per-version XSLT engines: XSLTC (1.0), Saxon 9 (2.0), Saxon 12 (3.0)
Each XSLT version now runs against its own true processor rather than
everything going through Saxon 12 in backwards-compatible mode:
- version=1.0 → port 8082, JDK built-in XSLTC (proper XSLT 1.0 validator)
- version=2.0 → port 8083, Saxon HE 9.9.1.8 (true XSLT 2.0 semantics)
- version=3.0 → port 8081, Saxon HE 12.5 (unchanged)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:26:50 +02:00
alexandrev-tibco 2bd3e94bb9 Support xsl:initial-template when no source XML is provided; drop "only" from copy
- Backend no longer defaults to <root/> as source document; when no XML
  parameter is detected, source is empty and setInitialContextNode is
  skipped, allowing Saxon to invoke xsl:initial-template automatically
- Remove "The only free online XSLT" wording from meta descriptions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 20:21:34 +02:00
alexandrev-tibco 9563518fb0 Fix SaxonDaemon classpath: quote glob to prevent shell expansion
The shell was expanding /opt/saxon/* into space-separated filenames,
causing Java to treat each jar as a separate argument and fail with
ClassNotFoundException. Single-quoting lets the JVM handle the glob.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 17:39:59 +02:00
alexandrev-tibco d6dffa530c Replace per-request JVM with persistent Saxon daemon
Start a long-lived Java HTTP server (SaxonDaemon) on 127.0.0.1:8081 at
container startup. The Go backend now proxies /transform calls to it instead
of spawning a new JVM per request, eliminating ~500ms cold-start overhead.

- SaxonDaemon.java: HttpServer on :8081, two Processor instances (normal +
  trace), warm-up at startup, per-request compile/transform
- Runner.java: make enableCompileWithTracing, DeduplicatingErrorReporter and
  attachTraceListener package-private for daemon access
- main.go: remove exec.Command path; POST to daemon, parse JSON response
- Dockerfile: compile SaxonDaemon, download Gson, ship start.sh
- start.sh: wait for daemon /health, then exec Go server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 17:26:46 +02:00
alexandrev-tibco 9420a2b094 Improve backend performance and add loading state to result pane
Backend:
- Add JVM startup flags (-XX:TieredStopAtLevel=1, -XX:+UseSerialGC,
  -Xms32m, -Xmx256m) to reduce per-request JVM startup time by ~30-50%
- Replace deprecated io/ioutil.TempDir with os.MkdirTemp (Go 1.17+)

Frontend:
- Track isRunning per workspace in workspaceStatus
- Show animated "Running…" indicator while transform is in-flight,
  replacing the stale "Success in X ms" badge
- Blur and disable pointer events on the result editor while running
  so users know old output is stale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 12:31:08 +02:00
alexandrev-tibco beca22e907 Add XSLT 3.0 support and expand test coverage
Frontend:
- Add XSLT 3.0 option to version selector
- Add tests: version selector renders 1.0/2.0/3.0, version change updates stylesheet

Backend:
- Validate version field: reject anything other than 1.0, 2.0, 3.0 or empty
- Add tests: pickSourceXML (preferred keys, fallback, no XML, HTML entity unescaping)
- Add tests: version validation rejects invalid, accepts all valid values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 14:58:42 +02:00
alexandrev 37e22ac26e new updates 2026-01-12 17:26:49 +01:00
alexandrev de28a305df version 0.2.0 2025-11-15 22:26:25 +01:00
alexandrev 39d6767ac5 +1 2025-09-23 16:09:29 +02:00
alexandrev-tibco 33048ee9a2 Merge remote-tracking branch 'refs/remotes/origin/codex/add-custom-xslt-functions-to-saxon' into codex/fix-xml-parameter-type-issue 2025-08-11 14:47:13 +02:00
alexandrev f91b12624d heuristics check 2025-08-05 08:58:07 +02:00
alexandrev d76c80011c Expand Saxon extension functions 2025-07-27 16:54:01 +02:00
alexandrev 44a72ca4ba backend: pass parameter XML via files 2025-07-22 19:02:44 +02:00
alexandrev 617a53043d backend: pass parameters via java args file 2025-07-22 18:51:54 +02:00
alexandrev bb20a9c9dc avoid command line overflow by passing params via files 2025-07-22 18:01:43 +02:00
alexandrev ba91c1453c fix parameter quoting 2025-07-04 20:51:57 +02:00
alexandrev e4abd1e056 feat: add goPro mode to backend 2025-07-04 09:01:47 +02:00
alexandrev 2d87652882 minimal updates 2025-07-01 21:34:45 +02:00
alexandrev 9cf8f7fa2f show transform success time 2025-07-01 20:55:07 +02:00
alexandrev c9a5982ba4 Include Saxon dependencies in runtime 2025-07-01 18:48:02 +02:00
alexandrev 3914a24f6c backend: enable CORS 2025-07-01 18:37:01 +02:00
alexandrev ce61e7d79b Fix make build by vendoring Go modules 2025-07-01 13:52:41 +02:00
alexandrev 2dc2b8cf44 Add Makefile and docker-compose for local setup 2025-07-01 13:07:42 +02:00
alexandrev 9201a9c02c Document environment variables 2025-07-01 10:03:09 +02:00
alexandrev b56df065bf Add Dockerfile for frontend 2025-07-01 09:39:15 +02:00
alexandrev 3268dbca4e Initial version 2025-07-01 09:00:24 +02:00