
Security News
Agent Baseline Publishes Security Reference Architecture for Software Factories
Agent Baseline’s new reference architecture shows how to secure software factories with four boundaries enforced outside the AI agent.
@reportwright/cli
Advanced tools
pw: render, test and diff ReportWright reports from the command line.
pw: render, snapshot-test, diff and import ReportWright reports from the command line, with no server.
Live demo: mrarun005.github.io/reportwright-demo. Switch to ReportWright runs the same importers as pw import in your browser: drop a .jrxml, .rdl or .rptdesign and see what carries over.
The CLI carries @reportwright/engine; fonts come from @reportwright/fonts.
pw render output, page 1 (reports from the repository's reports/):

npm i -D @reportwright/cli @reportwright/fonts # then: npx pw …
# or one-off, without installing:
npx -p @reportwright/cli pw --version
orders.pw.json is the report from the engine's examples (a table of customers and amounts with a
total); orders.json is its data:
{ "rows": [{ "customer": "Asha Rao", "amount": 1200.5 }, { "customer": "Ben Ode", "amount": 80 }] }
npx pw render orders.pw.json -d orders=orders.json -f pdf -o orders.pdf
# pw: wrote orders.pdf (1 pages, streamed)
-d orders=orders.json replaces the data source named orders with the file's JSON.
Each was run against this version.
Render to every format. -f is pdf, xlsx, docx, pptx, html, csv, json or txt; -o - writes to stdout.
npx pw render orders.pw.json -d orders=orders.json -f xlsx -o orders.xlsx
npx pw render orders.pw.json -d orders=orders.json -f csv -o orders.csv # Customer,Amount / Asha Rao,1200.5 / Ben Ode,80
npx pw render statement.pw.json -p accountId=LN-1 -f pdf -o statement.pdf # -p sets a report parameter
Archival, accessible or protected PDF.
npx pw render orders.pw.json -d orders=orders.json --pdfa --ua -o orders.pdf # PDF/A-2a + PDF/UA-1
npx pw render orders.pw.json -d orders=orders.json --password s3cret --deny copy -o locked.pdf
Bulk import a folder of legacy reports. One bad file does not stop the batch; the exit code says how it went.
npx pw import legacy -o imported --recursive
# pw: finance/Kitchen Sink.rdl → finance/kitchen-sink.pw.json: written, 2 unresolved
# pw: truncated.rdl: FAILED: The file is not readable XML: Text outside the root element (character 27503)
# pw: 1 of 2 reports imported, 1 failed, 2 unresolved references; report: imported/import-report.html
echo $? # 2: some failed
Snapshot-test reports as code. With reports/__tests__/orders.test.json:
{ "report": "../orders.pw.json", "cases": [{ "name": "two-orders", "data": { "orders": "orders.json" } }] }
npx pw test reports --update # 1 passed, 0 failed, 1 snapshot written
npx pw test reports # 1 passed, 0 failed (exit 1 when a case changes)
Diff two versions of a report (the words, page by page; exit 1 when they differ):
npx pw diff orders.pw.json orders-v2.pw.json -d orders=orders.json
# orders-v2.pw.json: page 1 changed
# - Orders
# + All orders
| Command | Options |
|---|---|
pw render <report.pw.json> | -p name=value (repeat), -d source=data.json (repeat), -f format (default pdf), -o file|-, --base URL (where relative data URLs go), --allow-internal, --pdfa, --ua, --values (Excel totals as values), --html tables|static (the HTML file; unset: the report's export.html, then PW_HTML_EXPORT; interactive needs the viewer), --drill-links inline|new-tab|off (drill-through needs a public --base), --static (alias of --html static), --layout (CSV of the printed layout), --password, --owner-password, --deny print|modify|copy|annotate|fillForms|accessibility|assemble|printHighQuality |
pw import <file> | -o out.pw.json |
pw assess <folder> | -o dir, --recursive, --usage file, --retire-days N (180), --hours simple=…,medium=…,complex=…, --fail-under pct, --max-files N (500), --max-bytes N (8 MB) |
pw import <folder> | -o outDir, --recursive, --force, --verify, --report path, --max-files N (2000), --max-bytes N (16 MB) |
pw test [folder] | --update, --layout |
pw diff <old> <new> | -p, -d, --base |
pw compare <original.pdf> <converted.pdf|report.pw.json> | -o dir (default .), -d, -p, --base, --tolerance PT (2), --threshold PCT (0), --dpi 72-100 (72), --numbers-only, --no-visual, --no-positions |
pw --version | prints the CLI and engine versions |
Exit codes: 0 ok; 1 a test failed, the pages differ or pw compare found a difference over its gate; 2 bad usage (pw import <folder>: 2 some failed, 1 none imported).
| Direction | Formats |
|---|---|
Output (-f) | pdf (also --pdfa, --ua, encrypted), xlsx, docx, pptx, html, csv, json, txt |
| Import | SSRS .rdl / .rdlc (+ shared data sources .rds), JasperReports .jrxml (+ style templates .jrtx), BIRT .rptdesign, ActiveReports .rdlx-json, referenced images. Jasper and BIRT crosstabs carry over as matrices (groups, measures, totals, styles); Jasper and BIRT charts as charts; Jasper markup as rich text; SSRS custom code's simple functions as report functions; SSRS maps as map items (embedded shapes as GeoJSON; a shapefile layer's .shp + .dbf are read when they are in the imported folder; SQL spatial layers are drawn from the query's geography binary or STAsText() WKT) |
@reportwright/fonts next to the CLI.pw render -f pdf -o file streams a flowing table, so memory stays flat on large reports (see the engine's
streaming notes); other formats render the whole model in memory.--max-files files (2000) of at most --max-bytes each (16 MB). BIRT libraries
(.rptlibrary) are listed but not read.@reportwright/engine (the same rendering from code), @reportwright/fonts,
@reportwright/fonts-cjk, @reportwright/viewer (view and design in the browser).
pw renderpw render -f pdf -o file streams a flowing table into the file as it is laid out (exportPdfStream; the message says
streamed), so a large report's memory stays flat; other reports, --password and -o - render the whole model first.
It writes to a temporary file beside the target and renames it when the export succeeded: a failed run leaves no
damaged PDF, and an older file at the path stays as it was.
pw assess <folder> answers "should we migrate, and how much work is it?" without writing a single converted report.
It runs the bulk importer in dry-run mode and writes only assessment.html and assessment.json (to -o, default .).
npx pw assess legacy -o assessment --usage runs.csv --retire-days 180 --hours simple=0.25,medium=2,complex=8 --fail-under 60
PW_DB_<NAME>).full converts as it is; small has approximated items, report notes or missing files; manual has dropped
items or untranslated expressions; failed does not parse. Configuring data sources and fonts is not a fix.--usage: no run in --retire-days, default 180); Simple (effort Low); Medium; Complex
(effort High, or failed). Effort: Low up to 3 issues in a small report, Medium above that; High for manual work with more than 3.--hours simple=…,medium=…,complex=….
These are planning numbers, not measurements.SELECT ItemPath, TimeStart, UserName, Format, Status FROM ExecutionLog3 WHERE ItemType = 'Report' AND TimeStart >= DATEADD(day, -365, GETDATE()).
A generic table has a report name (name, report or reportpath), runs and lastrun (or last run date). Rows
join to reports by file name without the extension. A report with no row has no runs.--fail-under <pct>; 2 bad usage.--max-files, --max-bytes).pw import legacy-reports -o imported --recursive [--force] [--verify] [--report path] [--max-files 2000] [--max-bytes 16777216]
pw import <folder> imports every SSRS (.rdl, .rdlc), JasperReports (.jrxml), BIRT (.rptdesign) and
ActiveReports (.rdlx-json) report in the folder (and its subfolders with --recursive) into <outDir>, which mirrors
the folder. Each report becomes <its folder>/<report id>.pw.json (the id is the file name in lower case with dashes,
e.g. Sales Report.rdl → sales-report.pw.json), because pw render and the server find subreports by that id.
One bad file never stops the batch. Its error goes into the report and the rest go on. Exit code: 0 every report was imported, 2 some failed, 1 none was imported (or the folder has none).
References are resolved across the batch. These are found among the folder's files: by their path relative to the report, else by their file name, nearest first.
.rds, named by a DataSourceReference): the provider comes from the .rds, but the
connection string does not (set PW_DB_<NAME> on the server, as for any SQL source);.jrtx, nested ones too): the report's own style of the same name wins;src rewritten to the copy.An unresolved reference is listed for its file and does not stop the import. BIRT libraries (.rptlibrary) are
listed, but the BIRT importer does not read them, so their items are missing.
The migration report is import-report.json and import-report.html in <outDir>. With
--report file.json it goes there with the .html beside it, and with --report folder it goes into that folder.
For each file it gives the status, the counts of items mapped, approximated and dropped, and every item approximated
or dropped with the reason. It also lists every expression that was not translated with its item and its line and
column, and the references with what each resolved to. The rest of the importer's notes are listed under the report.
Every importer warning is in the report once. The HTML page stands alone and escapes file names and expressions.
Line and column are where the item's element starts in the XML file. ActiveReports .rdlx-json is JSON and gives
none. A data set's or a filter's expression is reported in that report's notes, not with a line.
Edited outputs are kept. <outDir>/.pw-import.json holds the hash of each file as written. On a re-run, a
target whose hash differs (you edited it, or it was there before and is not from an import) is skipped with a message.
--force overwrites it. Unchanged targets are refreshed, and new ones are written.
--verify renders each imported report with its embedded data, or with <id>.data.json beside the output
({ "<data source>": [rows] }). A report with neither is skipped with a note. When the run replaced an earlier output,
the two are compared with pw diff's comparison (the words, page by page), and the changed lines go into the report.
Otherwise the check confirms that it renders. A verify makes no network requests.
The folder is untrusted. A report never opens a file by the path it names. Only files found by walking the
folder are read, so ../ and absolute paths cannot reach outside it. Symbolic links that point out of the folder,
or to folders, are refused and listed. Every output path is checked to stay in <outDir>, and nothing is written
through a link. --max-files and --max-bytes cap the file count and each file's size. Nothing in an imported file
is run: scriptlets stay text, and SSRS custom code is translated (simple single-Return functions become report
functions), never executed. A map's shapefile (.shp + .dbf) is read only from inside the folder, with the same caps.
pw --version (or -v) prints the CLI's version and the version of the engine it carries.
pw comparenpx pw compare sales-ssrs.pdf sales.pw.json -d sales=sales.json -o compare/
npx pw compare sales-ssrs.pdf sales-rw.pdf --threshold 0.5 # two PDFs
npx pw compare sales-ssrs.pdf sales.pw.json -d sales=sales.json --numbers-only # CI: only the numbers
Writes compare.html (self-contained) and compare.json, prints a summary. A .pw.json second argument is rendered
first, with -d/-p as in pw render. What it checks:
1,234.50 = 1234.5 = 1.234,50 =
$1,234.50; (12.00) = -12); a rounding difference (1,234.504 → 1,234.50) is flagged as rounding; anything
else is mismatched with the delta and % delta. Totals (labels with total, sum, grand, subtotal, Gesamt, Summe,
totale, somme, suma, 合計…) are reconciled separately.--tolerance points (positions
normalised for a different page size). Positions need pdftotext (poppler-utils); without it the words are compared
in reading order, from @reportwright/pdf's reader, and moves are not reported.pdftoppm is installed): both pages at --dpi, a red-on-faded overlay and the % of pixels that
differ per page.Verdict: Matches, Minor differences (only moves or pixels) or Differences found. Exit 1 when any number or
total differs (rounding included), the page counts differ, or the % of differing words or pixels passes --threshold;
--numbers-only gates on the numbers alone. Inputs are untrusted: each PDF is read under the reader's budget (at most
50 MB, 300 pages, 20,000 words a page, 50 pages rendered, 4 megapixels a page, tools stopped after 15 s). The
viewer and designer have no compare button yet: use the CLI or POST /api/reports/compare. See
docs/migration-compare.md.
pw testpw test <folder> renders every <folder>/__tests__/*.test.json and compares it with a snapshot beside it. Each test file says which report to render and which cases to run:
{
"report": "../loan-statement.pw.json",
"cases": [
{ "name": "home-loan", "parameters": { "accountId": "LN-1" }, "data": { "loans": "loans.data.json" }, "now": "2026-01-01T09:00:00" },
{ "name": "shape", "layout": true }
]
}
report: the report, relative to the __tests__ folder.cases[].name: names the snapshot (<file>.<name>.snap.txt). Required.cases[].parameters: report parameters (name → value).cases[].data: JSON for a data source by name: a file path (relative to __tests__) or the JSON itself.cases[].now: the report's "now" (default 2026-01-01T09:00:00), so dates in the output do not drift.cases[].state: drill-down toggles, e.g. { "toggles": { "Pivot|r0|/North": false } }.cases[].layout: true compares this case by layout (see below).A case without layout is compared by its words, page by page: pw test passes when the text is the same. A font size, a colour or a column width can change and still pass.
pw test reports --layout (or "layout": true on a case) compares the layout instead. Each page's snapshot lists every item's box (x, y, w, h in points, rounded to 0.5), a text's font size and colour, a shape's fill, and each table's column widths. Changing any of them fails the test and prints the lines that changed. Snapshots for layout cases are <file>.<name>.layout.snap.txt, so a case can be kept in both modes.
pw test reports --update writes the new snapshots: the words, or the layout for --layout runs and layout: true cases. Exit code 1 means a test failed.
The package is the report commands only (render, test, diff, import): no server code, no child processes, and it
reads no environment variables. pw audit and pw migrate-storage work on a ReportWright server's storage, so they run
from the app folder (node bin/pw.mjs audit verify).
MIT.
FAQs
pw: render, test and diff ReportWright reports from the command line.
The npm package @reportwright/cli receives a total of 0 weekly downloads. As such, @reportwright/cli popularity was classified as not popular.
We found that @reportwright/cli demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Agent Baseline’s new reference architecture shows how to secure software factories with four boundaries enforced outside the AI agent.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.