Skip to content

Changelog

Mikhail Deynekin edited this page Nov 14, 2026 · 6 revisions

Changelog

The repository's CHANGELOG.md is the source of truth. This page is a summary of the current line.

3.6.2 "Colophon" — 2026-11-14

A colophon is the note at the end of a book that says how it was made. This release answers the same question for the archive: it is byte-identical wherever it is built, and a job rebuilds every published release to say so.

Fixed

  • Settings written through a symlinked .md.ini no longer replace the link. In the release-directory layout the deploy points .md.ini at mdscribe-data/.md.ini. rename() replaces the link, not its target, so the app's atomic write turned the layout inside out. Measured in production: after one page request the release held a 1 125-byte regular .md.ini while the data directory kept an 18-byte seed — and every deploy seeds the new release from that seed, so every panel setting, the admin password hash included, would have been discarded on the next upgrade. Writes now resolve a symlinked path once and land in the target, atomically, leaving the link in place.

Added

  • .github/workflows/release.yml — on every published release it rebuilds the tag twice, asserts both archives are byte-identical, compares the digest with the published asset and runs verify-release on the rebuilt copy. It never uploads, so the comparison cannot be made true by the job itself.

Changed

  • The archive is a deterministic zip. Two builds of one commit used to differ by platform (same size, same 987 manifest hashes, 1259cc70… on Windows vs ab6c1f2d… on Linux) because ZipArchive decides the container's metadata. The archive is written by hand with every choice fixed. Measured: Windows, Debian and GitHub's Ubuntu runner all produce aa6ae601… for 3.6.2.
  • COMPOSER_ROOT_VERSION is pinned. Composer records the root package version in vendor/composer/installed.php, resolved from git — dev-main on a branch or an untagged commit, v3.6.1 at the tag. That one file moved the digest by its whole value.
  • The cost of a shorter opcache window is measured — about 0.17 ms per request against a 39 ms mean; see Updates.

Verification

  • PHPUnit 236 tests, 846 assertions, 1 skipped (the skip is a dangling symlink, which Windows cannot create), PHPStan level 6 clean, doctrine checker 12/0/0.
  • MD.Scribe-3.6.2.zip, 4 267 955 bytes, sha256 aa6ae601cd3dc219e3ba7a600966334dd7ed0f431067e7a94b953711ac3a9f5b, 988 manifest entries, 1022 files. verify-release 17/17 on the built archive, and the release job 17/17 on its own rebuild.
  • Deployed: tree fingerprint 5c4ce6668071c15af5ddd740eb9b4fe81989327c4b42bd2ac9370448eaa4d8d5 (988, 3.6.2), page fingerprint 85a5701cefa7ca8b8246a2d907f29fb3652860ff24723f0552854510521f11e6 (76 326 bytes); the machine API reports 988 files, all present.

3.6.1 "Cachet" — 2026-11-14

A cachet is the seal stamped on a document to prove it is genuine — one step past 3.6.0's signet, and the point of this release: the artifact, its digest and the deployed tree come from one command.

Added

  • tools/build-release.php — the release artifact in one command. It clones the release commit (--no-hardlinks), installs runtime dependencies only, regenerates the manifest inside that clone, checks that the code version, the manifest and --version agree, re-verifies every manifest entry against its file, and writes a flat archive plus its .sha256. It refuses a dirty tree without --commit, a file changed after the manifest was built, a missing manifest entry and a shipped file the manifest never mentions — naming the file each time. --dry-run writes nothing; --verify-only=<dir> re-checks any tree.
  • Reproducible archives. Two builds of one commit used to give the same size and a different sha256. Both wall-clock dependencies were found and pinned to the commit time: the manifest's generated field (tools/build-manifest.php honours SOURCE_DATE_EPOCH; behaviour is unchanged when it is unset) and every file's mtime, which the zip records. Two builds of one commit are now byte-identical, so a published digest describes a build rather than a moment.

