Repository navigation
Releases: ahaomar/un-editorial-check
Release list
Release 1.8.0 — the deep-audit release
The deep-audit release. A whole-repository review — every shipped file read line by line, unit by unit — found no critical defects and no injection paths, and produced a hardening batch for the hostile-input surfaces: every place the tool reads a document it does not trust now has a memory ceiling where it had none, and three crashes became refusals. No rule's catalogue meaning changes.
Hardened
- Decompression bombs are capped everywhere. A few kilobytes of hostile compressed bytes once inflated without limit — in the DOCX/ODT zip reader, and in the PDF inflater, LZW decoder and RunLength decoder. Each stream is now capped per entry and across the container, and the overflow becomes the ordinary named refusal, not an out-of-memory kill.
- Font tables carry budgets. A CMap whose
bfrangeblocks repeat maximal ranges, and a composite font's/Wwidth table, could materialise hundreds of millions of entries; both now refuse past a total budget (UE-DECODABLE refusals name the font). - The cross-reference chain cannot loop. Two xref tables whose trailers point at each other once drove unbounded recursion; the chain is now bounded by depth and by visited offsets, and a loop is a named refusal.
- Redirects are re-validated.
--urlfollows redirects manually: each hop re-applies the http(s)-only check, and the chain is bounded at eight, so a named URL that 302s cannot silently fetch somewhere the contract excludes. - Hostile regular expressions refuse at load. An organisation-profile
customRulespattern that nests a quantifier inside a quantified group — the catastrophic-backtracking signature — is refused when the profile loads, fail-closed, because Node has no regex timeout and one hanging pattern once meant one hanging scan. - A broken installation degrades honestly. A missing or partial
fonts/directory used to kill every invocation — even--version— with a raw ENOENT; the faces now load lazily, so only a run that renders a PDF touches them, through the ordinary error mapping. - A ceiling for every local input. Files and standard input are refused over 64 MiB with a reason, where a multi-gigabyte read was a heap-exhaustion crash.
Fixed
- The fixer never guesses across occurrences. A finding anchored to an offset whose text no longer holds the match is skipped, instead of an unbounded search that could rewrite a different occurrence of the same words.
ue:ignoreprose can no longer silence the engine. The suppression spec ends where the ids end: a stray word like "all" in<!-- ue:ignore UE-TE004 for all audiences -->is prose, not the off switch.- Zero-width custom-rule matches are skipped, not emitted as findings with no match text and no position.
- The re-run command keeps every target. Flag-stripping in the capped section stopped at nothing, so a file literally named
--reportvanished from the printed command; everything after--is copied verbatim. - SARIF
artifactLocationis a valid URI reference — filesystem paths are encoded;--urlvalues pass through. - One clean-run sentence on every surface — the terminal, the HTML report and the PDF all state a clean scan with the same words (the PDF said "No findings.").
- The acronym rule's comment now matches its code: the three-to-five letter window is documented as deliberate precision tuning — six-plus-letter runs are UE-RE008's shouting territory — rather than an unexplained cap.
Known limitations, stated for the next audit
- Hybrid-reference files whose supplemental xref stream is named indirectly, and
/Prevchains written as indirect references, fall to the recovery path rather than being read exactly. - Encryption detection still recognises the
/Encryptkey by file text rather than trailer structure, so a document about PDF encryption can be refused as encrypted.
Release 1.7.2 — the audit made visible
No rule, exit-code or report-shape change: the release that makes the audit visible.
- The audit is watched. The
socket-audit-watchGitHub Action reads the skill's published skills.sh security page hourly — stale, green, or alerts (which alone fails the run). Its page fetch is time-bounded. - A Security audits section in the README documents the three scanners, the public security page, and how every finding in the audit history was answered in the tree, never by exclusion.
- The indexed skill description is sharpened;
socket.ymlcarries the record of what the 5 October findings became. - 1.7.1 is also now tagged and released on GitHub as v1.7.1, restoring the every-version-released convention.
The same code is published on npm as un-editorial-check@1.7.2.
Release 1.7.1 — the two Socket LOWs answered with real hardening
No rule, exit-code or report-shape change. The two LOW findings the skills.sh Socket scanner raised against lib/output.mjs and lib/icons.mjs are answered with real hardening, locked by new assertions in tests/icons.mjs and tests/release-regressions.mjs:
- Terminal hygiene. The text renderer's control-character guard now covers
ruleIdandconfidencetoo — custom rules make the id caller-controlled, so a hostile id can no more repaint the terminal than a hostile message could. - SARIF shape, flat. The
pdfPageproperty is built by assignment rather than conditional spread — byte-identical output, and no construct a fragment parser can misread. - Quote-safe icon markup. The icon renderers escape all five XML-significant characters (the double quote included) in every attribute they interpolate, and validate stroke widths and icon sizes, refusing loudly on a non-numeric value.
The same code is published on npm as un-editorial-check@1.7.1.
Release 1.7.0 — embedded Roboto Condensed and the navy report chrome
1.7.0 – 2 October 2026
The typeface and palette release. The report no longer depends on a base-14 font that the reader's own system has to supply: Roboto Condensed Regular and Bold travel with the package under fonts/ and are embedded in every PDF, subset to the glyphs the report actually paints. The two renderers break their lines from the same measured advances, the text stays searchable and copyable, and a reader sees the intended typography without installing a font. The chrome — masthead, headings, rules, fills, borders, body ink and footer — moves to a navy palette read from a single shared module, lib/palette.mjs, which both renderers import, so a colour cannot drift between the formats again.
Added
- An embedded typeface, both faces.
lib/ttf.mjsreads a TrueType file, maps itscmap, takes advances fromhmtx, subsets to the glyphs needed and recomputes each table checksum;lib/winansi.mjsholds the encoding tables the two formats share. The PDF declares each face as a simple TrueType font withWinAnsiEncoding, a 224-entry/Widthsarray measured from the subset, a/FontDescriptorcarrying scaled metrics, and a/ToUnicodeCMap so copy and search return real characters.RobotoCondensed-Regular.ttfandRobotoCondensed-Bold.ttfship besideLICENSE-APACHE.txtandNOTICE.txt, are listed inpackage.jsonfiles, are required by the portability validator, and are named inignoredPathsso the licence text is never read as report copy. The Italic face was dropped rather than carried, which keeps the package at two font files. - Locks on the two bands. The masthead and the footer band had no assertions at all.
tests/pdf-structural.mjsnow pins the masthead tox=0at the full page width with its top edge at the top of the page, the footer band to0 0 595.28 48, the divider rule toy=48, and the band to sit above its rule, on every page of six fixtures.
Changed
- The report chrome is navy. Every page of both formats carries a full-bleed masthead band of
#24356Bwith reversed white type and a#1B2852divider beneath it. Headings are#1F3A7A, light fills#EEF1F8, borders#C5CEDF, body text#171b26, and the footer sits on an#EEF1F8band under a#1B2852rule with its ink darkened to#616161. The thirteen category hues, the severities and the marker codes are unchanged, except that the warning hue now agrees across the formats at#e07b00. - Presentation only. The rules, the summary and lane counts, the exit codes, JSON and SARIF, the clean-run sentence, and the groupings of
groupedandfullare untouched. The header and footer wording is unchanged; only the band behind it and the ink on it are new.
Fixed
- The fold note no longer reports a replacement that did not occur. An en dash, an em dash, curly single and double quotes and the ellipsis all encode correctly to WinAnsi, but the predicate that decided whether to warn treated them as unrepresentable, so ordinary punctuation produced a note saying characters had fallen back to
?when none had. The note is now gated on a character that genuinely becomes?, and the suite asserts both directions: a sentence containing all seven code points produces no note, while a character that truly cannot be represented still produces one. - Every written claim that the PDF used base-14 Helvetica is corrected. The README, the claim register, the icon and legend notes and the workspace rules now describe the embedded face; the base-14 tables that remain are the ones used when reading other people's PDFs.
Unchanged
- Zero npm dependencies, byte-identical output for identical input, the rule catalogue, the refusal behaviour, and the exit codes.
Release 1.6.1 — no sink-shaped text in the repository tree
1.6.1 – 2 October 2026
No runtime change. The security-audit test fixture that the skills.sh Socket scanner flagged as a dangerous sink has left the repository: it is generated at test runtime into a temporary directory by tests/lib/make-security-fixture.mjs, whose fragments mean no contiguous sink sequence exists anywhere in the tracked tree, while the suites still prove the security profile detects both sinks in the generated file (shape locked in both directions). socket.yml and .socketignore record the retirement.
Release 1.6.0 — reading reach, vocabulary, claim register, custom rules
1.6.0 – 2 October 2026
The reading-and-vocabulary release. The checker now reads Word documents (.docx) and OpenDocument documents (.odt) directly, retrieves a page named with --url, and reads one document from standard input (--stdin); reports can be written to PDF and HTML in one run; project file types can be taught to the scanner with extraExtensions. The editorial set grows from 32 to 42 rules: superseded country designations (UE-TE005), inclusive-language terminology pairs, a spelling map grown from 74 to 171 pairs including the -yse family and programme with a computing exemption, month-first written dates, wider counted nouns, doubled words padded with extra spaces, the configuration-gated doubled-space review (UE-GR004), the acronym first-use check (UE-HR006), the English-only honesty note (UE-HR007), the configuration-gated consistency review (UE-HR008, UE-HR009) and the claim register's completeness rules (UE-CL002, UE-CL003). New capabilities: --claims-out and --claims turn "a human should verify this figure" into a tracked, verifiable register; organisation profiles may carry their own bounded pattern checks (customRules) under a new organisation category; --emit-corrected writes the recovered copy of one PDF with the deterministic corrections applied, headed by a notice that it is a working text, not an edit of the document. The report legend carries thirteen categories. Two extraction gaps found during the work are fixed: JSX-shaped copy in JavaScript files now honours ue:ignore suppressions, and extension-less inputs (stdin, URLs) dispatch correctly. tests/audit-pdf-extraction.mjs joined the release gate. Refusal behaviour is unchanged and now covers every new format: an encrypted document, a scanned PDF, a non-HTML response and a misnamed container all refuse with exit 2 and a reason, never a clean run. --fix remains limited to deterministic spelling and grammar in prose files; terminology, country names, dates and claims are reported, never rewritten.
v1.5.2
1.5.2 – 1 October 2026
A count that was spelled wrongly in both report formats, a header label that broke across two lines, and a committed sample report so a reader can see the output without running anything. What the tool finds, the counts it reports, the rule catalogue and how it exits are unchanged from 1.5.1.
Fixed
- A count of one is spelled in the singular in both report formats. The console has printed
1 errorsince the first release while--reportprinted1 errors, becauselib/html.mjsandlib/report.mjseach wrote the plural unconditionally and the console wrote it conditionally. All three surfaces now take their words from oneseverityCounts()helper inlib/output.mjs, so the wording cannot drift between them again. Four assertions had pinned the old wording and moved with the change:tests/report-model.mjsasserted1 warnings, which is the defect itself;tests/audit-lanes.mjswidened its counts pattern to accept either form; andtests/audit-report-fix.mjslost two sites, one of them deliberately rewritten longhand so that the two independent checks it makes cannot agree by both calling the same helper. A new assertion pins the singular at a count of one, which leaves the counts locked at 0, 1 and 3 in the HTML and at 0, 1, 2 and 3 in the shared report model both formats read. - The occurrence table's
Locationheader no longer breaks across two lines.overflow-wrap: anywherewas declared forthandtdtogether. It belongs to the cells, which carry copied text that may need breaking anywhere, and not to header labels, which are fixed short words; applied to a header it splitLocationasLocati on. The declaration is now scoped to cells, and a header label wraps only at a space.
Added
- A committed sample report, together with the two files it was produced from.
tests/fixtures/demo/sample-report.htmlandtests/fixtures/demo/sample-report.pdfshow a complete review, anddocs/demo/sample-report.pngshows a rendered page so the design is visible in a repository listing without downloading anything. The copy sits beside them astests/fixtures/demo/report-source.mdandtests/fixtures/demo/report-source.html, anddocs/demo/README.mdgained a section that embeds the image, tabulates the three artifacts and gives the exact command that regenerates them. - The sample copy is written to be safe to publish. It carries typographical and stylistic defects only: a doubled word, a space before punctuation, a full stop running into the next word, a numeric date, a hyphenated year range, an all-caps word, malformed wording, a verbless fragment, an unsourced figure, a missing page title and an image without alt text. The entities are invented institutional ones — the Office, the Committee, the Board — and a word-boundary sweep against country names, religions, ethnic labels, gender terms, disability terms, animals, conflict vocabulary, governments and named leaders returns nothing. The run reports 17 findings as one error, eleven warnings and five audit findings across eight deterministic, four agent-review and five audit placements, with the harmful or discriminatory, the diplomatic and the quoted lanes present and each stated to be checked with nothing found in it.
- The sample artifacts are generated from a neutral directory. A report states its scan root, so producing one inside a contributor's checkout would publish that contributor's home directory in a tracked file. The regeneration command copies the two sources to
/tmp/un-editorial-check-demoand runs from there, which is what the committed artifacts record.
Unchanged
- The rule catalogue, the summary counts, the lane counts, the exit code, JSON and SARIF, the clean-run sentence, and the groupings of
groupedandfull. - Zero npm dependencies, and byte-identical output for identical input.
Verified, and how
p48-gate.shfail=0 across all eight steps and four corpus verifiers;qa-battery.mjs6 passed, 0 failed; the repository self-scan exits 0 with every sample artifact in place.- The pluraliser was mutation-tested in both directions. Replacing it with an unconditional plural fails
tests/audit-html-report.mjsatone error is spelled in the singular, failstests/report-model.mjsat the shared counts line and failstests/audit-report-fix.mjsat the restatement. Replacing it with an unconditional singular fails all four of those suites plustests/audit-lanes.mjs. Restoring it turns every one of them green again. - The sample copy was swept for sensitive terms before it was committed, and the repository self-scan was re-run afterwards to confirm that placing the report under
tests/fixtures/keeps the deliberate defects outside the sweep rather than exempting them from it.
v1.5.1
1.5.1 – 1 October 2026
One bug that made a generated report unusable in Adobe Acrobat, and the decode-uncertainty work that was open when 1.5.0 shipped. What the tool finds and how it exits are unchanged from 1.5.0.
Fixed
- A generated
--reportPDF opens in Adobe Acrobat. Adobe rejected every page of the report withAn error exists on this pagewhile PDFKit, Quick Look,qpdfand pypdf all opened it happily, which is why the defect survived seven validators.drawHeader()wrote the header rule as four coordinates followed bylwith nomto establish a current point first, andlappends to a point that does not exist. The siblingdrawRule()emitted them, so only the header rule was wrong, and it was wrong exactly once per page — which matched the symptom. The fix adds them, and a guard comment beside it states why the operator is load-bearing rather than incidental. - Two-column PDF input is split at the gutter for the templates people actually use. The gutter floor was a fraction of the page width,
0.035, which blocked gutters up to 20.83pt while IEEEtran's 18pt,revtexandacmart's 10pt and Springer's roughly 11pt all sit beneath it. The floor is now0.015. It was never what protected single-column pages — the hanging indent, the definition list and the centred heading passed at the old floor anyway, because the real protection is the straddle-and-balance test — and0.012was measured out as too eager, splitting a 7.76pt hanging indent and an 8.64pt definition list that are indistinguishable from a gutter at that scale. Held by a pair of fixtures in both directions:two-column-narrowat 12pt must split,hanging-indentat 7.76pt must not. The corpus is byte-identical under either floor. - A header comment that described a paragraph break that the module does not make. The module header claimed a large vertical gap between two runs becomes a paragraph break. It does not: sweeps of 8, 16, 32, 64 and 96pt all return two units, because no vertical gap is ever measured. The behaviour is kept and the comment now says what the module declines to do — no paragraph structure is recovered — with a
vertical-gapfixture 96pt apart that locks the statement.
Added
- Five fixtures for the decode questions that were open, taking the extraction suite from 22 cases to 27.
two-column-narrow,hanging-indent,rotated-page,non-latinandvertical-gap. Each carries a precondition proving it exhibits the property it is meant to exhibit before the behaviour behind it is asserted, so a fixture cannot pass by being inert. The non-latin case reconstructs eighteen single-byte codes whose Unicode exists only in the document's/ToUnicodeCMap — Greek, Cyrillic and Latin-1 — exactly./Rotatedoes not change content-space reading order: copy and drawing order survive and three displayed lines arrive as one unit, so that is recorded as a tripwire rather than applied to the coordinates, which would have changed reading order in the highest-risk module against no real corpus to validate against. - A path-state assertion over every rendered report. A tokenizer walks each page and fails if any path-extending operator —
l,c,v,y,h— is applied beforemorrehas established a current point in the subpath, with a guard asserting the tokenizer actually finds path operators to inspect. It runs over six fixtures insidenpm test, so the gate enforces it rather than a one-off check.
Changed
- The deliberate security fixture holds its two sinks as quoted data rather than as two statements. The fixture gives the security profile the patterns it detects, and it is never executed and never shipped in the npm package, but a scanner reading it as product source cannot tell that from the bytes. Its sinks are now inside an exported array of strings, so a reader parsing the file as code finds no HTML assignment and no dynamic execution outside a string literal — which is what the finding describes. The profile under test loses nothing from this:
lib/audits.mjsruns two raw-text regexes after masking comments, so quoting is invisible to it andtests/run.mjsstill requires exactlyUE-SE001andUE-SE004. The lock runs in both directions, since either half alone would let the fixture drift into being unscannable or live: stripping strings and comments must leave no sink behind, and the raw source must still carry both patterns. .socketignoreis added and mirrorssocket.yml'sprojectIgnorePaths. Neither format has been shown to be honoured by the pipeline that produces the skills.sh badge, and both files say so rather than implying an exclusion that has never taken effect.
Unchanged
- The rule catalogue, the summary counts, the lane counts, the exit code, JSON and SARIF, the clean-run sentence, and the groupings of
groupedandfull. - Zero npm dependencies, and byte-identical output for identical input.
Verified, and how
npm testexit 0,tests/audit-pdf-extraction.mjsexit 0 across 27 cases.p48-gate.shfail=0 across all eight steps and four corpus verifiers;qa-battery.mjs6 passed, 0 failed.- The regenerated report went from 104 path-state problems to 0,
qpdf --checkis clean, PDFKit renders every page, and the fixed report opens without complaint in Adobe Acrobat. - Every new lock was mutation-tested in both directions: removing the
mfails witha path operator was applied with no current point, restoring the live statements in the fixture fails withthe security fixture must carry no live HTML sink outside a string literal, and moving the sinks into comments fails the audit assertion with an empty id list.
v1.5.0
The remainder of the report redesign that 1.4.0 opened: what order the sections appear in, how much of the body they print, what every row carries, and how a path is named. 1.4.0 shipped the frame — the document title, the repeating header and the footer; this release ships what the frame surrounds. What the tool finds, the counts it reports, the rule catalogue and how it exits are as they were in 1.4.0.
The sections. Both formats read one plan, so they cannot reorder the sections between themselves, cap one and not the other, or disagree about a title: Summary, Categories, then Harmful / Discriminatory Content, Diplomatic Sensitivity, Editorial Errors, Editorial Warnings, Agent Review Required, Audit Findings, then Quoted material, Priority Recommendations, Sources. A heading carries its title, its lane and its count rather than a number set before it, and a lane with nothing in it keeps its heading and states that it was checked, so a clean lane cannot be mistaken for one that never ran. The PDF previously drew one Issues heading over every authored finding and the HTML report ordered its own lanes; the deterministic lane is now split by severity into Editorial Errors and Editorial Warnings. The separate Review queue (heuristic findings) is gone — Agent Review Required is that queue and carries every field the other sections do.
The cap. Under the default grouped layout, Editorial Warnings and Agent Review Required print twenty findings and then state how many they withheld beside the exact command that lists the rest: Showing 18 of 41 · 23 not listed above. Editorial Errors, the harmful and diplomatic lanes and Audit Findings are never capped, a whole group is never split at the boundary, and --report-detail full is never capped. The cap changes no count — the section header still claims every finding the scan produced, and severity counts, lane counts and the exit code are identical between a capped and an uncapped rendering. The cap never reaches JSON or SARIF.
Paths. Every path in a report is relative to a scan root stated once as Root in Summary — the common ancestor of the targets the scan was pointed at, not the working directory — so a forwarded report names a directory its recipient can actually resolve. --format json and SARIF still carry absolute paths.
Marks and rows. A severity mark, then the category's own mark beside its short code, then the position, the rule, the matched copy under Current, the repair under Should be, the explanation and all six provenance fields — one row shape in both formats and under both detail modes, so --report-detail full is now a count of rows rather than a second renderer. The marks are hand-authored path data drawn as PDF path operators and as inline <svg>: no image asset, no icon font and no glyph, so nothing outside the file is fetched to draw them and no glyph can fall through to ? in WinAnsi Helvetica. Each mark is chosen from the row's own severity and its own category, with the two-letter code and the category's own name printed beside it, so colour and shape are never the only signal and a greyscale print loses no information.
Added. Priority Recommendations, between Quoted material and Sources, derived only from counts that are in this report — how many harmful or discriminatory findings, how many diplomatic claims, and the rules that fired most often — so the section cannot advise a reader about anything the scan did not find.
Verified. p48-gate.sh fail=0 over eight steps and four corpus verifiers, qa-battery.mjs 6/6, smoke.sh 5/5, npm test 68 suites, version anchors consistent, and --self-scan exit 0 at 42 warnings and 0 errors across 98 files. Measured on the corpus the design was checked against: 95 findings, 17 errors and 78 warnings, lanes 49/41/4/1/0, 39 grouped issues, 21 grouped blocks and 95 under full in both formats, with the cap lines reading Showing 20 of 34 · 14 not listed above. and Showing 18 of 41 · 23 not listed above. Every lock moved in this release was shown to fail when its guarantee is removed.
Unchanged. The thirty-seven rules, the five lanes, the exact clean-run sentence No findings under the enabled, documented local rules., the --fix allow-list, the exit-code mapping, the grouping arithmetic, the twelve-category legend, the construction of the document symbol, the endorsement boundary, and zero npm dependencies. JSON and SARIF carry one result per finding whatever the report draws, and neither format takes --report-detail.
v1.4.0
The first instalment of the report redesign approved for this release: the furniture that frames every page of both report formats, and the document title. What the tool finds, the counts it reports, the order of the report's sections and how it exits are as they were in 1.3.1.
The document title. Both formats carried UN Editorial Review: it was the HTML <title> and <h1>, the PDF banner, and the fallback used when a scan supplied no targets. It is now Editorial Review Report, and one constant in lib/furniture.mjs feeds every place it appears, so the two formats cannot drift apart on the wording.
The header. Two rows of two cells, repeated on every page: un-editorial-check <version> against EDITORIAL REVIEW, then Document symbol: UE/<year>/<4 digits> · <date> against Distribution: General. The header was four full-width lines. The targets are no longer repeated on every page — the cover already states them, and the space they occupied is what lets the right-hand cells sit at the right margin. The header reads EDITORIAL REVIEW and never UNITED NATIONS, exactly as before.
The footer. Three cells across the width of the page: Page N of M on the left, © <year> un-editorial-check contributors in the centre, and the repository address on the right. The year is read from the scan's own date; where the input carries no date the year is left out of the line rather than guessed or taken from the machine running the scan, so two runs over one input draw the same footer. Two older lines are removed — Prepared by un-editorial-check, an independent editorial tool and report only; findings are not changed by this report. — and neither carried a promise the reader could act on. The sentence a reader can act on, This report changes nothing; re-run the checker to verify corrections., still opens the framing block of every report as it did before.
Category artwork. The twelve categories in the legend now carry an icon as vector path data — no image asset, no icon font, no glyph — held in a new lib/icons.mjs with one emitter per format. The artwork is built and locked; neither format draws it yet.
Not in this release. The rest of the approved redesign is deliberately left for a following release: the new order of the report's sections, sections carrying no numbers, the cap of twenty warnings printed with the count withheld and the exact re-run command beside it, and the drawing of the category artwork in both formats.
Verified. npm test 68 ok exit 0, check:syntax exit 0, check:portability exit 0, tracked JSON parse exit 0, npm pack --dry-run exit 0, --self-scan exit 0 at 44 warnings and 0 errors across 98 files, git diff --check exit 0, and an installed smoke test that reports 1.4.0 and writes a report carrying the new title. Self-scan delta measured against a post-Stage-A baseline in a throwaway worktree: zero findings added by the furniture change.
Unchanged. The thirty-seven rules, the five lanes, the exact clean-run sentence No findings under the enabled, documented local rules., the --fix allow-list, the exit-code mapping, the grouping arithmetic, the twelve-category legend, the construction of the document symbol, the endorsement boundary, and zero npm dependencies. --report-detail still selects the layout, and JSON and SARIF are untouched.