Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/architecture/current-system.md

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions docs/architecture/hugo-guide-catalogue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Hugo guide catalogue contract

The Hugo catalogue is derived from the current Hugo site, its pages and page resources. It is not loaded from the PowerShell discovery or assessment files. PowerShell's source assessment remains a separate build responsibility; see [current system](current-system.md).

## Ownership and naming

Capability partials live directly below `layouts/_partials/openguide/{guides,editions,translations,pdfs,history}/`. The folder names describe guide publishing capabilities. There is no nested `functions/` folder. HTML presentation stays under `components/`; the existing `functions/get-...` entry points remain compatibility adapters.

The paths in the following table are relative to [`openguide/`](../../system/OpenGuidePlatform.Hugo.Guides/layouts/_partials/openguide/). Inputs are Hugo template values, not JSON.

| Partial | Input | Result and responsibility |
|---|---|---|
| `guides/discover-guides.html` | Page | Current site's sections of type `guide`, deduplicated by relative permalink and ordered by weight. |
| `guides/get-guide-catalogue.html` | Page | `SiteLanguage`, `Guides[]`; aggregates editions, translations and PDF resources. |
| `editions/discover-guide-editions.html` | Dict: `Page`, `Scope`, optional `Order` | `Version`, `Page` records in descending date order, or original page order with `Order: source`. `children` uses the page's children; `section` uses the site's regular pages in the page's section. Both filter type `guide` and require a version in the URL. |
| `editions/get-edition-version.html` | Page | First permalink segment matching `YYYY.number`, or an empty string. |
| `editions/select-latest-guide-edition.html` | Page | `Version`, `Page`; date-first selection with the existing lexical URL fallback. |
| `editions/get-guide-version-options.html` | Page | Existing `version`, `url`, `title`, `date`, `page`, `isLatest` records. Latest links to the section root. |
| `translations/discover-edition-translations.html` | Edition Page | Union of `AllTranslations` languages and language suffixes found in edition PDFs. Each record retains pages and resources. |
| `translations/select-preferred-guide-translations.html` | Guide section Page | Public translation rows with `Version`. Orders editions numerically by year/month; an older readable translation takes precedence over a newer PDF-only translation. Preserves the legacy per-edition override hook. |
| `translations/project-legacy-translation.html` | Discovered translation record | The existing public translation fields, including availability and status. This operation reads `.Plain`. |
| `translations/resolve-translation-fallback.html` | Page | Same-path page in the first Hugo language site, preserving the existing fallback lookup. |
| `pdfs/discover-edition-pdfs.html` | Dict: `Page`, optional `Pattern` | All matching `Name`, `Language`, `Resource` records. Default pattern: `pdf/*.*.pdf`. Language is the penultimate dot-separated filename component. |
| `pdfs/select-translation-pdfs.html` | Dict: `PDFs`, `Language` | Case-insensitive language filter over discovered records, preserving order. |
| `pdfs/select-first-edition-pdf.html` | Dict: `Page`, `Pattern` | First matching resource, or `false`; preserves the rendering templates' previous `GetMatch` selection. |
| `history/get-guide-history-chain.html` | Page | Existing `current`, `history`, `forkSource`, `forkHistory` structure containing Hugo pages; preserves explicit fork edition and fallback behavior. |

## Internal structure

`get-guide-catalogue` returns this **in-memory shape**. The names below illustrate a Kanban guide; they are not configuration or a maintained inventory. All entries are discovered.

```text
SiteLanguage: "en"
Guides:
- Id: "the-kanban-guide" # Hugo section identifier
Page: <Hugo section Page>
Title: "The Kanban Guide"
LatestVersion: "2025.5"
Versions:
- Version: "2025.5"
Page: <Hugo edition Page>
Translations:
- Language: "en"
Page: <Hugo translated Page, or false if absent>
EditionPage: <calling edition Page>
PDFs:
- Name: "pdf/kanban-guide.en.pdf"
Language: "en"
Resource: <Hugo Resource>
LegacyPDF: <selected Resource, or false>
```

