129 lines
7.0 KiB
Go
129 lines
7.0 KiB
Go
package site
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// PlatformGuide is one `platforms/<name>.yml` file: configuration and public
|
|
// copy at the same time. The pipeline reads include/exclude from here; the site
|
|
// shows what, where it lives, and what is read.
|
|
type PlatformGuide struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
What string `yaml:"what" json:"what"`
|
|
Fits []string `yaml:"fits" json:"fits"`
|
|
Base string `yaml:"base" json:"base"`
|
|
Read []string `yaml:"read" json:"read"`
|
|
Public bool `yaml:"public" json:"public"`
|
|
Include struct {
|
|
Repos any `yaml:"repos" json:"repos"`
|
|
Paths []string `yaml:"paths" json:"paths"`
|
|
} `yaml:"include" json:"include"`
|
|
Exclude struct {
|
|
Repos []string `yaml:"repos" json:"repos"`
|
|
Paths []string `yaml:"paths" json:"paths"`
|
|
} `yaml:"exclude" json:"exclude"`
|
|
}
|
|
|
|
// LoadPlatforms reads every guide. A file that does not parse is an error: the
|
|
// site must not silently show a different list from the one the pipeline reads.
|
|
func LoadPlatforms(root string) ([]PlatformGuide, error) {
|
|
dir := filepath.Join(root, "platforms")
|
|
entries, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []PlatformGuide
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !(strings.HasSuffix(entry.Name(), ".yml") || strings.HasSuffix(entry.Name(), ".yaml")) {
|
|
continue
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var guide PlatformGuide
|
|
if err := yaml.Unmarshal(raw, &guide); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", entry.Name(), err)
|
|
}
|
|
if guide.Name == "" || guide.What == "" {
|
|
return nil, fmt.Errorf("%s: name and what are required", entry.Name())
|
|
}
|
|
out = append(out, guide)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|
|
|
|
// RenderPlatforms writes /platforms/index.html and /platforms/index.json.
|
|
//
|
|
// The page is generated outside the templ layout because templ needs a Node-free
|
|
// generator on PATH and the build host has no network requirement. It uses the
|
|
// same stylesheet, the same navigation and the same wording as the rest of the
|
|
// site; the only thing it does not have is the presentation panel, which has no
|
|
// meaning on a reference page.
|
|
func RenderPlatforms(root, outDir string) (int, error) {
|
|
guides, err := LoadPlatforms(root)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
target := filepath.Join(outDir, "platforms")
|
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var body strings.Builder
|
|
body.WriteString(`<p class="lede">The assistant reads four sources. Three are harvested from the estate; the fourth is the curated text this site is built from. Everything below is the same configuration the pipeline runs.</p>`)
|
|
for _, guide := range guides {
|
|
state := "enabled"
|
|
if !guide.Enabled {
|
|
state = "disabled"
|
|
}
|
|
body.WriteString(`<section class="platform">`)
|
|
fmt.Fprintf(&body, `<h2>%s <span class="conf">%s</span></h2>`, template.HTMLEscapeString(guide.Name), state)
|
|
fmt.Fprintf(&body, `<p>%s</p>`, template.HTMLEscapeString(strings.TrimSpace(guide.What)))
|
|
fmt.Fprintf(&body, `<dl><dt>Where it lives</dt><dd>%s</dd>`, template.HTMLEscapeString(guide.Base))
|
|
fmt.Fprintf(&body, `<dt>Feeds</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Fits, ", ")))
|
|
fmt.Fprintf(&body, `<dt>Reads</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Read, ", ")))
|
|
if len(guide.Include.Paths) > 0 {
|
|
fmt.Fprintf(&body, `<dt>Includes</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Include.Paths, ", ")))
|
|
}
|
|
if len(guide.Exclude.Paths) > 0 {
|
|
fmt.Fprintf(&body, `<dt>Excludes</dt><dd>%s</dd>`, template.HTMLEscapeString(strings.Join(guide.Exclude.Paths, ", ")))
|
|
}
|
|
body.WriteString(`</dl></section>`)
|
|
}
|
|
|
|
page, err := template.New("platforms").Parse(platformsPage)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
file, err := os.Create(filepath.Join(target, "index.html"))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer file.Close()
|
|
if err := page.Execute(file, map[string]any{"Body": template.HTML(body.String())}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
raw, err := json.MarshalIndent(map[string]any{"version": 1, "platforms": guides}, "", " ")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(target, "index.json"), append(raw, '\n'), 0o644); err != nil {
|
|
return 0, err
|
|
}
|
|
return len(guides), nil
|
|
}
|
|
|
|
const platformsPage = `<!doctype html><html lang="en-AU"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Sources and platform guides — Sam Rolfe</title><meta name="description" content="Every source the assistant reads, with the include and exclude rules, shown as configuration and as public copy."><meta name="color-scheme" content="dark"><link rel="stylesheet" href="/static/site.css"></head><body><a class="skip-link" href="#main">Skip to content</a><header class="topbar"><a class="brand" href="/"><span class="brand-mark">SR</span> <span class="brand-text">Sam Rolfe <span class="brand-sub">AI · Platform · Edge</span></span></a><nav class="nav nav-rail" aria-label="Sections"><a href="/ai/" class="nav-link"><span class="nav-gutter" aria-hidden="true">AI</span> <span class="nav-title">AI & Agentic Systems</span></a><a href="/platform/" class="nav-link"><span class="nav-gutter" aria-hidden="true">PL</span> <span class="nav-title">Platform & Infrastructure</span></a><a href="/edge/" class="nav-link"><span class="nav-gutter" aria-hidden="true">ED</span> <span class="nav-title">Edge & Devices</span></a><a href="/products/" class="nav-link"><span class="nav-gutter" aria-hidden="true">PR</span> <span class="nav-title">Products</span></a><a href="/history/" class="nav-link"><span class="nav-gutter" aria-hidden="true">HI</span> <span class="nav-title">History</span></a><a href="/about/" class="nav-link"><span class="nav-gutter" aria-hidden="true">AB</span> <span class="nav-title">About</span></a><a href="/evidence/" class="nav-link"><span class="nav-gutter" aria-hidden="true">EV</span> <span class="nav-title">Evidence index</span></a></nav></header><div class="shell"><main id="main" class="main"><nav class="crumbs" aria-label="Breadcrumb"><ol><li><a href="/">Home</a></li><li><span aria-current="page">Sources</span></li></ol></nav><article class="doc"><header class="doc-head"><p class="doc-kicker">AI overlay</p><h1>What the assistant reads</h1><p class="doc-claim">Four sources, one pool. Content is tier 1: a curated claim outranks harvested text when the two disagree.</p></header>{{.Body}}<p class="doc-foot"><a href="/evidence/">Every artefact</a> · <a href="/search">Search the site</a></p></article></main></div><footer class="footer"><p class="footer-line">Twenty-five years shipping software · Melbourne · remote · hybrid · office · travel</p><p class="footer-links"><a href="/evidence/">Evidence index</a> <a href="/search">Search</a></p></footer></body></html>`
|