Fixed

  • The CSP header no longer repeats img-src sources. With ALLOW_EXTERNAL_IMAGES on, the header read img-src 'self' blob: data: https: data: — the base of the directive already ended in data: and the switch appended it again. A browser ignores the repeat, which is why it survived: the header is read by the browser and by no check in the project. The policy is now assembled in src/Core/ContentSecurityPolicy.php, which collects sources per directive and de-duplicates them; five tests pin it, including the exact production value.
  • php tools/build-manifest.php in a working tree no longer lists local scratch directories. It walked the filesystem and knew nothing about .work/, .wiki/, .zwork/ or .ragraf/, so a manifest generated in a checkout came out at 5 139 entries and 1 122 MB — a gigabyte of the project's own code index, listed as shippable. Which files ship (ReleaseBuilder::ships()) and which the manifest describes (inManifest()) are now two named predicates in one place: content/ and tests/ travel in the archive but are never described, because an update must not replace the owner's documents. After the change: 987 entries, 12.2 MB.

Changed

  • The opcache window is measured, not assumed — see Updates: 61 s without a reset on the live pool, the first request with a token-scoped opcache_reset(), and one second on a real deploy.

Verification

  • PHPUnit OK (234 tests, 839 assertions), PHPStan level 6 clean, doctrine checker 12 checks with 0 errors and 0 warnings.
  • Archive MD.Scribe-3.6.1.zip, 4 261 196 bytes, sha256 723a44291812545065ff91e8bfc84f51611d6354f0b0ba8f75278b9d836f9f43, 987 manifest entries, 1021 files, flat layout. tools/verify-release.php against the built archive and against the published asset: 17 checks each, 0 failed.
  • Deployed to a live host: tree fingerprint 14746534056189ab9b18a28b67ebadf1d71a164f41c7ac4d416416021083944d (987 entries, 3.6.1), page fingerprint 5385cb50f3fb681cccd9b4d1c23d463e7a47efc46587abb7fdc128b883896c41 (76 326 bytes).

3.6.0 "Signet" — 2026-11-14

