Skip to content

fix: mixed vaults publish through the real Boris engine (#210) - #219

Merged
drawmeanelephant merged 1 commit into
mainfrom
fix/210-boris-mixed-vault-publish
Aug 26, 2026
Merged

drawmeanelephant merged 1 commit into
mainfrom
fix/210-boris-mixed-vault-publish

Conversation

@drawmeanelephant

Copy link
Copy Markdown
Owner

Fixes #210.

Problem

Publishing a mixed vault (Markdown + .cook) with Boris resolvable always failed: EROUTEMISSING / LinkAuditFailed. Oliver rendered recipe pages into extras, but their HTML was written to .publish/ after boris --input content … ran — and Boris's link audit is set membership against its own intended output manifest, so content/index.md's link to Recipes/Risotto.html failed the audit before that page ever existed. Pre-seeding dist/ does not satisfy the audit, and the builtin path never exercises it, which is why the existing mixed-vault test (preferBoris: false) and the Markdown-only Boris test missed it.

Fix

Render Textile/Cooklang pages through Oliver before staging, and stage them as Markdown sources whose bodies are the finished HTML (verified: CommonMark-style HTML blocks pass through Boris's renderer byte-for-byte). Each rendered recipe becomes one of Boris's own emitted pages — layout and nav included — so every route resolves in the audit. Concretely:

  • BorisPage gains prebuiltBodyHTML; BorisAdapter.stage accepts markupHTML, maps each rendered note to a <entityID>.md staged source (same closed frontmatter grammar), and writes it into content/.
  • BuiltinSiteCompiler emits prebuilt bodies directly into the theme layout (identical .publish/ shape as before); BorisCLICompiler is untouched.
  • BANALPublisher.publish() drops the post-compile extras pass entirely — both compiler paths converge on staging.
  • Notes Oliver can't render are still skipped before staging, so they never enter the index and never produce dangling links.

This lands ROADMAP M4's "Textile/Cooklang via Oliver in the same .publish/ folder" without touching Boris or widening its grammar; the rendered pages use the exact frontmatter keys serializeBorisSource already writes (id, title, status, tags). No reimplementation of any markup engine — the HTML still comes from Oliver.

Tests

  • testMixedVaultPublishesThroughBorisBinary — the Publish: mixed vault + Boris fails — recipe pages are written after Boris's link audit #210 repro end-to-end against the real boris + oliver binaries (skips when either is absent): publish succeeds, Recipes/risotto.html is emitted by Boris, index links it, source file untouched on disk.
  • testStageWritesRenderedMarkupAsMarkdownBody — binary-free unit coverage for staging (.md source with verbatim HTML body) and builtin emission.
  • Full suite green locally including the pre-existing mixed/Boris tests.

Docs updated per policy: docs/STATUS.md Publish Site entry and CHANGELOG.md under [Unreleased].

…blish through Boris (#210)

Boris's link audit counts only its own emitted pages plus assets, so a
mixed vault always failed: content/index.md linked Recipes/Risotto.html
while the rendered recipe was written to .publish/ only after compile.

Render Textile/Cooklang pages through Oliver before staging and write
them into content/ as Markdown sources whose bodies are the finished
HTML — CommonMark passes raw HTML blocks through byte-for-byte, so Boris
emits them as first-class pages (layout + nav included) and its audit
resolves every route. The builtin compiler pastes prebuilt bodies into
the theme layout directly; the post-compile extras pass is gone.
@drawmeanelephant
drawmeanelephant merged commit 5f71edb into main Aug 26, 2026
3 checks passed
@drawmeanelephant
drawmeanelephant deleted the fix/210-boris-mixed-vault-publish branch August 26, 2026 19:53
@itoqa

itoqa Bot commented Aug 26, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: c8b0379: 19 test cases ran, 18 passed ✅, 1 additional finding ⚠️.

Summary

Coverage spans publishing happy paths, mixed Markdown and rendered recipe content, nested pages, themes, navigation, assets, feeds, ordering, source preservation, retries, skipped content, cleanup, and failure handling. It also exercises boundary and adversarial behavior around empty output, unrenderable notes, compiler failures, retries, and overlapping publishes, with broad support for the expected application behavior.

Safe to merge — the only finding is a medium-severity concurrency defect in overlapping publishing, and it is explicitly unrelated to this PR rather than a regression or existing PR failure. The PR introduces no attributable failures or merge-blocking risk; the concurrency issue is a flag for later.

Tests run by Ito

View full run

Result Severity Type Description
General Whitespace-only rendered content is skipped, while even a small amount of real HTML is kept and published. This prevents blank pages without losing valid rendered content.
General A failed Boris audit stops the publish instead of creating a complete-looking site. The artifact and RSS feed are produced only after compilation succeeds.
General The local target could not run the macOS publishing app, but source and regression-test evidence supports the mixed-note publishing path. Rendered recipe HTML is prepared before staging so the publishing engine can include the page and resolve its links.
General The publishing workflow could not be opened in this Linux browser, but the local source supports creating the site artifact, copying its assets, and generating RSS from the same completed notes.
General Publishing starts with a clean staging area and rebuilds the site from the notes that are currently published. A page left over from an earlier publish is not included in the new site.
General Publishing keeps Cooklang and Textile notes in their original files, so the notes can be loaded again in their original formats. The rendered website is written separately as derived output.
General A second publish starts with clean generated folders, so output from a failed attempt cannot remain in the retry. The native recipe file stays unchanged because publishing writes only derived staging and site files.
Html A rendered recipe is staged as a Markdown page and its finished HTML is placed directly inside the normal themed page.
Html Markdown notes keep their normal rendering, while rendered Cooklang pages keep their finished HTML inside the same themed layout. The browser check was blocked by an unrelated static Linux workbench, but source inspection and publisher tests support the expected behavior.
Mixed The mixed Markdown and Cooklang publish flow could not be run because this Linux test environment cannot launch the native macOS app or the required Boris and Oliver tools. Source review confirms the flow stages rendered recipe pages before Boris compiles and audits the site.
Mixed A rendered nested recipe is given a Markdown staging path, keeps its finished HTML, and is written into the content folder for compilation.
Publish The publishing path keeps drafts out and orders published notes from newest to oldest across the site, note IDs, and RSS feed. The live check could not open the native app and the native test could not compile in Linux, but source verification supports the expected behavior.
Publish A note that cannot be rendered is recorded as skipped, while the Markdown note remains published. The skipped note is left out of the compiled ID list, page count, and RSS feed.
Render Published Markdown notes still produce site files when a recipe cannot be rendered. The unrenderable recipe is listed as skipped, left out of the site and staging files, and kept unchanged on disk.
Source Publishing creates the rendered recipe page without changing the original recipe file. The source code and publisher test both confirm that the native recipe bytes stay the same.
Source Rendered recipe content is staged at a derived Markdown path such as Recipes/risotto.md, while the original recipe file stays unchanged.
Theme Published root and nested pages keep the theme, link to each other with relative paths, and include staged assets. The browser could not open the native macOS publish screen because the local Linux workbench cannot run the app, but source checks support the expected behavior.
Theme The publisher keeps the finished recipe HTML inside the shared page theme and keeps the links between nested and root pages working. The browser check could not open the native macOS publisher in the Linux workbench, but source review and focused regression tests cover this behavior.
⚠️ Medium severity General Two publish actions can overwrite each other's temporary files. The generated site can contain a mixture of the two vault snapshots, missing pages, or a result that does not describe the files left on disk.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Overlapping publishes can mix site files
  • Severity: Medium Medium severity
  • Description: Two publish actions can overwrite each other's temporary files. The generated site can contain a mixture of the two vault snapshots, missing pages, or a result that does not describe the files left on disk.
  • Impact: When two publishes overlap, users may receive a site with mixed or missing pages instead of one complete version. Publishing again can usually replace the damaged output.
  • Steps to Reproduce:
    1. Prepare a vault with a published Markdown note and a published Cooklang or Textile note.
    2. Start two Publish Site actions almost at the same time, before the first one finishes.
    3. Wait for both actions to finish and inspect the index, nested page, assets, and reported publish result.
    4. Compare the final files with each publish's expected note set and check whether all links and pages belong to one complete snapshot.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: BANALPublisher.publish in Sources/BANALPublisher/BANALPublisher.swift:24-98 is a synchronous value-type method with no shared lock, actor, or other serialization around the complete publish transaction. It calls BorisAdapter.stage at lines 57-63 and then compiler.compile at lines 64-69 using the same configuration-provided stagingDirectory and artifactDirectory for every invocation. BorisAdapter.stage in Sources/BANALPublisher/BorisAdapter.swift:141-148 unconditionally removes the existing shared staging directory before creating content and layouts. A second invocation can remove that directory after the first invocation has begun writing it, so the first compiler can read a partially rebuilt tree or a tree belonging to the second request. BuiltinSiteCompiler.compile in Sources/BANALPublisher/SiteCompiler.swift:33-48 likewise removes and recreates the shared artifactDirectory while writing pages one at a time. BorisCLICompiler.compile in Sources/BANALPublisher/SiteCompiler.swift:124-127 removes the shared staging-relative dist directory, and lines 159-162 remove and replace the shared artifact directory after Boris exits. The later RSS and wrangler writes in BANALPublisher.swift:71-86 also target that same artifact directory. These destructive operations make overlapping calls race even though individual file writes use atomic options. The smallest practical fix is to serialize the whole publish operation for a given configuration, such as guarding BANALPublisher.publish with an actor or lock, so staging, compilation, RSS, and artifact metadata for one request finish before another request can replace the shared directories.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Publish: mixed vault + Boris fails — recipe pages are written after Boris's link audit

1 participant