An empty translated page remains a page. A PDF-only language without a page has `Page: false`; no translated page or URL is invented. Multiple matching PDFs remain in `PDFs`. `LegacyPDF` preserves the old single-download choice: last matching edition resource, or first matching translated-page resource when the edition has no match. Resource enumeration follows Hugo's `Resources.Match` order.

Raw discovery and catalogue aggregation do not read `.Content` or `.Plain`. They can be used from rendering without eagerly rendering every translation. Availability projection and preferred-translation selection do read `.Plain` and should not be invoked recursively from a shortcode that is itself being evaluated for that projection. No global cache is introduced; each call retains its language-site context.

## Public compatibility

[`functions/get-guide-translations-catalogue.html`](../../system/OpenGuidePlatform.Hugo.Guides/layouts/_partials/functions/get-guide-translations-catalogue.html) explicitly projects the internal catalogue into the existing JSON-compatible shape. Hugo Page and Resource objects must not be passed directly to `jsonify`. `index.translations.json` continues to use this adapter.

Public guide records retain `Title`, `Path`, `RelPermalink`, `Section`, `Type`, `Weight`, `Description`, `Versions`, `LatestVersion`. Edition records retain `Version`, `Title`, `Date`, `RelPermalink`, `Path`, `Description`, `Latest`, `Translations`.

Translation records retain exactly `Date`, `Language`, `LanguageName`, `Path`, `PathPdf`, `ReadOnline`, `ReadOnlineStub`, `ReadPDF`, `RelPermalink`, `Status`, `Title`, `VersionPath`, `Weight`. The preferred-language list additionally includes `Version`. `ReadOnlineStub` means a page exists, not that it has readable content. `ReadOnline` preserves the existing greater-than-ten-space-separated-words rule. Status remains `published`, `online-only`, `pdf-only` or `site-only`.

The rendering components retain their separate greater-than-500-character content rule and production draft checks. Their PDF patterns and first-match policy remain unchanged. Consolidating these differing availability rules would change behavior and is outside this refactor.

The six existing entry points for catalogue, edition translations, preferred translations, version options, latest version and history remain callable. The catalogue adapter and preferred selection continue calling the legacy per-edition translation hook. Catalogue projection and version options retain the legacy latest-version hook; history retains the legacy version-options hook. Their default implementations delegate to capability partials without recursion. These deliberate extension points preserve existing consumer overrides; raw catalogue discovery still describes the Hugo objects themselves. Consumer adoption must check any additional site-specific overrides.

## Verification

[`HugoCatalogue.Tests.ps1`](../../tests/Core/HugoCatalogue.Tests.ps1) builds multilingual fixtures with several guides, dated editions, empty translations, PDF-only languages, regional language tags, multiple PDFs, fork ancestry, equal-date ordering, consumer overrides and production language exclusion. Existing rendering and SEO tests cover their public output. Acceptance also requires the root platform build and both reference-site targets described in [platform development](../platform-development.md).