Added

  • tools/verify-release.php — 17 checks over the published asset: the digest the release publishes, every manifest entry, a refusal of unexpected files, php -S on a free port, six routes, the PWA media type and the version on the page, then the browser gate pointed at that extracted tree.
  • tools/deploy/mdscribe-deploy — placement that proves itself: digest check, staged manifest check, one atomic symlink swap (mv -T), opcache reset, live verification, automatic rollback. --dry-run, --status, --rollback, --install-tools. Releases live side by side in mdscribe.releases/, runtime state in mdscribe-data/.
  • Fingerprints — verify-tree.php --fingerprint (a sha256 over the manifest's path/sha256 pairs, identical for two installations of one release) and page-fingerprint.php (the served HTML with the per-request nonce and CSRF token normalised away). The deploy prints both as === release evidence ===.
  • A rendering gate in CI — tools/frontend/check-render.mjs drives a real browser: diagrams rendered, no failed diagram, no empty icon, KaTeX with zero errors, display math inside the column, zero CSP violations, zero console errors, the table fits and wraps, the shipped Mermaid major, theme re-render, and a fixture proving a server-side repair reaches the renderer.

Changed

  • Asset drift is blocking, and it covers public/assets and src/View/Icons.php. Tailwind's inputs are pinned (source(none)): before, the word "invisible" in a changelog entry added a utility to the shipped stylesheet.
  • PHPUnit 12.5. One test exercises a path where the application logs through error_log(); the line is declared with expectOutputRegex() instead of silencing the check.

Fixed

  • The updater replaced the files but not the manifest. config/manifest.json is excluded from its own file list, so the replacement loop could never carry it — after an update the tree held the new files while describing the old release, and every later status check reported them as locally modified. apply() now installs the archive's manifest, the previous one travels in the backup, and a restore puts the pairing back.
  • restoreApp() skipped every nested file on Windows — native separators failed a /-only allowlist, so config/manifest.json was silently dropped.
  • A deploy could remove vendor/ and still look successful — rsync --delete from a git archive removed 731 files and every page served a fatal.
  • A switch did not change the code that ran — opcache keys scripts by the path it was asked for, not by the symlink target, so the site kept reporting the previous version for minutes; a reload does not clear the shared segment.
  • Deployed files were world-writable — an archive built by zipfile carries no POSIX modes and unzip produced 0666.

3.5.0 "Loom" — 2026-11-14

Added

  • Mermaid 12.3.0. Edge IDs with animation (A e1@--> B, e1@{ animate: true }), step-by-step flow animation (e1@{ step: 1 }, 12.3+), the A ---o B / A ---x B edge markers and the @{ shape: … } family. Verified on an 18-diagram matrix in a live browser: 17 render, the 18th is the deliberate broken fixture.
  • The 12 defaults are pinned to the previous appearance (look: classic, layout: dagre): a sequence diagram keeps its 11.x geometry exactly, a flowchart keeps 12's wider layout — measured, and written down in Markdown features.

Fixed

  • A repaired diagram was parsed unrepaired. The server wrote the repaired source into the <pre> body and the author's text into data-mermaid-source, but the loader read the attribute — so every auto-repair was discarded and the diagram failed to parse while the badge announced the repair. Reproduced on 11.17.2 too, so the defect predates this release.
  • The vendor rebuild dropped the icon fallback. node build.mjs --vendor and node generate-icons.mjs both write src/View/Icons.php, but only the second added the circle-alert entry Icons::svg() falls back to; a rebuild left an empty <svg> for any unknown name. Both generators now apply the same rule and a test fails if the fallback disappears.

Changed

  • league/commonmark 2.10.7, KaTeX 0.19.1, lucide 0.577, tailwind 4.3.5, esbuild 0.28.2, actions/checkout@v7, actions/setup-node@v7.
  • Dependabot runs monthly with a two-pull-request cap per ecosystem; lucide 1.x and phpunit 13.x are ignored by version (not by update type — ignore also silences security updates).

The release archive MD.Scribe-3.5.0.zip was verified against its own manifest (979 entries, all sha256 matching) and served from a clean extraction before publishing.

3.4.4 "Keystone" — 2026-11-13

Found by looking at the rendered page, not at the status codes.

Fixed

  • Wide tables were clipped at the column edge. Document tables were drawn at their unwrapped content width with nowrap headers, so a table wider than the reading column ran past it and the text was cut mid-word. Measured: a 736px column held an 896px table; after the fix the same table is 734px with zero clipped cells and the text wraps.
  • The documented MIME fix could break a whole vhost. The guide advised a types { application/manifest+json webmanifest; } block to type the manifest. A types block replaces the inherited MIME table, so every .css, .js and .svg became application/octet-stream and the site rendered unstyled — measured on nginx 1.24 on the host this was deployed to. The guides now warn against it and map only the extension.

Changed

  • The web app manifest is served by the application (?api=manifest) with Content-Type: application/manifest+json, so no host MIME rule is needed; sub-directory installs get the right scope/start_url (/docs/). The static file remains as a fallback.

3.4.3 "Keystone" — 2026-11-13

Patch on top of 3.4.0. Everything below 3.4.0 is included.

Changed

  • PHP 8.5 floor. 8.3 and 8.4 are dropped in the bootstrap gate, composer.json and the CI matrix. Any 8.5.x patch is accepted; 8.5.12 is the verified version.
  • MIT licence, replacing PolyForm Noncommercial 1.0.0.
  • The self-updater installs the release archive (which ships vendor/) instead of a branch tarball that could never satisfy its own manifest. Unsafe archive paths are refused; check() reports archive and installable, and the panel disables Apply when the release carries no archive.
  • config/manifest.json is regenerated (948 files, 723 of them runtime vendor); it had been frozen at 3.1.0.

Added

  • Search operators tag:, path: and quoted values, combinable with free text.
  • Five document templates (blank, ADR, meeting notes, how-to, reference) with a New from template panel.
  • Editor toolbar (bold, italic, code, link, wikilink, list, alert, table) with Ctrl/⌘+B / +I.
  • Offline reading: manifest.webmanifest and a service worker — assets cache-first, documents network-first with the last read copy as the fallback, JSON APIs never cached (PWA_ENABLED).

Fixed

  • The updater was unreachable on a docroot-public install (public/updater.php bridge added).
  • A dry run now judges the release archive, not the local manifest, so "nothing to replace" is a real answer rather than a tautology.
  • Buttons that referenced icon names absent from the generated map (file-plus, folder-plus, image-plus) rendered as the fallback warning triangle.
  • The updater landing page no longer links to public/admin.php on installs where that path does not exist.
  • Documented the static MIME type for .webmanifest and the deliberate exclusion of bin/md-scribe-render from static analysis.

3.3.0 "Lattice" — 2026-11-13

Wikilinks, backlinks, tags index, revisions, static export, and the sanitiser fix that keeps ids for headings in non-Latin scripts (the reason #что-сначала-попробовал anchors were dead).

Clone this wiki locally