diff --git a/README.md b/README.md index ffb404b..dd5ec2b 100644 --- a/README.md +++ b/README.md @@ -44,13 +44,13 @@ Run the complete source, build, export, size, and route smoke gate: ./scripts/verify-production.sh ``` -The gate derives the exact GoSX and gotreesitter versions from `go.mod`, rejects local engine -overrides, builds the browser parser and all 206 lazy grammar assets from Go, validates every `.gsx` source, runs the Go tests, -creates a clean `gosx build --prod` artifact, runs `gosx export`, reports runtime size, boots -`dist/run.sh`, and requests every documentation route. Its Chrome check types a unique private -marker into the editor and proves that gotreesitter parses it without any source-bearing request; -it then proves an internal route change does not issue a document request. The `gosx` binary on -`PATH` must exactly match the version in `go.mod`. +The gate derives the exact GoSX and gotreesitter versions from `go.mod`. It rejects local engine +overrides. It builds the playground, all 206 lazy grammar assets, and the authoring engine from Go. +It validates every `.gsx` source and runs the Go tests. It creates a clean `gosx build --prod` +artifact, runs `gosx export`, reports runtime size, boots `dist/run.sh`, and requests every route. +It checks every internal link and validates the authoring asset surface. Its Chrome check proves +that playground source never enters a request. It also proves that internal navigation does not +request a new document. The `gosx` binary on `PATH` must exactly match the version in `go.mod`. Browser performance budgets are optional because they require a local Chrome installation: diff --git a/app/authoring/page.server.go b/app/authoring/page.server.go index b6124b1..f78baee 100644 --- a/app/authoring/page.server.go +++ b/app/authoring/page.server.go @@ -78,11 +78,11 @@ func init() { docsapp.RegisterStaticDocsPage( "Grammar Authoring", "Inherit a base grammar and author a delta of added/overridden rules in the browser — grammargen merges and compiles the extended grammar live.", + "/authoring", route.FileModuleOptions{ Load: loadAuthoring, Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) { return server.Metadata{ - Title: server.Title{Absolute: "Grammar Authoring — gotreesitter"}, Links: []server.LinkTag{{ Rel: "stylesheet", Href: docsapp.PublicAssetURL("authoring/authoring.css"), diff --git a/app/changelog/page.server.go b/app/changelog/page.server.go index 2941956..026fa1b 100644 --- a/app/changelog/page.server.go +++ b/app/changelog/page.server.go @@ -32,6 +32,7 @@ func init() { docsapp.RegisterStaticDocsPage( "Changelog", "Explore every GoTreeSitter release, current unreleased work, upgrade impact, and source evidence.", + "/changelog", route.FileModuleOptions{ Load: loadChangelog, Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) { @@ -45,10 +46,9 @@ func changelogMetadata() server.Metadata { const ( title = "Changelog — GoTreeSitter" description = "Explore GoTreeSitter releases, current work, upgrade impact, and source evidence." - siteURL = "https://gotreesitter.m31labs.dev" ) image := server.MediaAsset{ - URL: siteURL + docsapp.PublicAssetURL("social/changelog.png"), + URL: docsapp.SiteURL + docsapp.PublicAssetURL("social/changelog.png"), Width: 1200, Height: 630, Alt: "GoTreeSitter Changelog with an abstract syntax-tree release timeline.", @@ -57,11 +57,11 @@ func changelogMetadata() server.Metadata { return server.Metadata{ Title: server.Title{Absolute: title}, Description: description, - MetadataBase: siteURL, - Alternates: &server.Alternates{Canonical: siteURL + "/changelog"}, + MetadataBase: docsapp.SiteURL, + Alternates: &server.Alternates{Canonical: docsapp.SiteURL + "/changelog"}, OpenGraph: &server.OpenGraph{ Type: "website", - URL: siteURL + "/changelog", + URL: docsapp.SiteURL + "/changelog", SiteName: "GoTreeSitter", Title: title, Description: description, diff --git a/app/docs/__slug/page.server.go b/app/docs/__slug/page.server.go index 578210f..b382e68 100644 --- a/app/docs/__slug/page.server.go +++ b/app/docs/__slug/page.server.go @@ -62,16 +62,18 @@ func init() { "title": title, "description": doc.Frontmatter["description"], "content": node, + "slug": slug, }, nil }, Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) { values, _ := data.(map[string]any) title, _ := values["title"].(string) description, _ := values["description"].(string) - return server.Metadata{ - Title: server.Title{Default: title}, - Description: description, - }, nil + slug, _ := values["slug"].(string) + meta := docsapp.CanonicalMetadata("/docs/" + slug) + meta.Title = server.Title{Default: title + " | GoTreeSitter Docs"} + meta.Description = description + return meta, nil }, }, ) diff --git a/app/layout.gsx b/app/layout.gsx index 405c778..41851b2 100644 --- a/app/layout.gsx +++ b/app/layout.gsx @@ -12,7 +12,7 @@ func Layout() Node {
- + gotreesitter - {gtsVersion} + - + {gtsVersion} · 206/206 curated parity - Changelog - Playground - Authoring + + + } +// TopNavLink exposes the active tool route to sighted and assistive users. +func TopNavLink(props any) Node { + return <> + + + {props.Label} + + + + {props.Label} + + +} + // DocsNavLink renders one sidebar ` @@ -67,7 +81,7 @@ func DocsNavLink(props any) Node { diff --git a/app/modules.go b/app/modules.go index 02b0fdd..1281853 100644 --- a/app/modules.go +++ b/app/modules.go @@ -2,11 +2,14 @@ package docs import ( "log" + "strings" "m31labs.dev/gosx/route" "m31labs.dev/gosx/server" ) +const SiteURL = "https://gotreesitter.m31labs.dev" + var docsPublicAssetURL func(string) string func BindPublicAssetURL(fn func(string) string) { @@ -20,6 +23,14 @@ func PublicAssetURL(path string) string { return server.AssetURL(path) } +func CanonicalMetadata(path string) server.Metadata { + path = "/" + strings.TrimLeft(strings.TrimSpace(path), "/") + return server.Metadata{ + MetadataBase: SiteURL, + Alternates: &server.Alternates{Canonical: SiteURL + path}, + } +} + func RegisterDocsPage(title, description string, opts route.FileModuleOptions) { metadata := opts.Metadata opts.Metadata = func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) { @@ -41,13 +52,13 @@ func RegisterDocsPage(title, description string, opts route.FileModuleOptions) { } } -func RegisterStaticDocsPage(title, description string, opts route.FileModuleOptions) { +func RegisterStaticDocsPage(title, description, canonicalPath string, opts route.FileModuleOptions) { metaMetadata := opts.Metadata opts.Metadata = func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) { - meta := server.Metadata{ + meta := mergeDocsMetadata(server.Metadata{ Title: server.Title{Default: title + " | GoTreeSitter Docs"}, Description: description, - } + }, CanonicalMetadata(canonicalPath)) if metaMetadata == nil { return meta, nil } diff --git a/app/modules_test.go b/app/modules_test.go index 8a28664..3c47aaf 100644 --- a/app/modules_test.go +++ b/app/modules_test.go @@ -30,3 +30,15 @@ func TestMergeDocsMetadataPreservesStructuredSocialMetadata(t *testing.T) { t.Fatalf("base metadata changed unexpectedly: %#v", merged) } } + +func TestCanonicalMetadataUsesProductionOrigin(t *testing.T) { + for _, path := range []string{"changelog", "/changelog"} { + meta := CanonicalMetadata(path) + if meta.MetadataBase != SiteURL { + t.Fatalf("metadata base = %q, want %q", meta.MetadataBase, SiteURL) + } + if meta.Alternates == nil || meta.Alternates.Canonical != SiteURL+"/changelog" { + t.Fatalf("canonical metadata for %q = %#v", path, meta.Alternates) + } + } +} diff --git a/app/page.server.go b/app/page.server.go index 95527d3..95f59f2 100644 --- a/app/page.server.go +++ b/app/page.server.go @@ -9,6 +9,7 @@ func init() { RegisterStaticDocsPage( "Overview", "A pure-Go, byte-exact reimplementation of tree-sitter — no CGo, no C toolchain, 206 grammars built in.", + "/", route.FileModuleOptions{ Bindings: func(ctx *route.RouteContext, page route.FilePage, data any) route.FileTemplateBindings { return route.FileTemplateBindings{ diff --git a/app/playground/page.server.go b/app/playground/page.server.go index e9dd367..641f678 100644 --- a/app/playground/page.server.go +++ b/app/playground/page.server.go @@ -30,11 +30,11 @@ func init() { docsapp.RegisterStaticDocsPage( "Playground", "Parse source and run tree-sitter queries locally in a GoSX-managed WebAssembly engine.", + "/playground", route.FileModuleOptions{ Load: loadPlayground, Metadata: func(ctx *route.RouteContext, page route.FilePage, data any) (server.Metadata, error) { return server.Metadata{ - Title: server.Title{Absolute: "Playground — gotreesitter"}, Links: []server.LinkTag{{ Rel: "stylesheet", Href: docsapp.PublicAssetURL("playground/playground.css"), diff --git a/content/docs/authoring-languages.md b/content/docs/authoring-languages.md index 6425e8b..6defa65 100644 --- a/content/docs/authoring-languages.md +++ b/content/docs/authoring-languages.md @@ -43,7 +43,7 @@ The relevant functions are all public: |---|---|---| | `grammargen.ImportGrammarJSON(data []byte) (*Grammar, error)` | `grammargen/import_grammarjson.go` | Parse a resolved `grammar.json` (the output of `tree-sitter generate`) into the grammar IR. Rules, extras, conflicts, externals, inline, word, precedences, reserved sets, supertypes are all imported. | | `grammargen.GenerateLanguage(g *Grammar) (*gotreesitter.Language, error)` | `grammargen/encode.go` | Compile the IR into runtime parse tables. | -| `grammargen.GenerateLanguageAndBlob(g *Grammar) (*gotreesitter.Language, []byte, error)` | `grammargen/encode.go` | Same, plus the serialized gob+gzip blob in one pass. `...WithContext` variants exist for cancellation. | +| `grammargen.GenerateLanguageAndBlob(g *Grammar) (*gotreesitter.Language, []byte, error)` | `grammargen/encode.go` | Same, plus a serialized language blob in one pass. Blobs without `LargeStateGotos` keep the legacy gob+gzip format. Map-bearing blobs use a deterministic versioned envelope. Load either form with `gotreesitter.LoadLanguage`. `...WithContext` variants exist for cancellation. | | `grammargen.EmitGrammarGo(g *Grammar, pkgName, funcName string) ([]byte, error)` | `grammargen/emit_grammar_go.go` | Emit Go DSL source that reconstructs the grammar — useful for vendoring the grammar as reviewable Go code instead of JSON. | | `gotreesitter.LoadLanguage(data []byte) (*Language, error)` | `load_language.go` | Deserialize a blob at runtime. The only function needed to load a pre-compiled grammar — no grammargen import, no registry. | | `grammars.LoadLanguage(name string, data []byte)` | `grammars/embedded_loader.go` | Like the above, but also attaches any external scanner / external lex-state tables registered for `name` in the `grammars` registry. | diff --git a/public/docs.css b/public/docs.css index 8cea922..8ba810e 100644 --- a/public/docs.css +++ b/public/docs.css @@ -24,6 +24,8 @@ a{color:var(--blue);text-decoration:none;font-weight:600}a:hover{color:var(--vio .tnav{display:flex;gap:6px;align-items:center} .ghlink{font:600 13px 'JetBrains Mono',monospace;color:var(--ink);background:var(--paper);padding:7px 12px;border-radius:2px;border:2px solid var(--paper)} .ghlink:hover{background:var(--yellow);color:var(--ink)} +.ghlink[aria-current="page"]{background:var(--color-yellow);color:var(--color-ink)} +:where(a,button,input,select,textarea,summary):focus-visible{outline:3px solid var(--color-blue);outline-offset:3px} .newpill{font:600 11px 'JetBrains Mono',monospace;color:var(--ink);background:var(--yellow);padding:5px 9px;border-radius:2px;text-transform:uppercase;letter-spacing:.5px} .body{display:flex;align-items:flex-start;gap:0} .sidebar{position:sticky;top:58px;align-self:flex-start;width:250px;flex:none;height:calc(100vh - 58px);overflow-y:auto;padding:20px 16px 40px;border-right:4px solid var(--ink);background:var(--paper)} diff --git a/scripts/verify-production.sh b/scripts/verify-production.sh index ccd2695..78d0551 100755 --- a/scripts/verify-production.sh +++ b/scripts/verify-production.sh @@ -12,7 +12,7 @@ fail() { exit 1 } -for command in go gosx tinygo curl grep sed awk mktemp find; do +for command in git go gosx tinygo curl grep sed awk mktemp find sort; do command -v "$command" >/dev/null 2>&1 || fail "required command not found: $command" done @@ -43,10 +43,9 @@ gosx_cli_version="$(gosx version 2>&1)" grep -Fq "GoSX ${gosx_version#v}" README.md || fail "README GoSX requirement does not match go.mod" grep -Fq "m31labs.dev/gosx/cmd/gosx@$gosx_version" README.md || fail "README GoSX install command does not match go.mod" -# Application behavior must be authored in Go/GoSX. Framework-generated -# runtime assets in dist are allowed; repository-authored JS is not. -if find . -path './.git' -prune -o -path './dist' -prune -o -path './build' -prune \ - -o -type f \( -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o -name '*.tsx' \) -print -quit | grep -q .; then +# Application behavior must be authored in Go/GoSX. Ignore generated files +# through the repository's normal ignore rules. Reject tracked or new source. +if git ls-files --cached --others --exclude-standard -- '*.js' '*.jsx' '*.ts' '*.tsx' | grep -q .; then fail "application-authored JavaScript remains in the repository" fi if grep -R -n --include='*.gsx' '/dev/null || true fi rm -f "$server_log" "$page_body" + rm -rf "$route_pages" } trap cleanup EXIT @@ -99,20 +101,36 @@ for _ in $(seq 1 60); do done curl --silent --fail "$base/healthz" | grep -q '"ok":true' -routes=("/" "/playground") +routes=("/" "/playground" "/authoring") routes+=("/changelog") for source in content/docs/*.md; do routes+=("/docs/$(basename "$source" .md)") done -for route in "${routes[@]}"; do - curl --silent --show-error --fail --output /dev/null "$base$route" +for index in "${!routes[@]}"; do + route="${routes[$index]}" + route_page="$route_pages/$index.html" + curl --silent --show-error --fail --output "$route_page" "$base$route" + grep -Fq 'rel="canonical"' "$route_page" || fail "canonical metadata missing from $route" + grep -Fq "href=\"https://gotreesitter.m31labs.dev$route\"" "$route_page" || + fail "canonical metadata does not match $route" done +while IFS= read -r href; do + [[ -n "$href" ]] || continue + curl --silent --show-error --fail --output /dev/null "$base$href" || + fail "internal link does not resolve: $href" +done < <( + grep -rhoE 'href="/[^"]*"' "$route_pages" | + sed -e 's/^href="//' -e 's/"$//' -e 's/&/\&/g' -e 's/#.*$//' | + sort -u +) + curl --silent --fail "$base/" | grep -q '5.53×' || fail "landing headline metric (5.53× geomean) missing from /" curl --silent --fail "$base/docs/performance" | grep -q '5.526× C' || fail "performance geomean (5.526× C) missing from /docs/performance" curl --silent --show-error --fail --output "$page_body" "$base/changelog" grep -q 'History you can interrogate' "$page_body" || fail "changelog hero missing from /changelog" grep -q 'v0.47.1' "$page_body" || fail "v0.47.1 missing from /changelog" +grep -q 'href="/changelog" aria-current="page"' "$page_body" || fail "active changelog navigation state is missing" languages_html="$(curl --silent --show-error --fail "$base/docs/languages")" lang_search_url="$(grep -oE '/gosx/islands/LangSearch\.json\?v=[0-9a-f]{12}' <<<"$languages_html" | head -n 1)" @@ -134,8 +152,22 @@ curl --silent --show-error --fail --head "$base$wasm_url" | grep -Fqi 'content-t PLAYGROUND_BASE_URL="$base" go run ./cmd/verify-playground-browser +curl --silent --show-error --fail --output "$page_body" "$base/authoring" +grep -q 'data-gosx-engine="GotreesitterAuthoring"' "$page_body" || fail "authoring GoSX engine mount is missing" +authoring_wasm_url="$(grep -oE '/authoring/authoring\.wasm\?v=[0-9a-f]{12}' "$page_body" | head -n 1)" +[[ -n "$authoring_wasm_url" ]] || fail "content-versioned authoring WASM URL is missing" +curl --silent --show-error --fail --head "$base$authoring_wasm_url" | + grep -Fqi 'content-type: application/wasm' || fail "authoring engine is not served as application/wasm" +authoring_worker_url="$(grep -oE '/authoring/authoring-worker\.js\?v=[0-9a-f]{12}' "$page_body" | head -n 1)" +[[ -n "$authoring_worker_url" ]] || fail "content-versioned authoring worker URL is missing" +curl --silent --show-error --fail --output /dev/null "$base$authoring_worker_url" +authoring_bases_url="$(grep -oE '/authoring/bases/index\.json\?v=[0-9a-f]{12}' "$page_body" | head -n 1)" +[[ -n "$authoring_bases_url" ]] || fail "content-versioned authoring base index URL is missing" +curl --silent --show-error --fail "$base$authoring_bases_url" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"calc"' || + fail "authoring base index does not include calc" + if [[ "${RUN_BROWSER_PERF:-0}" == "1" ]]; then - for route in / /docs/getting-started /docs/performance /playground; do + for route in / /docs/getting-started /docs/performance /playground /authoring; do gosx perf --coverage --budget perf-budget.json --budget-profile docs "$base$route" done fi