Skip to content
Closed
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
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ endif
docs/admin/integrations/prometheus.md \
docs/admin/security/audit-logs.md \
docs/admin/setup/configuration-reference.md \
docs/reference/experiments.md \
docs/reference/cli/index.md \
coderd/apidoc/swagger.json \
coderd/rbac/object_gen.go \
Expand Down Expand Up @@ -160,6 +161,12 @@ _gen/bin/configdocgen: $(wildcard scripts/configdocgen/*.go) $(wildcard codersdk
@mkdir -p _gen/bin
go build -o $@ ./scripts/configdocgen

# experimentsdocgen reads the Experiment constants and their comments to
# produce the experiments reference page.
_gen/bin/experimentsdocgen: $(wildcard scripts/experimentsdocgen/*.go) $(wildcard scripts/docgenenv/*.go) $(wildcard codersdk/*.go) | _gen
@mkdir -p _gen/bin
go build -o $@ ./scripts/experimentsdocgen

_gen/bin/dbdump: $(wildcard coderd/database/gen/dump/*.go) $(DBDUMP_INPUTS) | _gen
@mkdir -p _gen/bin
go build -o $@ ./coderd/database/gen/dump
Expand Down Expand Up @@ -1016,6 +1023,7 @@ GEN_FILES := \
docs/reference/cli/index.md \
docs/admin/security/audit-logs.md \
docs/admin/setup/configuration-reference.md \
docs/reference/experiments.md \
coderd/apidoc/swagger.json \
docs/manifest.json \
provisioner/terraform/testdata/version \
Expand Down Expand Up @@ -1114,6 +1122,7 @@ gen/mark-fresh:
docs/reference/cli/index.md \
docs/admin/security/audit-logs.md \
docs/admin/setup/configuration-reference.md \
docs/reference/experiments.md \
coderd/apidoc/swagger.json \
docs/manifest.json \
site/e2e/provisionerGenerated.ts \
Expand Down Expand Up @@ -1356,6 +1365,13 @@ docs/admin/setup/configuration-reference.md: node_modules/.installed $(wildcard
pnpm exec markdown-table-formatter "$$tmpfile" && \
mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

docs/reference/experiments.md: node_modules/.installed $(wildcard scripts/experimentsdocgen/*.go) $(wildcard codersdk/*.go) docs/manifest.json _gen/bin/experimentsdocgen | _gen
tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && \
_gen/bin/experimentsdocgen --out="$$tmpfile" && \
pnpm exec markdownlint-cli2 --fix "$$tmpfile" && \
pnpm exec markdown-table-formatter "$$tmpfile" && \
mv "$$tmpfile" "$@" && rm -rf "$$tmpdir"

coderd/apidoc/.gen: \
node_modules/.installed \
scripts/apidocgen/node_modules/.installed \
Expand Down
2 changes: 2 additions & 0 deletions codersdk/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -5229,6 +5229,8 @@ func (e Experiment) DisplayName() string {
return "Workspace Usage Tracking"
case ExperimentMCPServerHTTP:
return "MCP HTTP Server Functionality"
case ExperimentMCPToolSearch:
return "MCP Tool Search"
case ExperimentWorkspaceBuildUpdates:
return "Workspace Build Updates Channel"
case ExperimentNATSPubsub:
Expand Down
8 changes: 7 additions & 1 deletion docs/install/releases/feature-stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ staging deployment.
Use the [Coder CLI](../../install/cli.md) `--experiments` flag to enable early
access features:

- Enable all early access features:
- Enable every experiment that is marked safe for general opt-in:

```sh
coder server --experiments=*
```

The wildcard does not enable every experiment. Coder enables only the ones it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-4] The hand-written wildcard note restates a live fact the generated page derives, so it drifts the moment ExperimentsSafe is populated. (Hisoka P3, Ryosuke P3, Knov P3)

The generated experiments.md computes the wildcard's effect from codersdk.ExperimentsSafe at run time: non-empty prints the safe list, empty prints "the wildcard enables nothing." This page hardcodes the empty case in prose: "no experiment currently carries that mark, so name the experiments you want instead." Nothing regenerates this file. The whole PR exists because hand-maintained experiment docs drift from source; this change fixes the drift in the reference page and reintroduces it here, on a security-relevant opt-in, with no mechanical guard. The day someone marks an experiment safe, make gen updates the reference and this guide asserts the opposite.

Keep the durable clause (the wildcard enables only the experiments Coder marks safe, not all) and drop the time-bound one, pointing at the generated reference, which already sits two lines down.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Accepted. Removed the time-bound empty-safe-set claim; the generated reference remains the source for the current wildcard set. Resolved in 553b35a.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in 553b35a. feature-stages.md dropped the time-bound empty-safe-set clause and links to the generated reference, which owns the dynamic wildcard fact.

🤖

marks as safe.

- Enable multiple early access features:

```sh
Expand All @@ -56,6 +59,9 @@ access features:
You can also use the `CODER_EXPERIMENTS`
[environment variable](../../admin/setup/index.md).

For every experiment a deployment can enable, and the key each one uses, refer
to the [experiments reference](../../reference/experiments.md).

You can opt-out of a feature after you've enabled it.

</details>
Expand Down
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2456,6 +2456,12 @@
}
]
},
{
"title": "Experiments",
"description": "Reference for the experimental features a Coder deployment can enable, with the key each one uses.",
"path": "./reference/experiments.md",
"icon_path": "./images/icons/puzzle.svg"
},
{
"title": "Agent API",
"description": "Reference for the Coder Agent API used by workspace agents to communicate with the server.",
Expand Down
45 changes: 45 additions & 0 deletions docs/reference/experiments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
# Code generated by make gen. DO NOT EDIT.
title: Experiments
description: "Reference for the experimental features a Coder deployment can enable, with the key each one uses."
icon_path: "./images/icons/puzzle.svg"
---

<!-- DO NOT EDIT | GENERATED CONTENT -->

An experiment is a feature that is not ready for production.
Experiments are disabled by default, are not guaranteed to be backward compatible, and can be renamed or removed at any time.

Enable one by passing its key to `coder server`:

```shell
coder server --experiments=<experiment-key>
```

The same keys work through the `CODER_EXPERIMENTS` environment variable.
For how experiments relate to beta and generally available features, refer to [Feature stages](../install/releases/feature-stages.md).

## The wildcard value

`--experiments=*` enables only the experiments Coder marks as safe for general opt-in, not every experiment on this page.

No experiment currently carries that mark, so the wildcard enables nothing.
Enable an experiment by naming its key.

## Available experiments

| Experiment | Key | Description |
|---------------------------------|-------------------------------|------------------------------------------------------------------------------------------|
| Agent Lifecycle Hooks | `agent-lifecycle-hooks` | Enables chat lifecycle hook webhooks for agent chats. |
| AI Gateway Seat Exclusion | `ai-gateway-seat-exclusion` | Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption. |
| Auto-fill Template Parameters | `auto-fill-parameters` | This should not be taken out of experiments until we have redesigned the feature. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-3] The generator publishes raw constant comments as the Description column, so an internal maintainer note ships as user documentation. (Ryosuke P3, Leorio P3, Chopper P3, Pariston P3, Mafuuu P3, Mafu-san P3, Netero P3, Knov P3)

The page's stated job is to tell an operator what each experiment does. For auto-fill-parameters the cell reads "This should not be taken out of experiments until we have redesigned the feature." That is a team decision about lifecycle; an operator deciding whether to enable the key learns nothing about the feature, which is the gap this page was built to close. The example row ("This isn't used for anything.") is a milder, arguably honest instance for a placeholder. The generator's guard fails on an absent comment but cannot tell a description from an internal note, so presence is enforced and fitness is not.

The PR body acknowledges this and says a follow-up was "filed," but no ticket is linked in the diff. Under the no-follow-up assumption the misleading rows ship as the official public description. This needs a human decision: link the tracking ticket, rewrite the two constant comments in codersdk/deployment.go in this PR (keeping any lifecycle note as a separate non-trailing comment so it stays out of the rendered cell), or explicitly accept the gap. See CRF-8 for the structural fix.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The missing-ticket premise is false: DOCS-905 exists, is linked to DOCS-139, and specifically tracks these two comments. I kept the wording follow-up there because behavior verification needs product ownership. 553b35a makes missing descriptions fail closed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Accepted as a ticketed deferral (DOCS-905). The two internal-note descriptions (auto-fill-parameters, example) still ship on the rendered page; tracking the wording change under product ownership is a reasonable disposition.

🤖

| Chat Advisor | `chat-advisor` | Enables the advisor tool for root agent chats. |
| Chat Virtual Desktop | `chat-virtual-desktop` | Enables virtual desktop and computer use provider for agents. |
| Example Experiment | `example` | This isn't used for anything. |
| MCP HTTP Server Functionality | `mcp-server-http` | Enables the MCP HTTP server functionality. |
| MCP Tool Search | `mcp-tool-search` | Defers MCP tool schemas behind a searchable catalog in agent chats. |
| NATS Pubsub | `nats_pubsub` | Enables embedded NATS pubsub. |
| SMTP and Webhook Notifications | `notifications` | Sends notifications via SMTP and webhooks following certain events. |
| Workspace Build Updates Channel | `workspace-build-updates` | Enables publishing workspace build updates to the all builds pubsub channel. |
| Workspace-Capable Licensing | `workspace-capable-licensing` | Counts only users holding the workspace-create permission toward the license seat limit. |
| Workspace Usage Tracking | `workspace-usage` | Enables the new workspace usage tracking. |
249 changes: 249 additions & 0 deletions scripts/experimentsdocgen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Command experimentsdocgen generates the experiments reference at
// docs/reference/experiments.md from codersdk.
//
// Two sources are combined. The set of experiments, their display names, and
// whether each one is safe to enable through the wildcard come from the
// codersdk package at run time. The per-experiment descriptions live as
// trailing comments on the Experiment constants, so they are read from the
// syntax tree of the file that declares them.
package main

import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"slices"
"strconv"
"strings"

"golang.org/x/xerrors"

utilstrings "github.com/coder/coder/v2/coderd/util/strings"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/scripts/atomicwrite"
"github.com/coder/coder/v2/scripts/docgenenv"
"github.com/coder/flog"
)

// routeTitles is the manifest breadcrumb to this page's route. The page
// mirrors that route's metadata into its front matter, so the manifest stays
// the single source of the title and description.
var routeTitles = []string{"Reference", "Experiments"}

const intro = `An experiment is a feature that is not ready for production.
Experiments are disabled by default, are not guaranteed to be backward compatible, and can be renamed or removed at any time.

Enable one by passing its key to ` + "`coder server`" + `:

` + "```shell" + `
coder server --experiments=<experiment-key>
` + "```" + `

The same keys work through the ` + "`CODER_EXPERIMENTS`" + ` environment variable.
For how experiments relate to beta and generally available features, refer to [Feature stages](../install/releases/feature-stages.md).

`

const wildcardSection = `## The wildcard value

` + "`--experiments=*`" + ` enables only the experiments Coder marks as safe for general opt-in, not every experiment on this page.

`

const tableSection = `## Available experiments

`

func main() {
manifestPath := flag.String("manifest", "docs/manifest.json", "path to the docs manifest that supplies the page metadata")
source := flag.String("source", "codersdk/deployment.go", "path to the Go file declaring the Experiment constants")
out := flag.String("out", "docs/reference/experiments.md", "path to write the generated reference page")
flag.Parse()

manifest, err := docgenenv.LoadManifest(*manifestPath)
if err != nil {
flog.Fatalf("%v", err)
}
route := manifest.FindRoute(routeTitles...)
if route == nil {
flog.Fatalf("manifest %q has no route %q", *manifestPath, strings.Join(routeTitles, " > "))
}

descriptions, err := readDescriptions(*source)
if err != nil {
flog.Fatalf("%v", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note [CRF-8] Structural alternative: a Description() method beside DisplayName() would remove the syntax-tree coupling and the internal-note leak in one move. (Pariston, Ryosuke)

Reusing the trailing comment is what forces the maintainer-note problem in CRF-3 and the doc-comment leak in CRF-7. DisplayName() already proves the codebase carries per-experiment strings as run-time-reachable code. A parallel Description() method would let the author write prose aimed at readers, keep the data reachable without parsing the syntax tree, be unit-testable directly, and give feature-stages.md one obvious thing to link. The author's scope note argues against a new product-side registry, but a method beside DisplayName() is not a registry; it is the pattern already in the file. Both approaches work; this one is worth weighing before this generator becomes the template for the other DOCS reference pages.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Considered. A Description() API would remove AST coupling, but changes codersdk product metadata and needs owner agreement. Filed DOCS-907 to evaluate it; current hardening remains AST-scoped.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Accepted as a ticketed deferral (DOCS-907). Agreed the Description() direction changes codersdk product metadata and needs owner agreement; keeping the current hardening AST-scoped is reasonable.

🤖

displayNames, err := readDisplayNames(*source)
if err != nil {
flog.Fatalf("%v", err)
}

content, err := render(*route, codersdk.ExperimentsKnown, codersdk.ExperimentsSafe, descriptions, displayNames)
if err != nil {
flog.Fatalf("render experiments reference: %v", err)
}
if err := atomicwrite.File(*out, []byte(content)); err != nil {
flog.Fatalf("write %s: %v", *out, err)
}
flog.Successf("wrote %s", *out)
}

// readDescriptions returns the trailing comment on each Experiment constant,
// keyed by the experiment's string value.
func readDescriptions(path string) (map[string]string, error) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, xerrors.Errorf("parse %q: %w", path, err)
}

descriptions := map[string]string{}
for node := range ast.Preorder(file) {
spec, ok := node.(*ast.ValueSpec)
if !ok {
continue
}
ident, ok := spec.Type.(*ast.Ident)
if !ok || ident.Name != "Experiment" || len(spec.Values) != 1 {
continue
}
lit, ok := spec.Values[0].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
continue
}
value, err := strconv.Unquote(lit.Value)
if err != nil {
continue
}
descriptions[value] = commentSentence(spec)
}

if len(descriptions) == 0 {
return nil, xerrors.Errorf("no Experiment constants found in %q", path)
}
return descriptions, nil
}

func readDisplayNames(path string) (map[string]bool, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit [CRF-21] readDisplayNames returns a presence map whose value is always true, so its name and its render parameter mismodel what the value is. (Mafuuu, Gon, Leorio, Zoro, Knov)

The function returns map[string]bool keyed by experiment value, true when the constant has an explicit case in DisplayName; it carries no display-name strings. The real name comes from exp.DisplayName() at render, which uses this map only as a presence check (if !displayNames[key]). A reader meeting it beside descriptions map[string]string in the render signature reasonably expects displayNames[key] to yield the name and instead gets a bool. This is the same class as the CRF-12 rename (commentText -> commentSentence) the author just fixed one function over. Rename to experimentsWithDisplayName / explicitDisplayNames, use map[string]struct{} since the bool value is never false, and add the one-line doc its sibling readDescriptions already carries.

🤖

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note [CRF-16] readDisplayNames re-parses the same source file and re-walks its Experiment ValueSpecs, duplicating the constant extraction readDescriptions already does. (Netero)

Both functions call parser.ParseFile on *source and iterate ast.Preorder to map Experiment constants (readDescriptions builds value->comment, readDisplayNames builds name->value). The extraction predicate is repeated. Consequence is low: the generator runs once at make gen and a divergent constant form fails loudly rather than emitting wrong output. Parsing once and passing the *ast.File to both extractors would remove the duplicated walk; the clean removal is the DOCS-907 structural change, so this is noted for maintenance, not blocking.

🤖

fset := token.NewFileSet()
file, err := parser.ParseFile(fset, path, nil, 0)
if err != nil {
return nil, xerrors.Errorf("parse %q: %w", path, err)
}

constants := map[string]string{}
for node := range ast.Preorder(file) {
spec, ok := node.(*ast.ValueSpec)
if !ok || len(spec.Names) != 1 || len(spec.Values) != 1 {
continue
}
ident, ok := spec.Type.(*ast.Ident)
if !ok || ident.Name != "Experiment" {
continue
}
lit, ok := spec.Values[0].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
continue
}
value, err := strconv.Unquote(lit.Value)
if err != nil {
continue
}
constants[spec.Names[0].Name] = value
}

displayNames := map[string]bool{}
for node := range ast.Preorder(file) {
decl, ok := node.(*ast.FuncDecl)
if !ok || decl.Name.Name != "DisplayName" || decl.Recv == nil || len(decl.Recv.List) != 1 {
continue
}
receiver, ok := decl.Recv.List[0].Type.(*ast.Ident)
if !ok || receiver.Name != "Experiment" {
continue
}
for node := range ast.Preorder(decl.Body) {
clause, ok := node.(*ast.CaseClause)
if !ok {
continue
}
for _, expr := range clause.List {
ident, ok := expr.(*ast.Ident)
if !ok {
continue
}
if value, ok := constants[ident.Name]; ok {
displayNames[value] = true
}
}
}
}

return displayNames, nil
}

func commentSentence(spec *ast.ValueSpec) string {
if spec.Comment == nil {
return ""
}
return sentence(spec.Comment.Text())
}

func render(route docgenenv.Route, known, safe codersdk.Experiments, descriptions map[string]string, displayNames map[string]bool) (string, error) {
var b strings.Builder
// The front matter title renders as the page heading, so the body starts
// at the intro and its sections begin at level two.
_, _ = b.WriteString(docgenenv.GeneratedHeader(route))
_, _ = b.WriteString(intro)
_, _ = b.WriteString(wildcardSection)

if len(safe) > 0 {
_, _ = b.WriteString("These experiments are safe to enable with the wildcard:\n\n")
for _, exp := range safe {
_, _ = fmt.Fprintf(&b, "- `%s`\n", exp)
}
_, _ = b.WriteString("\n")
} else {
_, _ = b.WriteString("No experiment currently carries that mark, so the wildcard enables nothing.\nEnable an experiment by naming its key.\n\n")
}

_, _ = b.WriteString(tableSection)
_, _ = b.WriteString("| Experiment | Key | Description |\n|------------|-----|-------------|\n")

known = slices.Clone(known)
slices.Sort(known)
for _, exp := range known {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-20] The page's completeness rests on the hand-maintained ExperimentsKnown list, which the generator parses enough to verify but never does. (Meruem, raised as Note; orchestrator P3)

The page promises every experiment a deployment can enable, and render derives that set from known (codersdk.ExperimentsKnown), whose only completeness guarantee is the comment "ExperimentsKnown should include all experiments defined above." No test or lint enforces it. Failure mode: a developer adds an Experiment constant with a DisplayName case and a comment but forgets to append it to ExperimentsKnown. Generation still succeeds (it only iterates known), so the page silently omits the experiment. I verified this is reachable, not merely cosmetic: in coderd/coderd.go ReadExperiments logs "ignoring unknown experiment" for a name not in ExperimentsKnown but then appends it unconditionally, so the omitted experiment is still user-enableable via --experiments=<key>. That is exactly the drift this generator advertises it prevents. Rated P3 rather than Note because it defeats the headline "cannot drift" guarantee for a reachable surface; latent only because the current tree has no such gap. Nearly-free fix: readDisplayNames already builds the full set of declared Experiment values; return it (or a sibling) and have render fail when a declared constant is absent from known, so completeness falls out of the parse instead of resting on a comment. (Distinct from CRF-8/CRF-16, which are about how metadata is sourced.)

🤖

key := string(exp)
desc, ok := descriptions[key]
if !ok || desc == "" {
return "", xerrors.Errorf("experiment %q has no description comment on its constant", key)
}
displayName := exp.DisplayName()
if !displayNames[key] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-19] The display-name gate checks that an explicit case exists, not that it returns an authored non-empty name, so it is narrower than the description gate it mirrors. (Knov P3, Hisoka Note)

Two residual gaps on the CRF-1 fix:

  1. Empty return. The description gate fails on desc == "" (line 220), but the display-name gate only checks !displayNames[key] (line 224). The rendered value comes from exp.DisplayName(), which nothing checks. A future case ExperimentX: return "" satisfies displayNames["x"] == true, so render proceeds and ships a blank leading cell silently, the same class of silent bad metadata CRF-1 set out to prevent. The fix mirrors the description gate: if !displayNames[key] || displayName == "".

  2. Over-broad match. readDisplayNames walks the entire DisplayName FuncDecl body and marks a constant satisfied if it appears in any CaseClause.List. Today DisplayName is one flat switch, so intent and check coincide. If it ever grows a second switch, a guard clause, or a nested switch that mentions an experiment constant for any reason, that constant is marked "has a display name" and render stops failing for it, re-opening the CRF-1 regression. Scoping collection to the SwitchStmt whose tag is the receiver ident would make the check mean what it reads. Both are latent; the empty-return case would at least surface in the docs diff, the scope case would not.

🤖

return "", xerrors.Errorf("experiment %q has no explicit display name", key)
}
_, _ = fmt.Fprintf(&b, "| %s | `%s` | %s |\n", markdownCell(displayName), key, markdownCell(desc))
}

return strings.TrimRight(b.String(), "\n") + "\n", nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note [CRF-15] The table is sorted by experiment key, so the leading "Experiment" (display name) column is not alphabetical. (Chopper)

Rows are ordered by slices.Sort(known) on the key string, so a reader scanning the first column (e.g. "SMTP and Webhook Notifications" under the notifications key) will not find the display names in order. Sorting by key is deterministic and defensible; at 13 rows the cost is small. Worth knowing, not worth changing.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declined. Key order is the stable operator-facing identifier order and matches the page's command input; at 13 rows, a separate display-name sort does not improve lookup enough to outweigh that consistency.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Defense accepted; closing. The panel judged 10/10 that sorting by key keeps the Key column (what an operator types into --experiments) alphabetical, and the unsorted display-name column is bounded friction at 13 rows. No re-raise.

🤖

}

func markdownCell(value string) string {
return strings.ReplaceAll(value, "|", `\|`)
}

// sentence collapses a comment to one line, capitalizes it, and gives it
// terminal punctuation so it reads as a sentence in a table cell.
func sentence(s string) string {
s = strings.Join(strings.Fields(s), " ")
if s == "" {
return s
}
s = utilstrings.Capitalize(s)
if !strings.HasSuffix(s, ".") {
s += "."
}
return s
}
Loading
Loading