Skip to content

Releases: ahaomar/un-editorial-check

Release 1.8.0 — the deep-audit release

Choose a tag to compare

@ahaomar ahaomar released this 06 Oct 11:38

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 bfrange blocks repeat maximal ranges, and a composite font's /W width 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. --url follows 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 customRules pattern 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:ignore prose 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 --report vanished from the printed command; everything after -- is copied verbatim.
  • SARIF artifactLocation is a valid URI reference — filesystem paths are encoded; --url values 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 /Prev chains written as indirect references, fall to the recovery path rather than being read exactly.
  • Encryption detection still recognises the /Encrypt key 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

Choose a tag to compare

@ahaomar ahaomar released this 06 Oct 10:22

No rule, exit-code or report-shape change: the release that makes the audit visible.

  • The audit is watched. The socket-audit-watch GitHub 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.yml carries 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

Choose a tag to compare

@ahaomar ahaomar released this 06 Oct 10:14

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 ruleId and confidence too — 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 pdfPage property 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

Choose a tag to compare

@ahaomar ahaomar released this 02 Oct 13:12

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.mjs reads a TrueType file, maps its cmap, takes advances from hmtx, subsets to the glyphs needed and recomputes each table checksum; lib/winansi.mjs holds the encoding tables the two formats share. The PDF declares each face as a simple TrueType font with WinAnsiEncoding, a 224-entry /Widths array measured from the subset, a /FontDescriptor carrying scaled metrics, and a /ToUnicode CMap so copy and search return real characters. RobotoCondensed-Regular.ttf and RobotoCondensed-Bold.ttf ship beside LICENSE-APACHE.txt and NOTICE.txt, are listed in package.json files, are required by the portability validator, and are named in ignoredPaths so 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.mjs now pins the masthead to x=0 at the full page width with its top edge at the top of the page, the footer band to 0 0 595.28 48, the divider rule to y=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 #24356B with reversed white type and a #1B2852 divider beneath it. Headings are #1F3A7A, light fills #EEF1F8, borders #C5CEDF, body text #171b26, and the footer sits on an #EEF1F8 band under a #1B2852 rule 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 grouped and full are 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

Choose a tag to compare

@ahaomar ahaomar released this 02 Oct 07:00

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

Choose a tag to compare

@ahaomar ahaomar released this 02 Oct 06:47

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

Choose a tag to compare

@ahaomar ahaomar released this 01 Oct 06:19

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 error since the first release while --report printed 1 errors, because lib/html.mjs and lib/report.mjs each wrote the plural unconditionally and the console wrote it conditionally. All three surfaces now take their words from one severityCounts() helper in lib/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.mjs asserted 1 warnings, which is the defect itself; tests/audit-lanes.mjs widened its counts pattern to accept either form; and tests/audit-report-fix.mjs lost 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 Location header no longer breaks across two lines. overflow-wrap: anywhere was declared for th and td together. 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 split Location as Locati 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.html and tests/fixtures/demo/sample-report.pdf show a complete review, and docs/demo/sample-report.png shows a rendered page so the design is visible in a repository listing without downloading anything. The copy sits beside them as tests/fixtures/demo/report-source.md and tests/fixtures/demo/report-source.html, and docs/demo/README.md gained 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-demo and 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 grouped and full.
  • Zero npm dependencies, and byte-identical output for identical input.

Verified, and how

  • p48-gate.sh fail=0 across all eight steps and four corpus verifiers; qa-battery.mjs 6 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.mjs at one error is spelled in the singular, fails tests/report-model.mjs at the shared counts line and fails tests/audit-report-fix.mjs at the restatement. Replacing it with an unconditional singular fails all four of those suites plus tests/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

Choose a tag to compare

@ahaomar ahaomar released this 01 Oct 04:11

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 --report PDF opens in Adobe Acrobat. Adobe rejected every page of the report with An error exists on this page while PDFKit, Quick Look, qpdf and pypdf all opened it happily, which is why the defect survived seven validators. drawHeader() wrote the header rule as four coordinates followed by l with no m to establish a current point first, and l appends to a point that does not exist. The sibling drawRule() emitted the m, so only the header rule was wrong, and it was wrong exactly once per page — which matched the symptom. The fix adds the m, 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, revtex and acmart's 10pt and Springer's roughly 11pt all sit beneath it. The floor is now 0.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 — and 0.012 was 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-narrow at 12pt must split, hanging-indent at 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-gap fixture 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-latin and vertical-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 /ToUnicode CMap — Greek, Cyrillic and Latin-1 — exactly. /Rotate does 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 before m or re has established a current point in the subpath, with a guard asserting the tokenizer actually finds path operators to inspect. It runs over six fixtures inside npm 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.mjs runs two raw-text regexes after masking comments, so quoting is invisible to it and tests/run.mjs still requires exactly UE-SE001 and UE-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.
  • .socketignore is added and mirrors socket.yml's projectIgnorePaths. 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 grouped and full.
  • Zero npm dependencies, and byte-identical output for identical input.

Verified, and how

  • npm test exit 0, tests/audit-pdf-extraction.mjs exit 0 across 27 cases.
  • p48-gate.sh fail=0 across all eight steps and four corpus verifiers; qa-battery.mjs 6 passed, 0 failed.
  • The regenerated report went from 104 path-state problems to 0, qpdf --check is 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 m fails with a path operator was applied with no current point, restoring the live statements in the fixture fails with the 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

Choose a tag to compare

@ahaomar ahaomar released this 30 Sep 10:40

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

Choose a tag to compare

@ahaomar ahaomar released this 30 Sep 03:27

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.