Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/Documents
diff options
context:
space:
mode:
authorLeitwolf2026-10-15 18:43:48 +0300
committerGitHub2026-10-15 18:43:48 +0300
commit0b4634261957c81626cf134d3126ae3d3a04735a (patch)
tree4690238a1af85bfb71a3ef237ed1ac8bae32983c /Documents
parentc525fa05bbe85f3e7337d4a43c21ee6c864c839a (diff)
parent607a15b1e856097ff4f8e3892368217a4672f16b (diff)
downloadwebsite-0b4634261957c81626cf134d3126ae3d3a04735a.tar.gz
Merge pull request #34 from Progmasoft/feature/tala-run
Serve tala.run, document talac and Tazel, and move the site API to ASP.NET Core 10
Diffstat (limited to 'Documents')
-rw-r--r--Documents/ARCHITECTURE.md43
-rw-r--r--Documents/DEVELOPMENT.md50
-rw-r--r--Documents/OPERATIONS.md105
-rw-r--r--Documents/api/CheckDocumentation.cs118
-rw-r--r--Documents/api/docfx.json39
-rw-r--r--Documents/api/index.md31
-rw-r--r--Documents/api/namespaces/Progmasoft.Tala.Website.Api.md10
-rw-r--r--Documents/api/packages.lock.json35
-rw-r--r--Documents/api/toc.yml7
9 files changed, 339 insertions, 99 deletions
diff --git a/Documents/ARCHITECTURE.md b/Documents/ARCHITECTURE.md
index c11cfea..2521b49 100644
--- a/Documents/ARCHITECTURE.md
+++ b/Documents/ARCHITECTURE.md
@@ -8,32 +8,38 @@ anything about a visitor on a server.
| Part | Technology | Artifact | Serves |
| --- | --- | --- | --- |
-| `frontend/` | Vue 3, Vite, TypeScript | static files in `frontend/dist/` | `xsharp-lang.xyz` |
-| `backend/` | Kotlin on JVM 25, Spring Boot | one executable jar | `xsharp-lang.xyz/api/v1/site` |
-| `forum/` | one HTML page with plain CSS and JavaScript | the five files as they are | `forum.xsharp-lang.xyz` |
+| `frontend/` | Vue 3, Vite, TypeScript | static files in `frontend/dist/` | `tala.run`, `talac.tala.run`, `tazel.tala.run` |
+| `backend/` | ASP.NET Core 10 | one published application | `tala.run/api/v1/site` |
+| `forum/` | one HTML page with plain CSS and JavaScript | the five files as they are | `forum.tala.run` |
```text
browser
| HTTPS
v
Nginx
- |-- xsharp-lang.xyz static files of the Vue build; unknown paths fall back to index.html
- |-- xsharp-lang.xyz/api/v1/site -> http://127.0.0.1:5086 (Spring)
- |-- forum.xsharp-lang.xyz five named static files; every other path is 404
- `-- www, docs, spec, viget permanent redirects
+ |-- tala.run, talac.tala.run, tazel.tala.run static files of the Vue build; unknown paths fall back to index.html
+ |-- tala.run/api/v1/site -> http://127.0.0.1:5086 (ASP.NET Core)
+ |-- forum.tala.run five named static files; every other path is 404
+ |-- www.tala.run permanent redirect to tala.run
+ `-- xsharp-lang.xyz and its subdomains permanent redirects to tala.run, and viget to viget.progmasoft.com
```
## Frontend
-A single-page application with three routes, defined in `frontend/src/main.ts`:
+A single-page application that serves three sites. `site.ts` decides the site from the host name, and
+`frontend/src/main.ts` registers the routes of that site:
-| Route | Page | Source |
-| --- | --- | --- |
-| `/` | why the language exists, and its current state | `pages/HomePage.vue` |
-| `/docs/overview` | the design direction and the compiler stages | `pages/OverviewPage.vue` |
-| `/docs/getting-started` | building the compiler from its repository | `pages/GettingStartedPage.vue` |
-
-Any other path redirects to `/`. The router uses history URLs, so the server must answer unknown paths with
+| Host | Route | Page | Source |
+| --- | --- | --- | --- |
+| `tala.run` | `/` | why the language exists, and its current state | `pages/HomePage.vue` |
+| `tala.run` | `/docs/overview` | the design direction and the compiler stages | `pages/OverviewPage.vue` |
+| `tala.run` | `/docs/getting-started` | building the compiler from its repository | `pages/GettingStartedPage.vue` |
+| `tala.run` | `/docs/syntax` | a first look at the syntax | `content/syntax.ts` |
+| `talac.tala.run` | `/docs/command-line`, `/docs/outputs` | the compiler | `content/talac.ts` |
+| `tazel.tala.run` | `/docs/commands`, `/docs/project-file` | the build system | `content/tazel.ts` |
+
+The pages from `content/` are rendered by `pages/DocPage.vue`; each topic carries its text in all four languages. The
+root of the two documentation hosts opens their first topic. Any other path redirects to the root. The router uses history URLs, so the server must answer unknown paths with
`index.html`.
The rest of the frontend is small:
@@ -67,7 +73,7 @@ to the backend does not publish it.
## Forum page
-`forum.xsharp-lang.xyz` is a notice that the forum is not open yet. It is not a forum: there are no accounts, no
+`forum.tala.run` is a notice that the forum is not open yet. It is not a forum: there are no accounts, no
posts and no service behind it. The page shows its one notice in the language of the browser and offers a theme
button; it has no language menu and stores no language. It is marked `noindex` in the page and in the response
header.
@@ -80,13 +86,14 @@ allows only its own script, stylesheet and two images.
- **Accounts and packages.** Progmasoft accounts and the ViGet package registry belong to the Progmaweb repository
and live on `progmasoft.com` hosts. This site links to neither a sign-in nor a registry.
- **A legacy redirect only.** `viget.xsharp-lang.xyz` redirects permanently to `viget.progmasoft.com`, keeping path
- and query. That redirect is routing compatibility, not a feature of this site.
+ and query. That redirect is routing compatibility, not a feature of this site; tala.run has no ViGet host.
- **Visitor data.** The language and theme a visitor chose are kept in the browser's local storage. The site sets no
cookie and sends nothing about the visitor to the backend.
## Search engines and crawlers
-- `sitemap.xml` lists exactly the three routes above.
+- Each host has its own `sitemap.xml` and `robots.txt`; Nginx serves `public/hosts/talac/` and
+ `public/hosts/tazel/` for the two documentation hosts. A sitemap lists exactly the routes of its host.
- Every route has its own title, description and canonical URL. A language is a preference, not a second URL, so
there are no `hreflang` alternatives.
- `robots.txt` names the crawlers the site has a rule for. Search and answer-retrieval crawlers are allowed. Among
diff --git a/Documents/DEVELOPMENT.md b/Documents/DEVELOPMENT.md
index 4781e76..0bf7f55 100644
--- a/Documents/DEVELOPMENT.md
+++ b/Documents/DEVELOPMENT.md
@@ -9,8 +9,7 @@
| --- | --- | --- |
| Node.js | 22.18 or newer; CI uses 24 | the frontend and all JavaScript tests |
| npm | the one that ships with Node.js | frontend dependencies |
-| Temurin JDK | 25 | the backend |
-| Gradle | 9.6.0, the version CI installs | the backend build; the repository has no Gradle wrapper |
+| .NET SDK | 10, as `global.json` allows | the backend |
## Frontend
@@ -25,7 +24,9 @@ npm run build # type check and production build into dist/
```
`npm run format` rewrites the files Prettier checks. The development server proxies `/api` to
-`http://127.0.0.1:5080`, the default port of a locally started backend.
+`http://127.0.0.1:5086`, the default port of a locally started backend. A development host has no subdomains, so
+`?site=talac` or `?site=tazel` opens the documentation of the compiler or the build system; the choice lasts for the
+tab, and `?site=language` returns to the language site.
`.npmrc` refuses install scripts of dependencies that were not reviewed. A new dependency with an install script
fails `npm ci` until its exact version is allowed in `package.json`.
@@ -33,31 +34,25 @@ fails `npm ci` until its exact version is allowed in `package.json`.
## Backend
```text
-cd backend
-gradle test bootJar dokkaGenerate --no-daemon
-gradle bootRun --no-daemon # serves on 127.0.0.1:5080
+dotnet restore Progmasoft.Tala.Website.slnx --locked-mode
+dotnet build Progmasoft.Tala.Website.slnx --configuration Release --no-restore
+dotnet test --project backend/Progmasoft.Tala.Website.Api.Tests/Progmasoft.Tala.Website.Api.Tests.csproj --configuration Release --no-build
+dotnet run --project backend/Progmasoft.Tala.Website.Api # serves on 127.0.0.1:5086
```
-- `test` runs the Spring tests.
-- `bootJar` builds `build/libs/visual-xsharp-website-api-1.0.0.jar`.
-- `dokkaGenerate` builds the API documentation and is a gate: a public declaration without KDoc, a broken link or an
- unresolved reference fails the build.
+- The build treats warnings as errors and requires a documentation comment on every public declaration.
+- DocFX builds the API reference from those comments. `dotnet tool restore`, then
+ `dotnet docfx Documents/api/docfx.json --warningsAsErrors` and
+ `dotnet run Documents/api/CheckDocumentation.cs -- Documents/api`; the second fails on any namespace, type or
+ member of the reference without a description. See [the API reference](api/index.md).
+- The tests run the endpoints in an in-memory host with xUnit v3 on Microsoft.Testing.Platform.
+- `PORT` changes the port; the service binds to loopback only.
-### Dependency locks and checksums
+### Dependency locks
-The backend pins every resolved version in `gradle.lockfile` and the SHA-256 of every downloaded file in
-`gradle/verification-metadata.xml`. After changing a dependency or a plugin, regenerate both and review the diff:
-
-```text
-gradle dependencies --write-locks --no-daemon
-gradle --refresh-dependencies --write-verification-metadata sha256 help test jacocoTestReport bootJar dokkaGenerate --no-daemon
-```
-
-`--refresh-dependencies` matters. With a warm local cache Gradle does not download every metadata file again and so
-does not record it; the build then passes locally and fails verification on a clean CI machine.
-
-A checksum is an integrity pin, not a trust review. Confirm the version and the repository of an artifact before
-accepting its checksum.
+Every project restores from its `packages.lock.json`, and CI restores with `--locked-mode`, so a resolved version
+cannot change unnoticed. After changing a package reference, run `dotnet restore Progmasoft.Tala.Website.slnx` and
+review the changed lock files.
## Forum page
@@ -78,13 +73,14 @@ All JavaScript tests run with `npm test` in `frontend/`.
| --- | --- |
| `locales.test.mjs` | the published locales, browser-language selection, the English fallback, that a stored choice wins, text direction, and when a change is remembered |
| `seo.test.mjs` | the title, description and canonical URL of every route in every language |
+| `sites.test.mjs` | which host is which site, links between the sites, the sidebars, that every topic is written in every language with the same code samples, and that the documentation teaches only the current command lines |
| `forum.test.mjs` | the notice in each language, the fallback, the absence of a language menu, and the theme button |
| `site-integrity.test.mjs` | sitemap and router agreement, forum non-indexing, logo and social image shape, absence of retired links, pinned workflow actions, and that the Nginx and systemd files agree with each other |
Two tests in `site-integrity.test.mjs` compare the site with a checkout of the compiler repository next to this one
and skip themselves when it is absent.
-The backend tests assert the content of `/api/v1/site`, the empty health response, that the API is read-only, and
+The API tests assert the content of `/api/v1/site`, the empty health response, that the API is read-only, and
that account and registry routes do not exist.
A change to a page is not verified by these tests alone. Look at it in a browser: all four languages, both themes, a
@@ -95,10 +91,10 @@ narrow window, and keyboard focus.
| Job | Enforces |
| --- | --- |
| `frontend` | `npm ci`, `npm audit` at moderate severity, type check, tests with coverage, format check, build |
-| `backend` | tests with coverage, the jar, and the Dokka documentation gate |
+| `backend` | locked restore, build with warnings as errors, tests with coverage, the published application, and the DocFX documentation gate |
| `frontend-coverage`, `backend-coverage` | upload coverage to Codecov |
| `dependency-review` | reviews dependency changes of a pull request |
-| CodeQL | static analysis of the workflows, TypeScript and Kotlin |
+| CodeQL | static analysis of the workflows, TypeScript and C# |
Only the coverage jobs receive an OIDC token; the jobs that install and build do not, and pull requests from forks
skip the upload. Workflow actions are pinned to commit hashes, and a test fails when one is not.
diff --git a/Documents/OPERATIONS.md b/Documents/OPERATIONS.md
index 72ab09a..f486d13 100644
--- a/Documents/OPERATIONS.md
+++ b/Documents/OPERATIONS.md
@@ -3,7 +3,8 @@
# Tala website operations
-This runbook covers only the language website and its not-yet-open forum page.
+This runbook covers only the language website, the documentation sites of the
+compiler and the build system, and the not-yet-open forum page.
The account site, corporate site, and package service have their own repositories
and deployments. A redirect from an old package hostname is retained for
compatibility, but it does not make package functions part of this website.
@@ -12,22 +13,28 @@ compatibility, but it does not make package functions part of this website.
| Host and path | Owner | Current treatment |
| --- | --- | --- |
-| `xsharp-lang.xyz/` | Vue language site | Live |
-| `xsharp-lang.xyz/docs/overview` | Vue language site | Live |
-| `xsharp-lang.xyz/docs/getting-started` | Vue language site | Live |
-| `xsharp-lang.xyz/api/v1/site` | Spring language API | Live; the only proxied API path |
-| `forum.xsharp-lang.xyz/` | Static placeholder | Live; noindex |
-| `www`, `docs`, `spec` subdomains | Redirect | Permanent redirect to `xsharp-lang.xyz` |
-| `viget.xsharp-lang.xyz/*` | Legacy redirect | Permanent redirect to the new package host |
-
-The main domain is served with `ops/nginx/xsharp.conf`. While it is switched to
-`ops/nginx/xsharp-maintenance.conf`, the three language routes answer 503 and
-`/api/v1/site` is not exposed.
+| `tala.run/`, `/docs/overview`, `/docs/getting-started`, `/docs/syntax` | Vue language site | Live |
+| `talac.tala.run/docs/*` | Vue build, compiler documentation | Live |
+| `tazel.tala.run/docs/*` | Vue build, build-system documentation | Live |
+| `tala.run/api/v1/site` | ASP.NET Core language API | Live; the only proxied API path |
+| `forum.tala.run/` | Static placeholder | Live; noindex |
+| `www.tala.run` | Redirect | Permanent redirect to `tala.run` |
+| `xsharp-lang.xyz`, `www`, `docs`, `spec` | Former domain | Permanent redirect to the same path of `tala.run` |
+| `forum.xsharp-lang.xyz` | Former domain | Permanent redirect to `forum.tala.run` |
+| `viget.xsharp-lang.xyz/*` | Legacy redirect | Permanent redirect to the package host |
+
+tala.run has no ViGet host; the registry lives at `viget.progmasoft.com`.
+
+The sites are served with `ops/nginx/tala.conf`. While it is switched to
+`ops/nginx/tala-maintenance.conf`, the routes of the three sites answer 503 and
+`/api/v1/site` is not exposed. One certificate, `/etc/letsencrypt/live/tala.run/`,
+covers tala.run, www.tala.run, talac.tala.run, tazel.tala.run and forum.tala.run;
+the former domain keeps its own certificates for its redirects.
Do not use a catch-all `/api/` proxy for this service. The production Nginx
configuration proxies exactly `/api/v1/site`; other `/api/` paths return 404.
-The Spring process binds only to `127.0.0.1`. Local runs default to port 5080;
-the checked-in systemd unit sets `PORT=5086`, matching the Nginx proxy.
+The API process binds only to `127.0.0.1`, on port 5086 unless `PORT` says
+otherwise; the checked-in systemd unit sets `PORT=5086`, matching the Nginx proxy.
Its `/health` endpoint is for local process checks, not an advertised public
route.
@@ -43,34 +50,21 @@ npm test
npm run build
```
-Run the backend gates from `backend/` with Temurin JDK 25:
+Run the backend gates from the repository root with the .NET 10 SDK:
```text
-gradle test bootJar dokkaGenerate --no-daemon
+dotnet restore Progmasoft.Tala.Website.slnx --locked-mode
+dotnet build Progmasoft.Tala.Website.slnx --configuration Release --no-restore
+dotnet test --project backend/Progmasoft.Tala.Website.Api.Tests/Progmasoft.Tala.Website.Api.Tests.csproj --configuration Release --no-build
+dotnet publish backend/Progmasoft.Tala.Website.Api/Progmasoft.Tala.Website.Api.csproj --configuration Release --output publish
```
-`dokkaGenerate` is the documentation gate: a public declaration without KDoc
-fails it.
-
-The backend checks in Gradle dependency locks and SHA-256 verification metadata
-to keep transitive versions and downloaded artifacts stable. When intentionally
-updating dependencies, run these commands from `backend/`, review both generated
-files, then rerun the ordinary backend gates:
-
-```text
-gradle dependencies --write-locks --no-daemon
-gradle --refresh-dependencies --write-verification-metadata sha256 help test jacocoTestReport bootJar dokkaGenerate --no-daemon
-```
-
-Keep `--refresh-dependencies`. Written from a warm local cache, the metadata
-misses files that Gradle did not download again, and the build then fails
-verification on a clean machine while it passes locally.
-
-Do not accept a checksum-only change without confirming the artifact version
-and source repository; the checksum is an integrity pin, not a trust review.
+The build is the documentation gate: a public declaration without its
+documentation comment fails it. Package versions are pinned in the
+`packages.lock.json` of each project.
The frontend tests assert canonical metadata, sitemap membership, asset shape,
-forum non-indexing, and the absence of retired site links. The Spring tests
+forum non-indexing, and the absence of retired site links. The API tests
assert site-only API behavior, health, read-only operation, and missing account
and registry routes. These gates are necessary but not a substitute for a
browser review of both themes, all four languages, and narrow screens.
@@ -96,8 +90,7 @@ Before publication, verify that:
All source links should target the current `Progmasoft/tala` default
branch. In particular, check the `Spec/`, `Documents/BUILDING.md`,
`Documents/CLI.md`, and `Examples/` targets after any repository move.
-The only frontend navigation routes are `/`, `/docs/overview`, and
-`/docs/getting-started`. A retired login, registration, or package route must
+The routes of each site are listed in the route table above. A retired login, registration, or package route must
not reappear in the site shell.
The forum placeholder links back to the language domain and to the compiler
@@ -146,23 +139,26 @@ On the server the site lives in one directory:
current/
frontend/ contents of frontend/dist
forum/ the five forum files
- backend/ the Spring jar, next to the files of the legacy process
+ site-api/ the published ASP.NET Core API
+ backend/ the legacy .NET process of Progmaweb's ViGet status route
previous-<timestamp>/ the release before, kept for rollback
```
`current` is a real directory, not a symbolic link. A release replaces it by
renaming:
-1. Build the frontend and the jar from the commit being released, and run the
- local gates.
+1. Build the frontend and publish the API from the commit being released, and
+ run the local gates.
2. On the server, copy `current` to a new directory next to it with
`cp -a`, which keeps owners, modes and SELinux labels, and replace the
changed parts inside the copy.
-3. Start the new jar on a free loopback port as the service user, request
+3. Start the new API on a free loopback port as the service user with
+ `PORT=<port> dotnet site-api/Progmasoft.Tala.Website.Api.dll`, request
`/health` and `/api/v1/site`, and stop it.
4. Rename `current` to `previous-<timestamp>` and the new directory to
`current`.
-5. Run `restorecon -R` on `current/frontend` and `current/forum`.
+5. Run `restorecon -R` on `current/frontend`, `current/forum` and
+ `current/site-api`.
6. Restart the API service, then check the public routes from outside.
Step 5 has to come after step 4. The SELinux rules that let Nginx read the
@@ -178,8 +174,8 @@ on the server itself before concluding that a release failed.
## Maintenance mode
-`ops/nginx/xsharp-maintenance.conf` answers the main domain with 503 and keeps
-the legacy redirect working. `ops/nginx/xsharp.conf` serves the site. Keep the
+`ops/nginx/tala-maintenance.conf` answers the three sites with 503 and keeps
+the legacy redirect working. `ops/nginx/tala.conf` serves them. Keep the
two mutually exclusive in the live Nginx include directory. Building the Vue
bundle or deploying the forum does not by itself authorize switching between
them.
@@ -196,14 +192,14 @@ Before switching the main site from maintenance to public:
5. Run both build/test sets shown above.
6. Install the Vue `dist/` contents at the static root named in the Nginx
config, preserving file permissions and SELinux labels.
-7. Install Java 25 on the host and test the Spring jar on loopback before
- replacing any running API service.
+7. Test the published API on loopback with the shared ASP.NET Core 10 runtime
+ before replacing the running API service.
8. Confirm the old service's unrelated status function has moved to the
correct owner before retiring the old process.
9. Make one explicit Nginx configuration switch, run `nginx -t`, and reload.
10. Check the public routes and headers again from outside the server.
-The Java service is not required for the static homepage render. If it is
+The API service is not required for the static homepage render. If it is
not ready, do not pretend it is ready by rewriting the API path. Delay the
rollout or explicitly publish static pages without the API after reviewing
the impact of that decision.
@@ -212,8 +208,8 @@ the impact of that decision.
The redirect hostname is intentionally split from the maintenance vhost.
Requests to `viget.xsharp-lang.xyz` must produce 308 to
-`https://viget.progmasoft.com` and preserve path and query string. This is
-true while `xsharp-lang.xyz` itself returns 503. The redirect certificate
+`https://viget.progmasoft.com` and preserve path and query string, also while
+the sites answer 503. The redirect certificate
uses the existing SAN entry for the old hostname. Do not remove the redirect
in the course of cleaning up language-site copy.
@@ -233,7 +229,7 @@ confirms the expected 503. The forum and legacy redirect should remain
unaffected.
Do not roll back by deleting certificates, disabling SELinux, or pointing
-unrelated hostnames at the language frontend. If the Spring process fails,
+unrelated hostnames at the language frontend. If the API process fails,
inspect its systemd journal and local `/health` response, then restore the
previous service unit or leave the site in maintenance until the owner
boundary is clear.
@@ -250,7 +246,7 @@ Neither the static site nor the forum placeholder should send personal data
to a third-party analytics service. The content security policy allows
same-origin scripts only, so a script added by an intermediary is blocked by
the browser rather than run. Theme and language preferences are kept
-locally in the browser. Nginx and Spring logs still need normal retention
+locally in the browser. Nginx and API logs still need normal retention
and access-control practices, but this repository does not define an account
database or a user profile store.
@@ -259,8 +255,9 @@ database or a user profile store.
After a release, perform the same checks from a clean browser profile with no
saved language or theme choice. Confirm that the theme is dark and that the
language is the browser's when it is one of the four, and English otherwise.
-Choose another language in the menu, navigate through both documentation
-routes, and confirm the choice persists. Choose Hebrew and confirm that text
+Choose another language in the menu, navigate through the documentation
+routes of all three sites, and confirm the choice persists on each host; a
+choice is kept per host, because the browser keeps storage per origin. Choose Hebrew and confirm that text
runs right to left while the layout and the code blocks stay as they are.
Switch to light mode and repeat. The forum keeps its own theme key and follows
the browser language; it should not change the main site's settings.
diff --git a/Documents/api/CheckDocumentation.cs b/Documents/api/CheckDocumentation.cs
new file mode 100644
index 0000000..f6b2da8
--- /dev/null
+++ b/Documents/api/CheckDocumentation.cs
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
+
+// Documentation coverage gate of the API reference.
+//
+// DocFX builds a reference from whatever comments exist and does not report a declaration that has none. This
+// program reads the metadata DocFX generated and fails when a namespace, type or member is undocumented, so the
+// reference cannot contain an entry without a description.
+//
+// Usage, after `dotnet docfx metadata Documents/api/docfx.json`:
+//
+// dotnet run Documents/api/CheckDocumentation.cs -- Documents/api
+//
+// The exit code is 0 when everything is documented, 1 when something is not, and 2 when the metadata is missing.
+
+using System.Text.RegularExpressions;
+
+string root = args.Length > 0 ? args[0] : "Documents/api";
+string referenceDirectory = Path.Combine(root, "reference");
+string namespaceDirectory = Path.Combine(root, "namespaces");
+
+if (!Directory.Exists(referenceDirectory))
+{
+ Console.Error.WriteLine($"No metadata in '{referenceDirectory}'. Run 'dotnet docfx metadata' first.");
+ return 2;
+}
+
+// A namespace has no declaration that could carry a comment. Its description is an overwrite file named after it.
+HashSet<string> describedNamespaces = Directory.Exists(namespaceDirectory)
+ ? Directory.EnumerateFiles(namespaceDirectory, "*.md")
+ .Where(HasDescription)
+ .Select(path => Path.GetFileNameWithoutExtension(path)!)
+ .ToHashSet(StringComparer.Ordinal)
+ : [];
+
+List<string> undocumented = [];
+int checkedItems = 0;
+
+foreach (string file in Directory.EnumerateFiles(referenceDirectory, "*.yml").Order(StringComparer.Ordinal))
+{
+ if (Path.GetFileName(file) == "toc.yml")
+ {
+ continue;
+ }
+
+ // The items a file defines come first; the `references` section after them repeats names of other files.
+ string text = File.ReadAllText(file).ReplaceLineEndings("\n");
+ int references = text.IndexOf("\nreferences:", StringComparison.Ordinal);
+ string definitions = references < 0 ? text : text[..references];
+
+ foreach (string item in ItemStart().Split(definitions).Skip(1))
+ {
+ string uid = item[..item.IndexOf('\n')].Trim();
+ string type = ItemType().Match(item) is { Success: true } match ? match.Groups[1].Value : "Item";
+ checkedItems++;
+
+ bool documented = type == "Namespace"
+ ? describedNamespaces.Contains(uid)
+ : ItemSummary().IsMatch(item);
+ if (!documented)
+ {
+ undocumented.Add($"{type} {uid}");
+ }
+ }
+}
+
+if (checkedItems == 0)
+{
+ Console.Error.WriteLine($"The metadata in '{referenceDirectory}' defines nothing; the gate would pass vacuously.");
+ return 2;
+}
+
+if (undocumented.Count != 0)
+{
+ Console.Error.WriteLine($"{undocumented.Count} of {checkedItems} API items have no documentation:");
+ foreach (string entry in undocumented)
+ {
+ Console.Error.WriteLine($" {entry}");
+ }
+
+ Console.Error.WriteLine("Add a <summary> to each declaration, or a file in Documents/api/namespaces for a namespace.");
+ return 1;
+}
+
+Console.WriteLine($"All {checkedItems} API items are documented.");
+return 0;
+
+// An overwrite file describes its namespace when it has text after its front matter and comment header.
+static bool HasDescription(string path)
+{
+ string text = File.ReadAllText(path).ReplaceLineEndings("\n");
+ int frontMatterEnd = text.IndexOf("\n---", 3, StringComparison.Ordinal);
+ if (!text.StartsWith("---\n", StringComparison.Ordinal) || frontMatterEnd < 0)
+ {
+ return false;
+ }
+
+ string body = HtmlComment().Replace(text[(frontMatterEnd + 4)..], string.Empty);
+ return text[..frontMatterEnd].Contains($"uid: {Path.GetFileNameWithoutExtension(path)}", StringComparison.Ordinal)
+ && !string.IsNullOrWhiteSpace(body);
+}
+
+internal static partial class Program
+{
+ // Every defined item starts with its identifier at the left margin.
+ [GeneratedRegex(@"^- uid: ", RegexOptions.Multiline)]
+ private static partial Regex ItemStart();
+
+ [GeneratedRegex(@"^ type: (\w+)", RegexOptions.Multiline)]
+ private static partial Regex ItemType();
+
+ // A summary with content: text on the same line, or a block scalar followed by an indented line.
+ [GeneratedRegex(@"^ summary: (?:[^\s>|].*|[>|][-+]?\n \S)", RegexOptions.Multiline)]
+ private static partial Regex ItemSummary();
+
+ [GeneratedRegex(@"<!--.*?-->", RegexOptions.Singleline)]
+ private static partial Regex HtmlComment();
+}
diff --git a/Documents/api/docfx.json b/Documents/api/docfx.json
new file mode 100644
index 0000000..58bc6c9
--- /dev/null
+++ b/Documents/api/docfx.json
@@ -0,0 +1,39 @@
+{
+ "metadata": [
+ {
+ "src": [
+ {
+ "src": "../../backend/Progmasoft.Tala.Website.Api",
+ "files": [
+ "Progmasoft.Tala.Website.Api.csproj"
+ ]
+ }
+ ],
+ "dest": "reference",
+ "includePrivateMembers": true
+ }
+ ],
+ "build": {
+ "content": [
+ {
+ "files": [
+ "index.md",
+ "toc.yml",
+ "reference/**/*.yml"
+ ]
+ }
+ ],
+ "dest": "_site",
+ "globalMetadata": {
+ "_appTitle": "Tala website API",
+ "_enableSearch": false
+ },
+ "overwrite": [
+ {
+ "files": [
+ "namespaces/*.md"
+ ]
+ }
+ ]
+ }
+}
diff --git a/Documents/api/index.md b/Documents/api/index.md
new file mode 100644
index 0000000..8cbed8a
--- /dev/null
+++ b/Documents/api/index.md
@@ -0,0 +1,31 @@
+<!--
+SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
+-->
+
+# Tala website API
+
+Reference documentation of the website's ASP.NET Core API, generated by DocFX from the XML documentation comments
+of `backend/Progmasoft.Tala.Website.Api`. The reference includes internal and private members: it documents the code
+for the people who maintain it.
+
+Build and check it from the repository root:
+
+```sh
+dotnet tool restore
+dotnet docfx Documents/api/docfx.json --warningsAsErrors
+dotnet run Documents/api/CheckDocumentation.cs -- Documents/api
+```
+
+## What the gate requires
+
+Three things fail the check, locally and in CI:
+
+- **A broken link or an unresolved reference.** DocFX runs with every warning treated as an error.
+- **An undocumented public declaration or a wrong `cref`.** The API project compiles its XML documentation, and the
+ build treats the compiler's documentation warnings as errors.
+- **Any undocumented item of the reference.** `CheckDocumentation.cs` reads the metadata DocFX generated and fails
+ when a namespace, type or member has no summary, internal and private members included.
+
+A namespace has no declaration that could hold a comment. Its description is a file in `namespaces/`, named after
+the namespace.
diff --git a/Documents/api/namespaces/Progmasoft.Tala.Website.Api.md b/Documents/api/namespaces/Progmasoft.Tala.Website.Api.md
new file mode 100644
index 0000000..de90b81
--- /dev/null
+++ b/Documents/api/namespaces/Progmasoft.Tala.Website.Api.md
@@ -0,0 +1,10 @@
+---
+uid: Progmasoft.Tala.Website.Api
+summary: *content
+---
+
+<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> -->
+<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 -->
+
+The API of the language website: the site's own public state at `/api/v1/site` and a liveness probe at `/health`.
+It has no account, package, publication or registry endpoint.
diff --git a/Documents/api/packages.lock.json b/Documents/api/packages.lock.json
new file mode 100644
index 0000000..0f57a57
--- /dev/null
+++ b/Documents/api/packages.lock.json
@@ -0,0 +1,35 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net10.0": {
+ "Microsoft.DotNet.ILCompiler": {
+ "type": "Direct",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g=="
+ },
+ "Microsoft.NET.ILLink.Tasks": {
+ "type": "Direct",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
+ }
+ },
+ "net10.0/win-x64": {
+ "Microsoft.DotNet.ILCompiler": {
+ "type": "Direct",
+ "requested": "[10.0.10, )",
+ "resolved": "10.0.10",
+ "contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==",
+ "dependencies": {
+ "runtime.win-x64.Microsoft.DotNet.ILCompiler": "10.0.10"
+ }
+ },
+ "runtime.win-x64.Microsoft.DotNet.ILCompiler": {
+ "type": "Transitive",
+ "resolved": "10.0.10",
+ "contentHash": "e5P00ObvdmAl49KkvGvqDLE5+EXu/IdXCDsLghgKFXKrNybyxMmH12HuGrsO7LTIGjXI58tyN3Hwxwr/MR0d8A=="
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Documents/api/toc.yml b/Documents/api/toc.yml
new file mode 100644
index 0000000..cf49dc1
--- /dev/null
+++ b/Documents/api/toc.yml
@@ -0,0 +1,7 @@
+# SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+# SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1
+
+- name: Overview
+ href: index.md
+- name: Reference
+ href: reference/