These checks establish local platform behavior. They do not establish consumer adoption or deployment approval.
4 changes: 3 additions & 1 deletion docs/platform-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Install the tools listed in the README and Node.js 20 or newer and npm (for real
./build.ps1 -Version 0.0.0-local
```

Without a version override, the platform calculates GitVersion using the same module operation as Actions. The repository currently uses GitVersion 5 configuration, so Dependencies installs a compatible 5.x tool beneath `.processing/tools/` without changing a global GitVersion installation. `-Version` remains an explicit override.
Without a version override, the platform calculates GitVersion using the same module operation as Actions. The repository uses GitVersion 6 configuration; Dependencies installs a compatible 6.x tool beneath `.processing/tools/` without changing a global GitVersion installation. `-Version` remains an explicit override.

The platform build runs preparation, tests, packaging and package verification. It writes to a fresh directory under `.processing/platform/`, then builds and validates the sample in preview and production from the exact package ZIP it produced. It does not publish a release or deploy the sample by default; the opt-in commands below add those operations. Pester is a platform-development dependency, not an everyday guide-site requirement.

Expand All @@ -34,6 +34,8 @@ The built-in hosting adapter follows the [Azure Static Web Apps CLI deployment c

## Build module ownership

For the current component boundaries, source/discovery behavior, file formats and build evidence, see [Current system](architecture/current-system.md).

`OpenGuidePlatform.PowerShell.PlatformBuild` owns platform engineering. `OpenGuidePlatform.PowerShell.GuideSiteBuild` owns guide-site stages and remains independently importable. They ship as separate GuideSite and PlatformBuild ZIP assets in one coordinated release. Root scripts dispatch to the selected module; existing `.build/` build/test/package entry points forward to PlatformBuild.

Both root build entry points accept `-PlatformSource Local|Preview|Production|Path`, `-PlatformRelease <tag>` and `-PlatformPath <directory-or-zip>`. Platform checkouts default to their local module. Installed consumers default to their installation lock. An explicit override does not change that lock. Preview/Production without a tag select the latest eligible release once at startup; Production means a non-prerelease platform package, independently of the site's `-Target`.
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ If you need help, include the command, finding and relevant report in a [GitHub
- [First-time site setup](docs/using/first-adoption.md) — policy, existing files and deployment setup.
- [SEO metadata](docs/using/seo-metadata.md) — homepage titles, social images, publisher logos, authors and publication licences.
- [Platform development](docs/platform-development.md) — build this repository, run the sample locally and understand releases.
- [Current system](docs/architecture/current-system.md) — component ownership, guide discovery, contracts, file formats and build evidence.
- [Workflow dependency locking](docs/platform-development.md#workflow-dependency-lockfile) — regenerate, verify and review Actions dependency locks when changing platform workflows.
- [Execution plan and current progress](docs/architecture/open-guide-platform-execution-plan.md).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ <h4 class="h6 text-muted mb-3 flex-shrink-0 d-none d-md-block">{{ i18n "guide_ta
{{ .Page.TableOfContents }}
{{ else }}
{{- /* Get the default guide content as fallback */ -}}
{{ $defaultSite := index hugo.Sites 0 }}
{{ $thisPageDefault := $defaultSite.GetPage .Path }}
{{ $thisPageDefault := partial "openguide/translations/resolve-translation-fallback.html" . }}

{{- if not $thisPageDefault }}
{{- fmt.Errorf "thisPageDefault page not found" }}
Expand Down Expand Up @@ -105,7 +104,7 @@ <h1 class="content-title mb-0">{{ .Title }}</h1>
{{- if eq .Site.Language.Lang "en" }}
{{- /* Show both EN and EN-US buttons for English */ -}}
{{- $pdfPatternEN := "pdf/*.en.pdf" }}
{{- $pdfResourceEN := .Resources.GetMatch $pdfPatternEN }}
{{- $pdfResourceEN := partial "openguide/pdfs/select-first-edition-pdf.html" (dict "Page" . "Pattern" $pdfPatternEN) }}
{{- if $pdfResourceEN }}
<a
href="{{ $pdfResourceEN.RelPermalink }}"
Expand All @@ -128,7 +127,7 @@ <h1 class="content-title mb-0">{{ .Title }}</h1>
{{- end }}

{{- $pdfPatternENUS := "pdf/*.en-us.pdf" }}
{{- $pdfResourceENUS := .Resources.GetMatch $pdfPatternENUS }}
{{- $pdfResourceENUS := partial "openguide/pdfs/select-first-edition-pdf.html" (dict "Page" . "Pattern" $pdfPatternENUS) }}
{{- if $pdfResourceENUS }}
<a
href="{{ $pdfResourceENUS.RelPermalink }}"
Expand All @@ -152,7 +151,7 @@ <h1 class="content-title mb-0">{{ .Title }}</h1>
{{- else }}
{{- /* Single PDF button for other languages */ -}}
{{- $pdfPattern := printf "pdf/*.%s.pdf" .Site.Language.Lang }}
{{- $pdfResource := .Resources.GetMatch $pdfPattern }}
{{- $pdfResource := partial "openguide/pdfs/select-first-edition-pdf.html" (dict "Page" . "Pattern" $pdfPattern) }}
{{- if $pdfResource }}
<a
href="{{ $pdfResource.RelPermalink }}"
Expand Down Expand Up @@ -201,9 +200,9 @@ <h1 class="content-title mb-0">{{ .Title }}</h1>
{{- /* PDF intro text with Google Analytics tracking */ -}}
{{- if eq .Site.Language.Lang "en" }}
{{- $pdfPatternEN := "pdf/*.en.pdf" }}
{{- $pdfResourceEN := .Resources.GetMatch $pdfPatternEN }}
{{- $pdfResourceEN := partial "openguide/pdfs/select-first-edition-pdf.html" (dict "Page" . "Pattern" $pdfPatternEN) }}
{{- $pdfPatternENUS := "pdf/*.en-us.pdf" }}
{{- $pdfResourceENUS := .Resources.GetMatch $pdfPatternENUS }}
{{- $pdfResourceENUS := partial "openguide/pdfs/select-first-edition-pdf.html" (dict "Page" . "Pattern" $pdfPatternENUS) }}
{{- if or $pdfResourceEN $pdfResourceENUS }}
<div class="alert alert-info mb-4" role="alert">
<i class="fa-solid fa-info-circle me-2"></i>
Expand All @@ -219,7 +218,7 @@ <h1 class="content-title mb-0">{{ .Title }}</h1>
{{- end }}
{{- else }}
{{- $pdfPattern := printf "*.%s.pdf" .Site.Language.Lang }}
{{- $pdfResource := .Resources.GetMatch $pdfPattern }}
{{- $pdfResource := partial "openguide/pdfs/select-first-edition-pdf.html" (dict "Page" . "Pattern" $pdfPattern) }}
{{- if $pdfResource }}
<div class="alert alert-info mb-4" role="alert">
<i class="fa-solid fa-info-circle me-2"></i>
Expand Down Expand Up @@ -249,8 +248,7 @@ <h1 class="content-title mb-0">{{ .Title }}</h1>
{{ .Content }}
{{- else }}
{{- /* Get the default guide content as fallback */ -}}
{{ $defaultSite := index hugo.Sites 0 }}
{{ $thisPageDefault := $defaultSite.GetPage .Path }}
{{ $thisPageDefault := partial "openguide/translations/resolve-translation-fallback.html" . }}

{{- if not $thisPageDefault }}
{{- fmt.Errorf "thisPageDefault page not found" }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
{{- $foundingContributors = where $foundingContributors "role" "in" (slice "contributor" "creator") -}}

{{ $pdfPattern := printf "pdf/*.%s*.pdf" $defaultSite.Language }}
{{ $pdfMatches := $guidePage.Resources.Match $pdfPattern }}
{{ $pdfMatches := slice }}{{ range partial "openguide/pdfs/discover-edition-pdfs.html" (dict "Page" $guidePage "Pattern" $pdfPattern) }}{{ $pdfMatches = $pdfMatches | append .Resource }}{{ end }}


<h2 class="mb-3">{{ i18n "download_official_version" . }}</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ <h5 class="card-title mb-0 {{ if $isLatest }}text-primary{{ else if $isFork }}te
</div>
{{ end }}
{{- $pdfPattern := "pdf/*.*.pdf" }}
{{- $pdfResources := .Resources.Match $pdfPattern }}
{{- $pdfResources := partial "openguide/pdfs/discover-edition-pdfs.html" (dict "Page" . "Pattern" $pdfPattern) }}
{{ if gt (len $pdfResources) 0 }}
<div class="mb-3">
<div class="text-muted small mb-2">
Expand All @@ -156,20 +156,14 @@ <h5 class="card-title mb-0 {{ if $isLatest }}text-primary{{ else if $isFork }}te
<div class="d-flex flex-wrap gap-1">
{{/* PDF language buttons */}}
{{ range $pdfResources }}
{{- $filename := .Name }}
{{- $langCode := "" }}
{{- $langCode := .Language }}
{{- $displayName := "" }}
{{/* Extract language code from filename like "guide.ja.pdf" */}}
{{- $parts := split $filename "." }}
{{- if ge (len $parts) 3 }}
{{- $langCode = index $parts (sub (len $parts) 2) }}
{{ end }}
{{/* Skip if no language code found */}}
{{- if eq $langCode "" }}
{{- continue }}
{{ end }}
{{ $displayName = partial "functions/get-language-display-name.html" $langCode }}
<a href="{{ .Permalink }}" class="btn btn-outline-secondary btn-sm" title="Download {{ $displayName }} PDF">
<a href="{{ .Resource.Permalink }}" class="btn btn-outline-secondary btn-sm" title="Download {{ $displayName }} PDF">
<i class="fas fa-file-pdf me-1"></i>
{{ $displayName }}
</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1 @@
{{/* Function: get-all-versions
Purpose: Get all available versions for a guide section, sorted by date (newest first)
Input: A guide page
Output: A slice of version objects with version string, URL, and title
*/}}

{{- $section := .Section -}}
{{- $versions := slice -}}

{{- /* Get all pages in the current section */ -}}
{{- $sectionPages := where .Site.RegularPages "Section" $section -}}

{{- /* Filter for guide pages */ -}}
{{- $guidePages := where $sectionPages "Type" "guide" -}}

{{- /* Get the latest version to determine section root URL */ -}}
{{- $latestVersion := partial "functions/get-latest-version" . -}}

{{- /* Get the section page URL for the latest version */ -}}
{{- $sectionPage := .Site.GetPage $section -}}
{{- $sectionUrl := "/" -}}
{{- if $sectionPage -}}
{{- $sectionUrl = $sectionPage.RelPermalink -}}
{{- else -}}
{{- $sectionUrl = printf "/%s/" $section -}}
{{- end -}}

{{- /* Extract versions from all guide pages */ -}}
{{- range $guidePages -}}
{{- $pathParts := split .RelPermalink "/" -}}
{{- $pageVersion := "" -}}
{{- range $pathParts -}}
{{- if and (ne . "") (findRE `^\d{4}\.\d+$` .) -}}
{{- $pageVersion = . -}}
{{- break -}}
{{- end -}}
{{- end -}}

{{- if $pageVersion -}}
{{- /* Use section root URL for latest version, specific version URL for others */ -}}
{{- $versionUrl := .RelPermalink -}}
{{- if eq $pageVersion $latestVersion -}}
{{- $versionUrl = $sectionUrl -}}
{{- end -}}

{{- $versionObj := dict
"version" $pageVersion
"url" $versionUrl
"title" .Title
"date" .Date
"page" .
"isLatest" (eq $pageVersion $latestVersion)
-}}
{{- $versions = $versions | append $versionObj -}}
{{- end -}}
{{- end -}}

{{- /* Sort versions by date (newest first) */ -}}
{{- $sortedVersions := sort $versions "date" "desc" -}}

{{- return $sortedVersions -}}
{{- return (partial "openguide/editions/get-guide-version-options.html" .) -}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Defer the Hugo refactor until consumers are verified

With consumer adoption still outstanding—as this commit itself records in docs/architecture/current-system.md:91—replacing this legacy implementation with a new openguide/ capability crosses the repository’s explicit gate against internal Hugo refactoring before all consumers have adopted and been verified. Keep the existing internals unchanged until that verification is complete, or include concrete verification for every consumer before landing this refactor.

AGENTS.md reference: AGENTS.md:L12-L12

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Martin explicitly approved this bounded catalogue refactor in the task, with preservation of existing behavior and compatibility hooks. That authorizes this work despite the earlier deferral; it does not waive the broader rollout gates. Consumer evidence: KanbanGuides preview and production passed with local OGP; The SAFe Delusion production passed and preview passed after revalidating the unchanged artifact following a browser timeout. ScrumGuide-ExpansionPack preview and production were tested against both the candidate and its installed v1.0.1: each has the same six FORBIDDEN_RESOURCE_PRESENT findings for existing latest/ routes (Code/Path/Message compared). This PR does not claim that consumer is fully verified or that all rollout gates have passed. No consumer tracked files were changed.

Loading
Loading