Repository navigation
Changelog
The repository's CHANGELOG.md is the
source of truth. This page is a summary of the current line.
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.
-
Settings written through a symlinked
.md.inino longer replace the link. In the release-directory layout the deploy points.md.iniatmdscribe-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.iniwhile 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.
-
.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 runsverify-releaseon the rebuilt copy. It never uploads, so the comparison cannot be made true by the job itself.
-
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 vsab6c1f2d…on Linux) becauseZipArchivedecides the container's metadata. The archive is written by hand with every choice fixed. Measured: Windows, Debian and GitHub's Ubuntu runner all produceaa6ae601…for 3.6.2. -
COMPOSER_ROOT_VERSIONis pinned. Composer records the root package version invendor/composer/installed.php, resolved from git —dev-mainon a branch or an untagged commit,v3.6.1at 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.
- 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, sha256aa6ae601cd3dc219e3ba7a600966334dd7ed0f431067e7a94b953711ac3a9f5b, 988 manifest entries, 1022 files.verify-release17/17 on the built archive, and the release job 17/17 on its own rebuild. - Deployed: tree fingerprint
5c4ce6668071c15af5ddd740eb9b4fe81989327c4b42bd2ac9370448eaa4d8d5(988, 3.6.2), page fingerprint85a5701cefa7ca8b8246a2d907f29fb3652860ff24723f0552854510521f11e6(76 326 bytes); the machine API reports 988 files, all present.
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.
-
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--versionagree, 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-runwrites 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'sgeneratedfield (tools/build-manifest.phphonoursSOURCE_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.
-
The CSP header no longer repeats
img-srcsources. WithALLOW_EXTERNAL_IMAGESon, the header readimg-src 'self' blob: data: https: data:— the base of the directive already ended indata: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 insrc/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.phpin 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/andtests/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.
-
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.
- 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, sha256723a44291812545065ff91e8bfc84f51611d6354f0b0ba8f75278b9d836f9f43, 987 manifest entries, 1021 files, flat layout.tools/verify-release.phpagainst 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 fingerprint5385cb50f3fb681cccd9b4d1c23d463e7a47efc46587abb7fdc128b883896c41(76 326 bytes).
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 -Son 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 inmdscribe.releases/, runtime state inmdscribe-data/. -
Fingerprints —
verify-tree.php --fingerprint(a sha256 over the manifest's path/sha256 pairs, identical for two installations of one release) andpage-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.mjsdrives 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/assetsandsrc/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 withexpectOutputRegex()instead of silencing the check.
Fixed
-
The updater replaced the files but not the manifest.
config/manifest.jsonis 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, soconfig/manifest.jsonwas silently dropped. -
A deploy could remove
vendor/and still look successful —rsync --deletefrom 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
zipfilecarries no POSIX modes andunzipproduced0666.
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+), theA ---o B/A ---x Bedge 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 intodata-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 --vendorandnode generate-icons.mjsboth writesrc/View/Icons.php, but only the second added thecircle-alertentryIcons::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/commonmark2.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 —
ignorealso 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.
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
nowrapheaders, 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. Atypesblock replaces the inherited MIME table, so every.css,.jsand.svgbecameapplication/octet-streamand 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) withContent-Type: application/manifest+json, so no host MIME rule is needed; sub-directory installs get the rightscope/start_url(/docs/). The static file remains as a fallback.
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.jsonand 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()reportsarchiveandinstallable, and the panel disables Apply when the release carries no archive. -
config/manifest.jsonis 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.webmanifestand 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-
publicinstall (public/updater.phpbridge 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.phpon installs where that path does not exist. - Documented the static MIME type for
.webmanifestand the deliberate exclusion ofbin/md-scribe-renderfrom static analysis.
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).
MD.Scribe 3.6.2 "Colophon" · PHP 8.5 (verified on 8.5.12) · MIT · repository · releases