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

summaryrefslogtreecommitdiffhomepage
path: root/Documents
diff options
context:
space:
mode:
authorLeitwolf112026-10-15 18:35:17 +0300
committerLeitwolf112026-10-15 18:35:17 +0300
commit018696d64cdb4088c0dd502ceab8d8ed9e11543e (patch)
tree402412587d273cd488b941878f902d313b47fa41 /Documents
parenta460c4c7e922f37705b2be3f1fa7c789f514cd51 (diff)
downloadwebsite-018696d64cdb4088c0dd502ceab8d8ed9e11543e.tar.gz
Gate the API documentation with DocFX
The Kotlin backend checked its documentation with Dokka; the ASP.NET Core API had only the compiler warning for a public declaration without a comment. CI now builds the API reference with DocFX, every warning as an error, and Documents/api/CheckDocumentation.cs fails when a namespace, type or member of the reference has no description, internal and private members included. The setup is the one Progmaweb uses: the docfx tool pinned in .config/dotnet-tools.json, the configuration and the namespace description under Documents/api, and the generated reference ignored.
Diffstat (limited to 'Documents')
-rw-r--r--Documents/DEVELOPMENT.md6
-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
7 files changed, 245 insertions, 1 deletions
diff --git a/Documents/DEVELOPMENT.md b/Documents/DEVELOPMENT.md
index 835ae05..0bf7f55 100644
--- a/Documents/DEVELOPMENT.md
+++ b/Documents/DEVELOPMENT.md
@@ -41,6 +41,10 @@ dotnet run --project backend/Progmasoft.Tala.Website.Api # serves on 127.0.
```
- 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.
@@ -87,7 +91,7 @@ narrow window, and keyboard focus.
| Job | Enforces |
| --- | --- |
| `frontend` | `npm ci`, `npm audit` at moderate severity, type check, tests with coverage, format check, build |
-| `backend` | locked restore, build with warnings as errors, tests with coverage, and the published application |
+| `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 C# |
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/