pandorga 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +77 -0
- data/DESIGN.md +1434 -0
- data/LICENSE +21 -0
- data/README.md +81 -0
- data/_data/themes/dark.yml +97 -0
- data/_data/themes/light.yml +120 -0
- data/_data/themes/shared.yml +37 -0
- data/_data/translations.yml +85 -0
- data/_includes/chord-tab.html +38 -0
- data/_includes/content-runtime/00-config.html +22 -0
- data/_includes/content-runtime/10-utils.html +176 -0
- data/_includes/content-runtime/20-markdown.html +264 -0
- data/_includes/content-runtime/25-code-blocks.html +315 -0
- data/_includes/content-runtime/30-includes-bib.html +190 -0
- data/_includes/content-runtime/30-includes-xcite.html +122 -0
- data/_includes/content-runtime/30-includes.html +365 -0
- data/_includes/content-runtime/40-listing.html +225 -0
- data/_includes/content-runtime/41-listing-helpers.html +71 -0
- data/_includes/content-runtime/42-listing-filter.html +208 -0
- data/_includes/content-runtime/43-blocks.html +379 -0
- data/_includes/content-runtime/50-detail.html +861 -0
- data/_includes/content-runtime/55-toc.html +176 -0
- data/_includes/content-runtime/60-fragments.html +567 -0
- data/_includes/content-runtime/90-bootstrap.html +76 -0
- data/_includes/cut-in.html +10 -0
- data/_includes/cv/cv-box.html +18 -0
- data/_includes/dict-card.html +24 -0
- data/_includes/figure.html +36 -0
- data/_includes/home/band-index.html +43 -0
- data/_includes/home/band-listing.html +49 -0
- data/_includes/mathjax.html +10 -0
- data/_includes/ongoing-product.html +1 -0
- data/_includes/page/content-runtime.html +20 -0
- data/_includes/page/date_evolution.html +19 -0
- data/_includes/page/figure-lightbox.html +102 -0
- data/_includes/page/formatted_date.html +15 -0
- data/_includes/page/hero-default.html +18 -0
- data/_includes/page/listing-runtime-attrs.html +8 -0
- data/_includes/page/mermaid.html +101 -0
- data/_includes/page/network-graph.html +1253 -0
- data/_includes/page/page-header.html +41 -0
- data/_includes/page/professional-connections.html +2 -0
- data/_includes/page/site-footer.html +101 -0
- data/_includes/page/timeline-section.html +868 -0
- data/_includes/plotly.html +22 -0
- data/_includes/theme/dev-reexport.html +100 -0
- data/_includes/theme/favicons.html +4 -0
- data/_includes/theme/font-loader.html +55 -0
- data/_includes/theme/shell-theme-boot.html +115 -0
- data/_includes/theme/site-logo.html +6 -0
- data/_includes/theme/tailwind-theme-colors.json.liquid +65 -0
- data/_includes/theme/tailwind-theme-fonts.json.liquid +6 -0
- data/_includes/theme/theme-context.liquid +20 -0
- data/_includes/theme/theme-toggle.html +94 -0
- data/_includes/theme/theme-vars-block.liquid +161 -0
- data/_includes/theme/theme-vars.html +16 -0
- data/_includes/transcription.html +80 -0
- data/_includes/tweet.html +22 -0
- data/_includes/video.html +11 -0
- data/_includes/xcite.html +3 -0
- data/_includes/youtube-short.html +26 -0
- data/_includes/youtube.html +12 -0
- data/_layouts/article.html +27 -0
- data/_layouts/articles.html +534 -0
- data/_layouts/bibliography.html +417 -0
- data/_layouts/blog.html +544 -0
- data/_layouts/cv.html +393 -0
- data/_layouts/home.html +319 -0
- data/_layouts/network.html +9 -0
- data/_layouts/pandorga/articles.html +4 -0
- data/_layouts/pandorga/bibliography.html +4 -0
- data/_layouts/pandorga/blog.html +4 -0
- data/_layouts/pandorga/cv.html +4 -0
- data/_layouts/pandorga/media.html +4 -0
- data/_layouts/pandorga/network.html +4 -0
- data/_layouts/pandorga/portfolio.html +4 -0
- data/_layouts/post.html +7 -0
- data/_layouts/product.html +5 -0
- data/_layouts/project.html +27 -0
- data/_layouts/projects.html +534 -0
- data/_layouts/resources.html +387 -0
- data/_layouts/shell.html +203 -0
- data/_layouts/text.html +246 -0
- data/_plugins/bibliography_artifact.rb +57 -0
- data/_plugins/content_backend.rb +37 -0
- data/_plugins/content_collections.rb +51 -0
- data/_plugins/content_detail_paths.rb +58 -0
- data/_plugins/content_headers.rb +73 -0
- data/_plugins/content_keys.rb +310 -0
- data/_plugins/content_local_serve.rb +168 -0
- data/_plugins/content_site_adapter.rb +159 -0
- data/_plugins/content_validators_hook.rb +20 -0
- data/_plugins/content_writing_index.rb +52 -0
- data/_plugins/content_writing_paths.rb +55 -0
- data/_plugins/force_utf8_encoding.rb +13 -0
- data/_plugins/git_chronology.rb +211 -0
- data/_plugins/git_metadata.rb +43 -0
- data/_plugins/job_sections.rb +25 -0
- data/_plugins/lib/content_validators.rb +27 -0
- data/_plugins/lib/resource_validator.rb +56 -0
- data/_plugins/lib/tag_validator.rb +153 -0
- data/_plugins/lib/text_excerpt.rb +56 -0
- data/_plugins/lib/writing_entry.rb +107 -0
- data/_plugins/local_detail_rewrites.rb +194 -0
- data/_plugins/timeline.rb +507 -0
- data/_plugins/writing_slug_limits.rb +70 -0
- data/assets/css/article.css +1330 -0
- data/assets/css/base.css +1432 -0
- data/assets/css/cv.css +13 -0
- data/assets/css/global-link.css +94 -0
- data/assets/css/ledger/bibliography.css +307 -0
- data/assets/css/ledger/chrome.css +118 -0
- data/assets/css/ledger/cv.css +836 -0
- data/assets/css/ledger/detail.css +1270 -0
- data/assets/css/ledger/footer.css +298 -0
- data/assets/css/ledger/home.css +1490 -0
- data/assets/css/ledger/network.css +106 -0
- data/assets/css/ledger/projects.css +716 -0
- data/assets/css/ledger/rascunhos.css +379 -0
- data/assets/css/ledger/sources.css +327 -0
- data/assets/css/ledger/writing.css +754 -0
- data/assets/tailwind-play.js +15 -0
- data/examples/full/Gemfile +7 -0
- data/examples/full/_config.yml +92 -0
- data/examples/full/_redirects +3 -0
- data/examples/full/content/collections/articles/2026-01-01-welcome.md +9 -0
- data/examples/full/content/collections/data/contacts.yml +3 -0
- data/examples/full/content/collections/data/education.yml +4 -0
- data/examples/full/content/collections/jobs/research-fellow.md +12 -0
- data/examples/full/content/collections/posts/2026-01-02-note.md +9 -0
- data/examples/full/content/collections/products/ledger-tool.md +6 -0
- data/examples/full/content/collections/projects/demo-kit.md +10 -0
- data/examples/full/content/collections/resources/style-guide.md +7 -0
- data/examples/full/content/pages/cv/summary.md +1 -0
- data/examples/full/content/pages/headers.yml +20 -0
- data/examples/full/index.html +4 -0
- data/examples/minimal/Gemfile +7 -0
- data/examples/minimal/_config.yml +47 -0
- data/examples/minimal/_redirects +3 -0
- data/examples/minimal/content/collections/articles/2026-01-01-hello.md +10 -0
- data/examples/minimal/content/collections/articles/2026-01-02-second.md +9 -0
- data/examples/minimal/content/pages/headers.yml +6 -0
- data/examples/minimal/index.html +4 -0
- data/exe/pandorga +7 -0
- data/functions/api/studio/[[path]].js +562 -0
- data/functions/api/studio/_lib/auth.js +131 -0
- data/functions/api/studio/_lib/blocks.js +156 -0
- data/functions/api/studio/_lib/gemini.js +62 -0
- data/functions/api/studio/_lib/github.js +278 -0
- data/functions/api/studio/_lib/guidelines.js +143 -0
- data/functions/api/studio/_lib/paths.js +76 -0
- data/functions/api/studio/_lib/prompts.js +165 -0
- data/functions/api/studio/_lib/rate-limit.js +40 -0
- data/functions/api/studio/_lib/rewrite.js +136 -0
- data/functions/api/studio/_lib/sanitize.js +143 -0
- data/lib/pandorga/cli.rb +66 -0
- data/lib/pandorga/commands/doctor.rb +66 -0
- data/lib/pandorga/commands/export.rb +30 -0
- data/lib/pandorga/commands/install_functions.rb +41 -0
- data/lib/pandorga/commands/new.rb +113 -0
- data/lib/pandorga/commands/publish.rb +53 -0
- data/lib/pandorga/commands/serve.rb +34 -0
- data/lib/pandorga/commands/studio_schema.rb +309 -0
- data/lib/pandorga/jekyll/generator.rb +74 -0
- data/lib/pandorga/jekyll/plugins/bibliography_artifact.rb +57 -0
- data/lib/pandorga/jekyll/plugins/content_backend.rb +37 -0
- data/lib/pandorga/jekyll/plugins/content_collections.rb +51 -0
- data/lib/pandorga/jekyll/plugins/content_detail_paths.rb +58 -0
- data/lib/pandorga/jekyll/plugins/content_headers.rb +73 -0
- data/lib/pandorga/jekyll/plugins/content_keys.rb +310 -0
- data/lib/pandorga/jekyll/plugins/content_local_serve.rb +168 -0
- data/lib/pandorga/jekyll/plugins/content_site_adapter.rb +159 -0
- data/lib/pandorga/jekyll/plugins/content_validators_hook.rb +20 -0
- data/lib/pandorga/jekyll/plugins/content_writing_index.rb +52 -0
- data/lib/pandorga/jekyll/plugins/content_writing_paths.rb +55 -0
- data/lib/pandorga/jekyll/plugins/force_utf8_encoding.rb +13 -0
- data/lib/pandorga/jekyll/plugins/git_chronology.rb +211 -0
- data/lib/pandorga/jekyll/plugins/git_metadata.rb +43 -0
- data/lib/pandorga/jekyll/plugins/job_sections.rb +25 -0
- data/lib/pandorga/jekyll/plugins/lib/content_validators.rb +27 -0
- data/lib/pandorga/jekyll/plugins/lib/resource_validator.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/tag_validator.rb +153 -0
- data/lib/pandorga/jekyll/plugins/lib/text_excerpt.rb +56 -0
- data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +107 -0
- data/lib/pandorga/jekyll/plugins/local_detail_rewrites.rb +194 -0
- data/lib/pandorga/jekyll/plugins/timeline.rb +507 -0
- data/lib/pandorga/jekyll/plugins/writing_slug_limits.rb +70 -0
- data/lib/pandorga/jekyll.rb +15 -0
- data/lib/pandorga/object_store_env.rb +51 -0
- data/lib/pandorga/registry/schema.yml +115 -0
- data/lib/pandorga/registry.rb +279 -0
- data/lib/pandorga/version.rb +5 -0
- data/lib/pandorga.rb +19 -0
- data/scripts/cdn-allowlist.json +19 -0
- data/scripts/content/backfill-content-keys.rb +99 -0
- data/scripts/content/band_art.rb +362 -0
- data/scripts/content/export-content-json.rb +741 -0
- data/scripts/content/flatten-ansi-capture.mjs +153 -0
- data/scripts/content/hero_portraits.rb +28 -0
- data/scripts/content/publish-content-to-object-store.sh +78 -0
- data/scripts/content/publish-content-to-r2.sh +5 -0
- data/scripts/content/sync-content-json-to-r2.rb +167 -0
- data/scripts/content/sync-content-redirects.rb +12 -0
- data/scripts/generate-configuration-doc.rb +127 -0
- data/scripts/lib/content_license.rb +50 -0
- data/scripts/lib/content_storage_config.rb +70 -0
- data/scripts/lib/normalize-display-math.mjs +52 -0
- data/scripts/lib/r2_json_sync.rb +76 -0
- data/scripts/lib/withdrawn_public_objects.rb +23 -0
- data/scripts/lib/writing_index_contract.rb +61 -0
- data/scripts/pdf/md-to-pdf.mjs +1400 -0
- data/scripts/pdf/md-to-pdf.sh +27 -0
- data/scripts/test/fixtures/dual-template/_config.yml +32 -0
- data/scripts/test/fixtures/dual-template/index.html +4 -0
- data/scripts/test/render-parity/article-note.json +5 -0
- data/scripts/test/render-parity/blockquote.json +5 -0
- data/scripts/test/render-parity/blocks/ansi-basic.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-bold-is-bright.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-drops-cursor-moves.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-escapes-markup.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-extended-colour.json +7 -0
- data/scripts/test/render-parity/blocks/ansi-real-control-byte.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-date-once.json +19 -0
- data/scripts/test/render-parity/blocks/archive-card-empty-foot.json +7 -0
- data/scripts/test/render-parity/blocks/archive-card-footer-dates.json +22 -0
- data/scripts/test/render-parity/blocks/archive-card-meta-parts.json +26 -0
- data/scripts/test/render-parity/blocks/archive-card.json +32 -0
- data/scripts/test/render-parity/blocks/cta-link.json +13 -0
- data/scripts/test/render-parity/blocks/lang-pill-pt.json +8 -0
- data/scripts/test/render-parity/blocks/lang-pill.json +8 -0
- data/scripts/test/render-parity/blocks/ledger-row-meta-parts.json +29 -0
- data/scripts/test/render-parity/blocks/ledger-row.json +35 -0
- data/scripts/test/render-parity/blocks/meta-strip.json +27 -0
- data/scripts/test/render-parity/blocks/poster-media-image.json +8 -0
- data/scripts/test/render-parity/blocks/poster-media-video.json +9 -0
- data/scripts/test/render-parity/blocks/section-banner.json +13 -0
- data/scripts/test/render-parity/blocks/system-card-custom-meta.json +22 -0
- data/scripts/test/render-parity/blocks/system-card.json +48 -0
- data/scripts/test/render-parity/blocks/tag-chips-links.json +12 -0
- data/scripts/test/render-parity/blocks/tag-chips.json +9 -0
- data/scripts/test/render-parity/cut-in-blank-line.json +9 -0
- data/scripts/test/render-parity/cut-in-right.json +9 -0
- data/scripts/test/render-parity/cut-in.json +8 -0
- data/scripts/test/render-parity/figure.json +5 -0
- data/scripts/test/render-parity/load-runtime.mjs +337 -0
- data/scripts/test/render-parity/ongoing-product.json +5 -0
- data/scripts/test/render-parity/plotly.json +5 -0
- data/scripts/test/render-parity/poem.json +5 -0
- data/scripts/test/render-parity/run-absent-field-checks.mjs +232 -0
- data/scripts/test/render-parity/run-block-fixture.mjs +86 -0
- data/scripts/test/render-parity/run-detail-parity-checks.mjs +123 -0
- data/scripts/test/render-parity/run-field-flow-checks.mjs +227 -0
- data/scripts/test/render-parity/run-fixture.mjs +159 -0
- data/scripts/test/render-parity/selectors.mjs +43 -0
- data/scripts/test/render-parity/video.json +5 -0
- data/scripts/test/render-parity/youtube-short.json +10 -0
- data/scripts/test/render-parity/youtube.json +8 -0
- data/scripts/test/test-absent-field-absent-element.rb +43 -0
- data/scripts/test/test-cdn-pins.rb +44 -0
- data/scripts/test/test-cloudflare-watch-paths-doc.rb +31 -0
- data/scripts/test/test-configuration-doc.rb +36 -0
- data/scripts/test/test-content-detail-urls.rb +346 -0
- data/scripts/test/test-content-keys.rb +110 -0
- data/scripts/test/test-content-license.rb +45 -0
- data/scripts/test/test-content-render-parity.rb +38 -0
- data/scripts/test/test-content-validators.rb +12 -0
- data/scripts/test/test-css-structure.rb +72 -0
- data/scripts/test/test-cv-export-contract.rb +158 -0
- data/scripts/test/test-detail-render-parity.rb +40 -0
- data/scripts/test/test-dev-reexport.rb +122 -0
- data/scripts/test/test-doctor.rb +39 -0
- data/scripts/test/test-dual-template-instances.rb +87 -0
- data/scripts/test/test-dual-template-runtime.rb +167 -0
- data/scripts/test/test-examples-build.rb +60 -0
- data/scripts/test/test-export-content-json-incremental.rb +138 -0
- data/scripts/test/test-export.rb +53 -0
- data/scripts/test/test-exported-fields-reach-the-view.rb +60 -0
- data/scripts/test/test-form-control-theming.rb +143 -0
- data/scripts/test/test-git-chronology.rb +414 -0
- data/scripts/test/test-hero-portraits.mjs +97 -0
- data/scripts/test/test-hero-portraits.rb +162 -0
- data/scripts/test/test-home-band-art.rb +135 -0
- data/scripts/test/test-install-functions.rb +39 -0
- data/scripts/test/test-language-export.rb +79 -0
- data/scripts/test/test-listing-filter-contract.rb +119 -0
- data/scripts/test/test-math-row-breaks.mjs +79 -0
- data/scripts/test/test-math-row-breaks.rb +18 -0
- data/scripts/test/test-new.rb +113 -0
- data/scripts/test/test-no-personal-data.rb +85 -0
- data/scripts/test/test-public-cv-export.rb +164 -0
- data/scripts/test/test-r2-config-consistency.rb +57 -0
- data/scripts/test/test-r2-json-sync.rb +193 -0
- data/scripts/test/test-registry-home.rb +34 -0
- data/scripts/test/test-runtime-js-syntax.rb +64 -0
- data/scripts/test/test-slim-writing-index.rb +126 -0
- data/scripts/test/test-studio-api-contract.rb +99 -0
- data/scripts/test/test-studio-content-files.rb +79 -0
- data/scripts/test/test-studio-liquid-roundtrip.mjs +179 -0
- data/scripts/test/test-studio-rate-limit.mjs +70 -0
- data/scripts/test/test-studio-rewrite-api-contract.rb +59 -0
- data/scripts/test/test-studio-rewrite-sanitize.mjs +210 -0
- data/scripts/test/test-studio-schema-composition.rb +126 -0
- data/scripts/test/test-studio-slug-config.rb +167 -0
- data/scripts/test/test-template-packages.rb +51 -0
- data/scripts/test/test-text-excerpt.rb +51 -0
- data/scripts/test/test-unwrap-media-src.mjs +39 -0
- data/scripts/test/test-unwrap-media-src.rb +19 -0
- data/scripts/test/test-withdrawn-content-export.rb +69 -0
- data/scripts/test/test-writing-index-export-slugs.rb +84 -0
- data/scripts/test/test-writing-slug-limits.rb +61 -0
- data/scripts/test/test-xcite-chronology.rb +106 -0
- data/scripts/validate.sh +74 -0
- data/scripts/visual/build-cv-pages-json.js +33 -0
- data/scripts/visual/build-site-pages-json.js +94 -0
- data/scripts/visual/publish-visual-inspection-to-r2.sh +42 -0
- data/scripts/visual/take-screenshots.js +88 -0
- data/scripts/visual/visual-inspection-pages.json +113 -0
- data/studio/assets/index-B_j0pMD5.css +1 -0
- data/studio/assets/index-Ch4YEv9g.js +285 -0
- data/studio/index.html +27 -0
- data/studio/schema.yml +869 -0
- data/studio-app/README.md +27 -0
- data/studio-app/index.html +26 -0
- data/studio-app/package-lock.json +2499 -0
- data/studio-app/package.json +26 -0
- data/studio-app/skills-lock.json +47 -0
- data/studio-app/src/api/client.js +71 -0
- data/studio-app/src/editor/body-editor.js +70 -0
- data/studio-app/src/editor/liquid-core.js +382 -0
- data/studio-app/src/editor/liquid.js +77 -0
- data/studio-app/src/lib/document.js +207 -0
- data/studio-app/src/lib/list-controls.js +317 -0
- data/studio-app/src/lib/schema.js +66 -0
- data/studio-app/src/lib/yaml-doc.js +100 -0
- data/studio-app/src/main.js +3693 -0
- data/studio-app/src/media/browser.js +578 -0
- data/studio-app/src/preview/render.js +430 -0
- data/studio-app/src/styles/studio.css +2826 -0
- data/studio-app/vite.config.js +25 -0
- data/templates/articles/README.md +40 -0
- data/templates/articles/fixtures/article.md +9 -0
- data/templates/articles/screenshots/README.md +6 -0
- data/templates/articles/studio.yml +73 -0
- data/templates/articles/template.yml +21 -0
- data/templates/bibliography/README.md +41 -0
- data/templates/bibliography/fixtures/references.json +9 -0
- data/templates/bibliography/screenshots/README.md +6 -0
- data/templates/bibliography/studio.yml +66 -0
- data/templates/bibliography/template.yml +18 -0
- data/templates/blog/README.md +38 -0
- data/templates/blog/fixtures/post.md +9 -0
- data/templates/blog/screenshots/README.md +6 -0
- data/templates/blog/studio.yml +69 -0
- data/templates/blog/template.yml +20 -0
- data/templates/cv/README.md +34 -0
- data/templates/cv/fixtures/job.md +12 -0
- data/templates/cv/screenshots/README.md +6 -0
- data/templates/cv/studio.yml +200 -0
- data/templates/cv/template.yml +25 -0
- data/templates/media/README.md +38 -0
- data/templates/media/fixtures/resource.md +7 -0
- data/templates/media/screenshots/README.md +6 -0
- data/templates/media/studio.yml +58 -0
- data/templates/media/template.yml +17 -0
- data/templates/network/README.md +34 -0
- data/templates/network/fixtures/sources.json +3 -0
- data/templates/network/screenshots/README.md +6 -0
- data/templates/network/studio.yml +28 -0
- data/templates/network/template.yml +10 -0
- data/templates/portfolio/README.md +39 -0
- data/templates/portfolio/fixtures/project.md +10 -0
- data/templates/portfolio/screenshots/README.md +6 -0
- data/templates/portfolio/studio.yml +89 -0
- data/templates/portfolio/template.yml +18 -0
- metadata +505 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
7
|
+
DOC = ROOT.join("docs/ops/cloudflare.md")
|
|
8
|
+
|
|
9
|
+
def check!(condition, message = "condition was false")
|
|
10
|
+
raise "Assertion failed: #{message}" unless condition
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
text = DOC.read
|
|
14
|
+
|
|
15
|
+
puts "Testing Cloudflare watch paths documentation..."
|
|
16
|
+
|
|
17
|
+
check! text.include?("`*`"), "missing include pattern `*`"
|
|
18
|
+
check! text.include?("`content/*`"), "missing exclude pattern content/*"
|
|
19
|
+
check! text.include?("`docs/*`"), "missing exclude pattern docs/*"
|
|
20
|
+
check! text.include?("Settings → Build → Build watch paths"),
|
|
21
|
+
"missing dashboard navigation path"
|
|
22
|
+
check! text.include?("SKIP_DEPENDENCY_INSTALL"),
|
|
23
|
+
"missing SKIP_DEPENDENCY_INSTALL (avoids Pages pip install of study deps)"
|
|
24
|
+
check! text.include?("bundle install") && text.include?("jekyll build"),
|
|
25
|
+
"missing lean Pages build command with explicit bundle install"
|
|
26
|
+
check! text.include?("pandorga install-functions"),
|
|
27
|
+
"missing pandorga install-functions in Pages build command (PLT-0.2)"
|
|
28
|
+
check! text.include?("3.4.4"),
|
|
29
|
+
"missing Pages v3 Ruby 3.4.4 pin (avoids compiling Ruby from source)"
|
|
30
|
+
|
|
31
|
+
puts "Cloudflare watch paths documentation OK."
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Refs: PLT-AC-11
|
|
5
|
+
require "open3"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
9
|
+
GENERATOR = ROOT.join("scripts/generate-configuration-doc.rb")
|
|
10
|
+
SCHEMA = ROOT.join("lib/pandorga/registry/schema.yml")
|
|
11
|
+
DOC = ROOT.join("docs/configuration.md")
|
|
12
|
+
|
|
13
|
+
unless SCHEMA.file?
|
|
14
|
+
warn "FAIL test-configuration-doc: missing #{SCHEMA}"
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless DOC.file?
|
|
19
|
+
warn "FAIL test-configuration-doc: missing #{DOC}"
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
stdout, stderr, status = Open3.capture3("ruby", GENERATOR.to_s, "--check")
|
|
24
|
+
unless status.success?
|
|
25
|
+
warn "FAIL test-configuration-doc: #{stderr}\n#{stdout}"
|
|
26
|
+
exit 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
%w[cv articles blog media network bibliography portfolio].each do |name|
|
|
30
|
+
next if DOC.read.include?(name) && SCHEMA.read.include?(name)
|
|
31
|
+
|
|
32
|
+
warn "FAIL test-configuration-doc: template #{name} missing from doc or schema"
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "PASS test-configuration-doc"
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
$stdout.sync = true
|
|
5
|
+
$stderr.sync = true
|
|
6
|
+
|
|
7
|
+
# End-to-end gate: editorial JSON export (storage contract) + shell detail URLs.
|
|
8
|
+
#
|
|
9
|
+
# Simulates CMS publish → R2 JSON → shell poster (HTML) → runtime JSON fetch.
|
|
10
|
+
# Fails if any published post/article would 404 at /posts/:slug/ or /articles/:slug/
|
|
11
|
+
# without a per-slug shell rebuild.
|
|
12
|
+
#
|
|
13
|
+
# Run via ./scripts/validate.sh and content-detail-url-gate.yml.
|
|
14
|
+
|
|
15
|
+
require "fileutils"
|
|
16
|
+
require "json"
|
|
17
|
+
require "net/http"
|
|
18
|
+
require "open3"
|
|
19
|
+
require "pathname"
|
|
20
|
+
require "socket"
|
|
21
|
+
require "tempfile"
|
|
22
|
+
require "timeout"
|
|
23
|
+
|
|
24
|
+
require_relative "../../_plugins/content_detail_paths"
|
|
25
|
+
|
|
26
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
27
|
+
CONTENT_ROOT = REPO_ROOT.join("content")
|
|
28
|
+
EXPORT_ROOT = REPO_ROOT.join("_content_json")
|
|
29
|
+
SITE_ROOT = REPO_ROOT.join("_site")
|
|
30
|
+
REDIRECTS_FILE = REPO_ROOT.join("_redirects")
|
|
31
|
+
|
|
32
|
+
SYNTHETIC_SLUG = "2099-01-01-ci-detail-url-gate"
|
|
33
|
+
SYNTHETIC_MARKER = "Synthetic post proving detail URLs work without a shell rebuild."
|
|
34
|
+
|
|
35
|
+
DETAIL_RUNTIME_MARKERS = [
|
|
36
|
+
"ContentRuntime",
|
|
37
|
+
"contentSlug",
|
|
38
|
+
"loadCollection",
|
|
39
|
+
"Skipping missing content object"
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
def fail!(message)
|
|
43
|
+
raise message
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check!(condition, message)
|
|
47
|
+
fail!(message) unless condition
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run!(command, chdir: REPO_ROOT, env: {})
|
|
51
|
+
stdout, stderr, status = Open3.capture3(env, *command, chdir: chdir.to_s)
|
|
52
|
+
return [stdout, stderr] if status.success?
|
|
53
|
+
|
|
54
|
+
fail!("Command failed (#{command.join(' ')}):\n#{stderr}\n#{stdout}")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def free_port
|
|
58
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
59
|
+
port = server.addr[1]
|
|
60
|
+
server.close
|
|
61
|
+
port
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_redirects!
|
|
65
|
+
check!(REDIRECTS_FILE.file?, "Missing #{REDIRECTS_FILE}")
|
|
66
|
+
|
|
67
|
+
text = REDIRECTS_FILE.read
|
|
68
|
+
check!(text.include?("/posts/:slug #{ContentDetailPaths::POST_SHELL.rstrip} 200"),
|
|
69
|
+
"_redirects must rewrite /posts/:slug/ to #{ContentDetailPaths::POST_SHELL}")
|
|
70
|
+
check!(text.include?("/articles/:slug #{ContentDetailPaths::ARTICLE_SHELL.rstrip} 200"),
|
|
71
|
+
"_redirects must rewrite /articles/:slug/ to #{ContentDetailPaths::ARTICLE_SHELL}")
|
|
72
|
+
check!(text.include?("/products/:slug #{ContentDetailPaths::PRODUCT_SHELL.rstrip} 200"),
|
|
73
|
+
"_redirects must rewrite /products/:slug/ to #{ContentDetailPaths::PRODUCT_SHELL}")
|
|
74
|
+
check!(text.include?("/projects/:slug #{ContentDetailPaths::PROJECT_SHELL.rstrip} 200"),
|
|
75
|
+
"_redirects must rewrite /projects/:slug/ to #{ContentDetailPaths::PROJECT_SHELL}")
|
|
76
|
+
require_relative "../../_plugins/content_keys"
|
|
77
|
+
check!(ContentKeys.generated_redirects_precede_catchalls?(text),
|
|
78
|
+
"_redirects must place content-key 301s above /articles/:slug, /posts/:slug, and /blog/:slug 200 rewrites")
|
|
79
|
+
|
|
80
|
+
if text.match?(%r{/social/(posts|articles)/|/social/(posts|articles)/detail})
|
|
81
|
+
fail!("_redirects still targets /social/ detail shells (regression): use #{ContentDetailPaths::POST_SHELL} and #{ContentDetailPaths::ARTICLE_SHELL}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def prepare_synthetic_post!
|
|
86
|
+
posts_dir = CONTENT_ROOT.join("collections/posts")
|
|
87
|
+
synthetic = posts_dir.join("#{SYNTHETIC_SLUG}.md")
|
|
88
|
+
return if synthetic.file?
|
|
89
|
+
|
|
90
|
+
File.write(
|
|
91
|
+
synthetic,
|
|
92
|
+
<<~MARKDOWN
|
|
93
|
+
---
|
|
94
|
+
layout: post
|
|
95
|
+
title: CI detail URL gate
|
|
96
|
+
date: 2099-01-01
|
|
97
|
+
tags:
|
|
98
|
+
- devaneio
|
|
99
|
+
language: ptbr
|
|
100
|
+
key: ci-detail-url-gate
|
|
101
|
+
description: Synthetic fixture for detail URL regression tests.
|
|
102
|
+
published: true
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
#{SYNTHETIC_MARKER}
|
|
106
|
+
MARKDOWN
|
|
107
|
+
)
|
|
108
|
+
@synthetic_created = true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def cleanup_synthetic_post!
|
|
112
|
+
return unless @synthetic_created
|
|
113
|
+
|
|
114
|
+
path = CONTENT_ROOT.join("collections/posts/#{SYNTHETIC_SLUG}.md")
|
|
115
|
+
FileUtils.rm_f(path)
|
|
116
|
+
|
|
117
|
+
# Re-export so a local _content_json left from this gate never retains the
|
|
118
|
+
# synthetic slug (the content publish workflow no longer runs this gate).
|
|
119
|
+
export_content!
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def export_content!
|
|
123
|
+
run!(%w[bundle exec ruby scripts/content/export-content-json.rb content _content_json],
|
|
124
|
+
env: { "CONTENT_API_BASE_URL" => "" })
|
|
125
|
+
check!(EXPORT_ROOT.join("manifest.json").file?, "Export did not write manifest.json")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def load_manifest
|
|
129
|
+
JSON.parse(EXPORT_ROOT.join("manifest.json").read)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def verify_export_contract!(manifest)
|
|
133
|
+
%w[posts articles].each do |collection|
|
|
134
|
+
files = manifest.dig("files", collection)
|
|
135
|
+
check!(files.is_a?(Array) && !files.empty?, "manifest.files.#{collection} is empty")
|
|
136
|
+
|
|
137
|
+
files.each do |relative_path|
|
|
138
|
+
json_path = EXPORT_ROOT.join(relative_path)
|
|
139
|
+
check!(json_path.file?, "Missing exported JSON: #{relative_path}")
|
|
140
|
+
|
|
141
|
+
payload = JSON.parse(json_path.read)
|
|
142
|
+
slug = payload["slug"].to_s
|
|
143
|
+
check!(!slug.empty?, "JSON missing slug: #{relative_path}")
|
|
144
|
+
check!(payload["body_markdown"].to_s.strip != "", "JSON missing body_markdown: #{relative_path}")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
synth = EXPORT_ROOT.join("collections/posts/#{SYNTHETIC_SLUG}.json")
|
|
149
|
+
check!(synth.file?, "Synthetic post was not exported to storage layout")
|
|
150
|
+
check!(JSON.parse(synth.read)["body_markdown"].include?(SYNTHETIC_MARKER),
|
|
151
|
+
"Synthetic post body missing marker text")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def build_shell!
|
|
155
|
+
run!(%w[bundle exec jekyll build --baseurl "" --config _config.yml,_config_dev.yml],
|
|
156
|
+
env: { "CONTENT_API_BASE_URL" => "" })
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def verify_shell_structure!
|
|
160
|
+
check!(SITE_ROOT.join("pages/blog/index.html").file?,
|
|
161
|
+
"Missing blog listing shell at /pages/blog/")
|
|
162
|
+
check!(SITE_ROOT.join("pages/articles/index.html").file?,
|
|
163
|
+
"Missing articles listing shell at /pages/articles/")
|
|
164
|
+
check!(SITE_ROOT.join(ContentDetailPaths::POST_SHELL.delete_prefix("/"), "index.html").file?,
|
|
165
|
+
"Missing rewrite target #{ContentDetailPaths::POST_SHELL}")
|
|
166
|
+
check!(SITE_ROOT.join(ContentDetailPaths::ARTICLE_SHELL.delete_prefix("/"), "index.html").file?,
|
|
167
|
+
"Missing rewrite target #{ContentDetailPaths::ARTICLE_SHELL}")
|
|
168
|
+
check!(SITE_ROOT.join(ContentDetailPaths::PRODUCT_SHELL.delete_prefix("/"), "index.html").file?,
|
|
169
|
+
"Missing rewrite target #{ContentDetailPaths::PRODUCT_SHELL}")
|
|
170
|
+
check!(SITE_ROOT.join(ContentDetailPaths::PROJECT_SHELL.delete_prefix("/"), "index.html").file?,
|
|
171
|
+
"Missing rewrite target #{ContentDetailPaths::PROJECT_SHELL}")
|
|
172
|
+
check!(!SITE_ROOT.join("products").directory?,
|
|
173
|
+
"Static /products/ pages must not be generated; detail URLs use runtime fetch")
|
|
174
|
+
check!(!SITE_ROOT.join("projects").directory?,
|
|
175
|
+
"Static /projects/ pages must not be generated; detail URLs use runtime fetch")
|
|
176
|
+
check!(!SITE_ROOT.join("social").directory?,
|
|
177
|
+
"Obsolete /social/ shells still generated; detail URLs must reuse listing layouts")
|
|
178
|
+
check!(SITE_ROOT.join("_redirects").file?, "_redirects was not copied into _site")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def slug_from_manifest_path(relative_path)
|
|
182
|
+
File.basename(relative_path, ".json")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def detail_path(type, slug)
|
|
186
|
+
case type
|
|
187
|
+
when "post" then ContentDetailPaths.post_detail_path(slug)
|
|
188
|
+
when "article" then ContentDetailPaths.article_detail_path(slug)
|
|
189
|
+
when "product" then ContentDetailPaths.product_detail_path(slug)
|
|
190
|
+
when "project" then ContentDetailPaths.project_detail_path(slug)
|
|
191
|
+
else
|
|
192
|
+
fail!("Unknown detail type: #{type}")
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def json_path(type, slug)
|
|
197
|
+
collection = case type
|
|
198
|
+
when "post" then "posts"
|
|
199
|
+
when "article" then "articles"
|
|
200
|
+
when "product" then "products"
|
|
201
|
+
when "project" then "projects"
|
|
202
|
+
else fail!("Unknown detail type: #{type}")
|
|
203
|
+
end
|
|
204
|
+
"/collections/#{collection}/#{slug}.json"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def http_get(port, path)
|
|
208
|
+
uri = URI("http://127.0.0.1:#{port}#{path}")
|
|
209
|
+
Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
|
|
210
|
+
http.get(uri.request_uri)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def verify_runtime_detail!(port, type, slug)
|
|
215
|
+
url = detail_path(type, slug)
|
|
216
|
+
rewrite_target = ContentDetailPaths.rewrite_path(url)
|
|
217
|
+
check!(rewrite_target, "No local rewrite registered for #{url}")
|
|
218
|
+
|
|
219
|
+
shell_path = SITE_ROOT.join(rewrite_target.delete_prefix("/").chomp("/"), "index.html")
|
|
220
|
+
check!(shell_path.file?, "Rewrite target missing on disk: #{rewrite_target}")
|
|
221
|
+
|
|
222
|
+
response = http_get(port, url)
|
|
223
|
+
check!(response.code.to_i == 200,
|
|
224
|
+
"Detail URL #{url} returned HTTP #{response.code} (expected 200). Published content would 404 in production.")
|
|
225
|
+
|
|
226
|
+
body = response.body.to_s
|
|
227
|
+
DETAIL_RUNTIME_MARKERS.each do |marker|
|
|
228
|
+
check!(body.include?(marker),
|
|
229
|
+
"Detail URL #{url} is missing runtime marker #{marker.inspect}; shell cannot load storage JSON")
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
json_url = json_path(type, slug)
|
|
233
|
+
json_response = http_get(port, json_url)
|
|
234
|
+
check!(json_response.code.to_i == 200,
|
|
235
|
+
"Storage JSON #{json_url} returned HTTP #{json_response.code} for slug #{slug}")
|
|
236
|
+
|
|
237
|
+
payload = JSON.parse(json_response.body)
|
|
238
|
+
check!(payload["slug"] == slug, "JSON slug mismatch for #{json_url}")
|
|
239
|
+
check!(payload["body_markdown"].to_s.strip != "", "JSON body empty for #{json_url}")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def start_server!(port)
|
|
243
|
+
log = Tempfile.new("jekyll-serve-log")
|
|
244
|
+
log.close
|
|
245
|
+
|
|
246
|
+
wrapper_pid = spawn(
|
|
247
|
+
{ "CONTENT_API_BASE_URL" => "" },
|
|
248
|
+
"bundle", "exec", "jekyll", "serve",
|
|
249
|
+
"--host", "127.0.0.1",
|
|
250
|
+
"--port", port.to_s,
|
|
251
|
+
"--config", "_config.yml,_config_dev.yml",
|
|
252
|
+
"--skip-initial-build",
|
|
253
|
+
"--no-watch",
|
|
254
|
+
"--detach",
|
|
255
|
+
[:out, :err] => log.path,
|
|
256
|
+
chdir: REPO_ROOT.to_s
|
|
257
|
+
)
|
|
258
|
+
_pid, status = Process.wait2(wrapper_pid)
|
|
259
|
+
output = File.read(log.path)
|
|
260
|
+
fail!("jekyll serve --detach failed:\n#{output}") unless status.success?
|
|
261
|
+
|
|
262
|
+
if output =~ /pid '(\d+)'/
|
|
263
|
+
@server_pid = Regexp.last_match(1).to_i
|
|
264
|
+
else
|
|
265
|
+
fail!("Could not detect jekyll serve pid from output: #{output}")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
Timeout.timeout(30) do
|
|
269
|
+
loop do
|
|
270
|
+
response = http_get(port, ContentDetailPaths::POST_SHELL)
|
|
271
|
+
break if response.code.to_i == 200
|
|
272
|
+
|
|
273
|
+
sleep 0.25
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
rescue Timeout::Error
|
|
277
|
+
fail!("Timed out waiting for jekyll serve on port #{port}")
|
|
278
|
+
ensure
|
|
279
|
+
FileUtils.rm_f(log.path)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def stop_server!
|
|
283
|
+
return unless @server_pid
|
|
284
|
+
|
|
285
|
+
Process.kill("TERM", @server_pid)
|
|
286
|
+
rescue Errno::ESRCH
|
|
287
|
+
nil
|
|
288
|
+
ensure
|
|
289
|
+
@server_pid = nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def verify_all_detail_urls!(manifest, port)
|
|
293
|
+
failures = []
|
|
294
|
+
|
|
295
|
+
{
|
|
296
|
+
"post" => manifest.dig("files", "posts") || [],
|
|
297
|
+
"article" => manifest.dig("files", "articles") || [],
|
|
298
|
+
"product" => manifest.dig("files", "products") || [],
|
|
299
|
+
"project" => manifest.dig("files", "projects") || []
|
|
300
|
+
}.each do |type, files|
|
|
301
|
+
files.each do |relative_path|
|
|
302
|
+
slug = slug_from_manifest_path(relative_path)
|
|
303
|
+
begin
|
|
304
|
+
verify_runtime_detail!(port, type, slug)
|
|
305
|
+
puts " OK #{detail_path(type, slug)}"
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
failures << "#{detail_path(type, slug)}: #{e.message}"
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
return if failures.empty?
|
|
313
|
+
|
|
314
|
+
fail!(<<~MSG.strip)
|
|
315
|
+
Detail URL gate failed for #{failures.length} published item(s):
|
|
316
|
+
- #{failures.join("\n - ")}
|
|
317
|
+
MSG
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
puts "Testing content detail URLs (storage export + shell poster)..."
|
|
321
|
+
|
|
322
|
+
@synthetic_created = false
|
|
323
|
+
port = free_port
|
|
324
|
+
@server_pid = nil
|
|
325
|
+
manifest = nil
|
|
326
|
+
|
|
327
|
+
begin
|
|
328
|
+
validate_redirects!
|
|
329
|
+
prepare_synthetic_post!
|
|
330
|
+
export_content!
|
|
331
|
+
manifest = load_manifest
|
|
332
|
+
verify_export_contract!(manifest)
|
|
333
|
+
build_shell!
|
|
334
|
+
verify_shell_structure!
|
|
335
|
+
start_server!(port)
|
|
336
|
+
verify_all_detail_urls!(manifest, port)
|
|
337
|
+
ensure
|
|
338
|
+
stop_server!
|
|
339
|
+
cleanup_synthetic_post!
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
post_count = manifest&.dig("counts", "posts") || 0
|
|
343
|
+
article_count = manifest&.dig("counts", "articles") || 0
|
|
344
|
+
product_count = manifest&.dig("counts", "products") || 0
|
|
345
|
+
project_count = manifest&.dig("counts", "projects") || 0
|
|
346
|
+
puts "All content detail URL tests passed (#{post_count} posts, #{article_count} articles, #{product_count} products, #{project_count} projects, synthetic gate included)."
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "set"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
|
|
8
|
+
require_relative "../../_plugins/content_keys"
|
|
9
|
+
|
|
10
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
11
|
+
|
|
12
|
+
def check!(condition, message)
|
|
13
|
+
raise message unless condition
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
puts "Testing ContentKeys..."
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
ContentKeys.validate!(REPO_ROOT)
|
|
20
|
+
puts " key uniqueness: OK"
|
|
21
|
+
rescue ContentKeys::ValidationError => e
|
|
22
|
+
raise "ContentKeys validation failed: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
entries = ContentKeys.published_entries(REPO_ROOT)
|
|
26
|
+
check! !entries.empty?, "expected published keyed content entries"
|
|
27
|
+
|
|
28
|
+
entries.each do |entry|
|
|
29
|
+
check! !entry["key"].empty?, "missing key on #{entry['repo_relative_path']}"
|
|
30
|
+
check! !entry["path_segment"].to_s.empty?, "missing path segment on #{entry['repo_relative_path']}"
|
|
31
|
+
check! entry["detail_path"].start_with?("/"), "invalid detail path for #{entry['repo_relative_path']}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
rules = ContentKeys.redirect_rules(entries)
|
|
35
|
+
rules.each do |source, target, code|
|
|
36
|
+
check! source.start_with?("/"), "redirect source must be absolute: #{source}"
|
|
37
|
+
check! target.start_with?("/"), "redirect target must be absolute: #{target}"
|
|
38
|
+
check! code == 301, "redirects must be permanent"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
expected_aliases = {
|
|
42
|
+
"ai-agents-sdd" => %w[ai-agents-sdd],
|
|
43
|
+
"vibe-coding" => %w[2026-03-24-where-to-do-vibe-coding 2026-03-24-when-to-vibe-code],
|
|
44
|
+
"wsl" => %w[20260224-wsl wsl],
|
|
45
|
+
"stitch-and-claude" => %w[stitch-and-claude]
|
|
46
|
+
}
|
|
47
|
+
expected_aliases.each do |key, slugs|
|
|
48
|
+
entry = entries.find { |item| item["key"] == key }
|
|
49
|
+
check! !entry.nil?, "missing published entry for #{key}"
|
|
50
|
+
aliases = ContentKeys.alias_slugs(entry)
|
|
51
|
+
slugs.each do |slug|
|
|
52
|
+
check! aliases.include?(slug), "#{key} aliases must include #{slug} (got #{aliases.inspect})"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Studio once rewrote includes as key="" when fromBlock destructured the RegExp
|
|
57
|
+
# match wrong. Reject empty xcite targets in published markdown.
|
|
58
|
+
xcite_empty = /\{%-?\s*include\s+xcite\.html\s+key=["']\s*["'][^%]*-?%\}/
|
|
59
|
+
known_keys = entries.map { |item| item["key"] }.to_set
|
|
60
|
+
xcite_re = /\{%-?\s*include\s+xcite\.html\s+key=["']([^"']+)["'][^%]*-?%\}/
|
|
61
|
+
Pathname.glob(REPO_ROOT.join("content/collections/*/*.md")).each do |path|
|
|
62
|
+
path.read.each_line.with_index(1) do |line, lineno|
|
|
63
|
+
if line.match?(xcite_empty)
|
|
64
|
+
raise "empty xcite key in #{path.relative_path_from(REPO_ROOT)}:#{lineno}"
|
|
65
|
+
end
|
|
66
|
+
line.scan(xcite_re).flatten.each do |target|
|
|
67
|
+
next if known_keys.include?(target)
|
|
68
|
+
|
|
69
|
+
raise "unknown xcite key #{target.inspect} in #{path.relative_path_from(REPO_ROOT)}:#{lineno}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
puts " xcite includes: OK"
|
|
74
|
+
|
|
75
|
+
redirects_path = REPO_ROOT.join("_redirects")
|
|
76
|
+
text = redirects_path.read
|
|
77
|
+
check! ContentKeys.generated_redirects_precede_catchalls?(text),
|
|
78
|
+
"generated content-key 301s must sit above /articles/:slug, /posts/:slug, and /blog/:slug 200 rewrites"
|
|
79
|
+
|
|
80
|
+
%w[
|
|
81
|
+
/articles/20260224-wsl
|
|
82
|
+
/articles/2026-03-20-stitch-e-claude
|
|
83
|
+
/articles/2026-03-24-where-to-do-vibe-coding
|
|
84
|
+
].each do |source|
|
|
85
|
+
check! text.include?("#{source} "), "expected 301 source #{source} in _redirects"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Dir.mktmpdir("content-keys-redirects-") do |tmp|
|
|
89
|
+
stale = Pathname.new(tmp).join("_redirects")
|
|
90
|
+
stale.write(<<~REDIRECTS)
|
|
91
|
+
/media/* https://example.test/media/:splat 302
|
|
92
|
+
|
|
93
|
+
/articles/:slug /pages/articles/ 200
|
|
94
|
+
/posts/:slug /pages/blog/ 200
|
|
95
|
+
/blog/:slug /pages/blog/ 200
|
|
96
|
+
|
|
97
|
+
#{ContentKeys::REDIRECTS_BEGIN}
|
|
98
|
+
/articles/stale-alias /articles/canonical/ 301
|
|
99
|
+
#{ContentKeys::REDIRECTS_END}
|
|
100
|
+
REDIRECTS
|
|
101
|
+
|
|
102
|
+
ContentKeys.sync_redirects_file!(REPO_ROOT, redirects_path: stale)
|
|
103
|
+
synced = stale.read
|
|
104
|
+
check! ContentKeys.generated_redirects_precede_catchalls?(synced),
|
|
105
|
+
"sync_redirects_file! must move generated 301s above SPA catch-all 200s"
|
|
106
|
+
check! synced.include?("/articles/20260224-wsl /articles/2026-02-24-wsl/ 301"),
|
|
107
|
+
"sync must emit content key 301 redirects"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
puts "All ContentKeys tests passed (#{entries.length} entries, #{rules.length} redirect rule(s))."
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/content_license"
|
|
8
|
+
|
|
9
|
+
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
10
|
+
EXPORT_ROOT = REPO_ROOT.join("_content_json")
|
|
11
|
+
|
|
12
|
+
def check!(condition, message)
|
|
13
|
+
raise message unless condition
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
puts "Testing content license export..."
|
|
17
|
+
|
|
18
|
+
config_license = ContentLicense.load(REPO_ROOT)
|
|
19
|
+
check!(config_license["id"] == "CC-BY-4.0", "content_license.id must be CC-BY-4.0 in _config.yml")
|
|
20
|
+
|
|
21
|
+
run_export = !EXPORT_ROOT.join("manifest.json").file?
|
|
22
|
+
if run_export
|
|
23
|
+
system("ruby", REPO_ROOT.join("scripts/content/export-content-json.rb").to_s, "content", "_content_json") || exit(1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
manifest_path = EXPORT_ROOT.join("manifest.json")
|
|
27
|
+
check!(manifest_path.file?, "manifest.json missing after export")
|
|
28
|
+
|
|
29
|
+
manifest = JSON.parse(manifest_path.read)
|
|
30
|
+
check!(manifest["content_license"].is_a?(Hash), "manifest.json missing content_license")
|
|
31
|
+
check!(manifest["content_license"]["id"] == "CC-BY-4.0", "manifest content_license.id must be CC-BY-4.0")
|
|
32
|
+
check!(manifest["content_license"]["url"].to_s.include?("creativecommons.org"), "manifest content_license.url must point to Creative Commons")
|
|
33
|
+
|
|
34
|
+
%w[articles posts].each do |collection|
|
|
35
|
+
sample = manifest.dig("files", collection, 0)
|
|
36
|
+
check!(sample, "manifest.files.#{collection} is empty")
|
|
37
|
+
|
|
38
|
+
json_path = EXPORT_ROOT.join(sample)
|
|
39
|
+
payload = JSON.parse(json_path.read)
|
|
40
|
+
check!(payload["license"].is_a?(Hash), "#{sample} missing license object")
|
|
41
|
+
check!(payload["license"]["id"] == "CC-BY-4.0", "#{sample} license.id must be CC-BY-4.0")
|
|
42
|
+
check!(payload["copyright"].to_s.include?("©"), "#{sample} missing copyright notice")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
puts "All content license tests passed."
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
9
|
+
FIXTURE_DIR = Pathname.new(__dir__).join("render-parity")
|
|
10
|
+
RUNNER = FIXTURE_DIR.join("run-fixture.mjs")
|
|
11
|
+
|
|
12
|
+
# The shared ledger blocks (43-blocks.html) render listings in the browser, so
|
|
13
|
+
# a Jekyll build never executes them and no Ruby gate can see their output.
|
|
14
|
+
# Their fixtures call the real runtime through load-runtime.mjs.
|
|
15
|
+
BLOCK_DIR = FIXTURE_DIR.join("blocks")
|
|
16
|
+
BLOCK_RUNNER = FIXTURE_DIR.join("run-block-fixture.mjs")
|
|
17
|
+
|
|
18
|
+
def check!(condition, message = "condition was false")
|
|
19
|
+
raise "Assertion failed: #{message}" unless condition
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_fixtures!(runner, paths, label)
|
|
23
|
+
check! paths.any?, "no #{label} fixtures found"
|
|
24
|
+
paths.each do |fixture_path|
|
|
25
|
+
stdout, stderr, status = Open3.capture3(
|
|
26
|
+
"node", runner.to_s, fixture_path, chdir: ROOT.join("scripts").to_s
|
|
27
|
+
)
|
|
28
|
+
check! status.success?, "#{File.basename(fixture_path)} failed:\n#{stderr}\n#{stdout}"
|
|
29
|
+
puts " #{stdout.strip}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
puts "Testing content render parity..."
|
|
34
|
+
|
|
35
|
+
run_fixtures!(RUNNER, Dir.glob(FIXTURE_DIR.join("*.json")).sort, "markdown parity")
|
|
36
|
+
run_fixtures!(BLOCK_RUNNER, Dir.glob(BLOCK_DIR.join("*.json")).sort, "ledger block")
|
|
37
|
+
|
|
38
|
+
puts "All content render parity tests passed."
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
require_relative "../../_plugins/lib/content_validators"
|
|
7
|
+
|
|
8
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
9
|
+
|
|
10
|
+
puts "Testing ContentValidators..."
|
|
11
|
+
ContentValidators.validate_all!(ROOT)
|
|
12
|
+
puts "All ContentValidators checks passed."
|