Skip to content

feat: add generated experiments reference (DOCS-139) - #29311

Closed
nickvigilante wants to merge 2 commits into
mainfrom
vigilante/docs-139-wave-3-build-feature-flags-experiments-reference
Closed

nickvigilante wants to merge 2 commits into
mainfrom
vigilante/docs-139-wave-3-build-feature-flags-experiments-reference

Conversation

@nickvigilante

Copy link
Copy Markdown
Contributor

Summary

Coder declares its experiments in codersdk with a key, a display name, and a one-line comment explaining what each does, but the docs never listed them. A reader who met --experiments in a guide had no way to learn which values it accepts short of reading the source.

scripts/experimentsdocgen renders docs/reference/experiments.md from codersdk.ExperimentsKnown. The per-experiment descriptions exist only as trailing comments on the Experiment constants, so the generator reads them from the syntax tree and takes the keys, display names, and wildcard-safe set from the package at run time. Generation fails when a known experiment has no description, rather than publishing a blank cell. Wired into make gen, so the page cannot drift.

Two corrections found while building it

  1. --experiments=* does not do what the docs said. docs/install/releases/feature-stages.md described it as "enable all early access features". coderd.ReadExperiments expands the wildcard to codersdk.ExperimentsSafe, which is currently an empty slice, so the wildcard enables nothing at all. The guide now states the real behavior and links to the reference.
  2. ExperimentMCPToolSearch had no DisplayName case, so it fell through to the title-casing default and rendered as "Mcp Tool Search". Added the case, matching the existing ExperimentMCPServerHTTP precedent. This also affects anywhere the display name surfaces in the product.

Verification

  • go test ./scripts/experimentsdocgen passes: comment extraction (trailing, doc-comment fallback, bare constant, non-experiment constants ignored), the failure path when a file declares no experiments, the failure path when a known experiment lacks a description, and a guard that every entry in ExperimentsKnown appears on the rendered page with its display name.
  • go test ./codersdk ./coderd for the experiment-related tests passes.
  • Vale, markdownlint, markdown-table-formatter, and make lint/emdash clean on both changed pages. Full make pre-commit passed, including the regenerated typesGenerated.ts and CLI golden files being unaffected.
Scope notes

Part of the DOCS Automated reference documentation project. Picked as the next generator because the experiment metadata is small, fully declarative, and needs no new product-side registry.

Two things deliberately left alone:

  • Descriptions are published verbatim from the constant comments. Two read as internal notes rather than user documentation ("This isn't used for anything." for example, and the auto-fill parameters comment, which explains a team decision rather than the feature). Rewriting them means editing the source comments and asserting behavior I have not verified, so it belongs in its own change. Follow-up filed.
  • Experiment stage and age are not rendered. Nothing in the source tracks when an experiment entered the stage or where it sits on the path to beta, so there is no truthful way to generate it today.

Generated with Coder Agents on behalf of @nickvigilante. Linear: DOCS-139

Coder declares its experiments in codersdk with a key, a display name, and a
one-line comment explaining what each one does, but the docs never listed
them. A reader who saw --experiments in a guide had no way to learn which
values it accepts short of reading the source.

Add scripts/experimentsdocgen, which renders docs/reference/experiments.md
from codersdk.ExperimentsKnown. The descriptions exist only as trailing
comments on the Experiment constants, so the generator reads them from the
syntax tree and takes everything else from the package at run time.
Generation fails when a known experiment has no description rather than
publishing a blank cell. Wire it into make gen so the page cannot drift.

Correct the feature stages guide while here: it told readers that
--experiments=* enables all early access features. ReadExperiments expands
the wildcard to ExperimentsSafe, which is currently empty, so the wildcard
enables nothing. The guide now says so and links to the new reference.

Give ExperimentMCPToolSearch a DisplayName case as well. Without one it fell
through to the title-casing default and rendered as "Mcp Tool Search".

Fixes DOCS-139
@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@linear-code

linear-code Bot commented Sep 14, 2026

Copy link
Copy Markdown

DOCS-139

@coder-agents-review

coder-agents-review Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Chat: Review in progress (16/16 reviewers complete) | View chat
Requested: 2026-09-14 22:13 UTC by @nickvigilante

deep-review v0.9.0 | Round 2 | f5be7ae..553b35a

Last posted: Round 2, 20 findings (1 P2, 9 P3, 5 Nit, 5 Note), COMMENT. Review

Finding inventory

Finding inventory: PR #29311

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (553b35a) scripts/experimentsdocgen/main.go:168 Missing description fails gen but missing DisplayName case ships a title-cased guess silently R1 Knov P2, Meruem P3, Mafuuu Note Yes
CRF-2 P3 Author fixed (553b35a) scripts/experimentsdocgen/main_test.go:114 Display-name assertion is tautological; cannot fail on name correctness R1 Meruem P3, Bisky Note, Netero Note Yes
CRF-3 P3 Deferred (DOCS-905) docs/reference/experiments.md:35 Internal maintainer notes ship as user-facing descriptions; deferral has no ticket R1 Netero/Mafuuu/Pariston/Leorio/Ryosuke/Chopper/Knov/Mafu-san P3 Yes
CRF-4 P3 Author fixed (553b35a) docs/install/releases/feature-stages.md:50 Hand-written wildcard note restates a fact the generated page derives; drifts when ExperimentsSafe populated R1 Hisoka P3, Ryosuke P3, Knov P3 Yes
CRF-5 P3 Author fixed (553b35a) scripts/experimentsdocgen/main.go:168 Table cells interpolate desc/display name without escaping ` ` R1 Hisoka P3, Mafuuu P3, Meruem/Ryosuke/Pariston Note
CRF-6 P3 Author fixed (553b35a) scripts/experimentsdocgen/main.go:147 Non-empty ExperimentsSafe branch has 0% coverage; untestable without mutating a global R1 Bisky P3, Chopper P3, Hisoka Note Yes
CRF-7 P3 Author fixed (553b35a) scripts/experimentsdocgen/main.go:133 Doc-comment fallback publishes the Go identifier into user docs; fails silently R1 Chopper P3, Netero/Knov/Meruem Note Yes
CRF-8 Note Deferred (DOCS-907) scripts/experimentsdocgen/main.go:76 Structural alternative: a Description() method beside DisplayName() removes AST coupling and the internal-note leak R1 Pariston, Ryosuke Yes
CRF-9 Nit Author fixed (553b35a) scripts/experimentsdocgen/main.go:181 sentence uppercases first byte not rune; reimplements existing UTF-8-safe util.Capitalize R1 Robin Nit, Hisoka Nit, Netero/Mafuuu/Knov Note Yes
CRF-10 Note Author fixed (553b35a) scripts/experimentsdocgen/main.go:100 ast.Inspect with always-true callback; ast.Preorder (Go 1.23) is the plain-walk form R1 Ging-go Yes
CRF-11 Nit Author fixed (553b35a) scripts/experimentsdocgen/main.go:90 readDescriptions doc repeats the AST rationale already in the package doc R1 Gon Yes
CRF-12 Nit Author fixed (553b35a) scripts/experimentsdocgen/main.go:129 commentText returns a formatted sentence, not raw comment text; name misleads R1 Gon Yes
CRF-13 Note Author fixed (553b35a) scripts/experimentsdocgen/main.go:40 intro hardcodes example/workspace-usage keys inside a drift-prevention generator R1 Gon, Ryosuke Yes
CRF-14 Nit Author fixed (553b35a) scripts/experimentsdocgen/main_test.go:44 wrapped test case name implies multi-line but comment is single-line R1 Bisky Yes
CRF-15 Note Author contested; panel closed R2 (10/10 accept) scripts/experimentsdocgen/main.go:171 Table sorted by key, so display-name (first) column is not alphabetical R1 Chopper Yes
CRF-16 Note Open scripts/experimentsdocgen/main.go:129 readDisplayNames re-parses the source and re-walks Experiment ValueSpecs already extracted by readDescriptions R2 Netero Yes
CRF-17 P3 Open scripts/experimentsdocgen/main_test.go:94 No test exercises readDisplayNames' negative path; a lenient readDisplayNames would pass both existing tests R2 Bisky Yes
CRF-18 P3 Open scripts/experimentsdocgen/main_test.go:135 TestRenderFailsOnMissingDescription not isolated (nil displayNames), passes via display-name guard; both failure tests assert only err!=nil R2 Chopper Yes
CRF-19 P3 Open scripts/experimentsdocgen/main.go:224 Display-name gate checks case presence, not authored non-empty content; empty-return case ships a blank cell, and the walk matches any case-clause mention R2 Knov P3, Hisoka Note Yes
CRF-20 P3 Open scripts/experimentsdocgen/main.go:217 Page completeness rests on unenforced ExperimentsKnown; a declared constant omitted from it is silently absent yet still user-enableable R2 Meruem (Note; orchestrator P3) Yes
CRF-21 Nit Open scripts/experimentsdocgen/main.go:129 readDisplayNames returns a presence map (always true), misnamed and undocumented; should be a named set (map[string]struct{}) R2 Mafuuu/Gon/Leorio/Zoro/Knov Nit Yes

Contested and acknowledged

CRF-15 (Note, main.go:171) - table sorted by key, not display name

  • Finding: Rows sorted by experiment key, so the leading display-name column is not alphabetical.
  • Author defense (R2, PRRC_kwDOGkVX1s7vBNi6): 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.
  • Status: Contested, no panel disposition yet. Reviewers judge in R2.
  • Panel closure (R2, 10/10 accept): Hisoka, Mafuuu, Mafu-san, Pariston, Ryosuke, Chopper, Meruem, Zoro, Knov, and Leorio each independently verified against the rendered page that only the display-name column is unsorted while the Key column (the string an operator types into --experiments) stays alphabetical, and judged the bounded friction at 13 rows acceptable in exchange for page/command-input consistency.

Law analysis

Not run. Effective additions 475 (R2), below the 1000 threshold.

Round log

Round 1

Netero first pass: 1 P3, 2 Notes, mechanical floor clean (P3 and below), panel proceeded. Panel of 14 (bisky, hisoka, mafu-san, mafuuu, pariston, gon, leorio, ging-go, ryosuke, robin, komugi, chopper + wildcards knov, meruem). 1 P2, 6 P3, 4 Note, 4 Nit. Komugi: no findings. Event COMMENT (no P0-P1). Reviewed against f5be7ae..f6019c7.

Round 2

Churn guard: PROCEED. 12 addressed (553b35a), 2 deferred with tickets (CRF-3 DOCS-905, CRF-8 DOCS-907), 1 contested (CRF-15). Effective additions grew 344 -> 475. Reviewed against f6019c7..553b35a.

Round 2 panel

Netero (advisory, post-panel) verified R1 fixes and added CRF-16. Panel of 14 (bisky, hisoka, mafu-san, mafuuu, pariston, gon, leorio, ging-go, ryosuke, meruem, chopper, komugi + wildcards zoro, knov). All R1 fixes verified genuine and non-tautological. CRF-15 closed by panel (10/10 accept). New: 4 P3 (CRF-17, CRF-18, CRF-19, CRF-20), 1 Note (CRF-16), 1 convergent Nit (CRF-21). Orchestrator verified the Meruem/Pariston contradiction on ReadExperiments against coderd/coderd.go: unknown-but-named experiments are appended (logged "ignoring" but still enabled), so CRF-20 is a reachable gap; set P3. Event COMMENT (no P0-P1).

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@coder-agents-review coder-agents-review Bot left a comment

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.

This is a clean, well-scoped generator. The design has three sources each owning one slice of the page (experiment set and display names from the codersdk package at run time, descriptions from the syntax tree, page metadata from the manifest), it fails generation when a known experiment has no description rather than publishing a blank cell, and it is wired into make gen so the list cannot drift from ExperimentsKnown. The two corrections found while building it both check out against code: the wildcard genuinely enables nothing (ExperimentsSafe is empty, and ReadExperiments expands * to it), and the ExperimentMCPToolSearch display-name case is a real fix. Komugi found the output fully deterministic and the failure-path tests are genuine, not vacuous.

No P0 or P1 findings. Counts: 1 P2, 6 P3, 4 Notes, 4 Nits.

The findings cluster around one theme the reviewers reached from several angles: the generator republishes data it did not author and does not fully guard. The description path fails loud on a missing comment, but the display-name path fails open (CRF-1), and the test that names "display name" asserts nothing about it (CRF-2), so the exact bug this PR fixed by hand can recur silently. Two rows publish maintainer notes as user documentation (CRF-3), and the disclosed follow-up links no ticket, which needs a human decision: link the ticket, fix the two constant comments in this PR, or explicitly accept shipping them. Pariston and Ryosuke converged on the structural fix (CRF-8): a Description() method beside DisplayName() would make the description authored-and-reviewed prose instead of scavenged comments, and would close CRF-1, CRF-3, and CRF-7 in one move. Worth weighing before this generator becomes the template for the other DOCS reference pages.

One process note: the PR fixes doc drift in the reference page and reintroduces it in the hand-written feature-stages.md, which hardcodes the same wildcard fact the generated page now derives (CRF-4). That is the same class of bug this PR exists to remove.

As Ryosuke put it: "Right now the description is riding in the trunk as cargo the code never meant to carry to the reader. Give it a seat."

🤖 This review was automatically generated with Coder Agents.

Comment thread scripts/experimentsdocgen/main.go Outdated
if !ok {
return "", xerrors.Errorf("experiment %q has no description comment on its constant", key)
}
_, _ = fmt.Fprintf(&b, "| %s | `%s` | %s |\n", exp.DisplayName(), key, desc)

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.

P2 [CRF-1] Missing descriptions fail generation, but a missing DisplayName case renders a title-cased guess and ships silently, so the generator does not prevent the exact bug this PR fixed by hand. (Knov P2, Meruem P3, Mafuuu Note)

render enforces one half of the metadata contract. A known experiment with no description returns an error at line 166 and generation fails loud. A known experiment with no case in DisplayName() falls through to the default at codersdk/deployment.go:5248, which title-cases the key (mcp-tool-search -> "Mcp Tool Search", and underscore keys are worse: nats_pubsub -> "Nats_pubsub"). render writes that verbatim and succeeds.

This is not hypothetical: correction #2 in the PR body is precisely this failure for ExperimentMCPToolSearch, which proves the class is live and recurring. TestRenderCoversEveryKnownExperiment does not catch it (see CRF-2). The description path is fail-closed; the display-name path is fail-open for the same class of omission.

Make the two paths symmetric: have render reject an experiment whose DisplayName() equals the default title-casing of its key, or store display names beside descriptions so both are enforced by the same error. The higher severity reflects that the automation added here leaves open the exact hole it was built to close.

🤖

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. Generation now parses explicit DisplayName switch cases and fails when a known experiment lacks one. 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. render now rejects any known experiment absent from readDisplayNames, and the display-name path fails closed like the description path. Two residual gaps on this same guard are raised as new findings (CRF-19: presence checked, not authored non-empty content; CRF-17: the negative path is untested).

🤖

Comment thread scripts/experimentsdocgen/main_test.go Outdated
if !strings.Contains(page, "`"+string(exp)+"`") {
t.Errorf("generated page is missing experiment %q", exp)
}
if !strings.Contains(page, exp.DisplayName()) {

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-2] The display-name assertion is tautological and cannot fail on the quality it appears to guard. (Meruem P3, Bisky Note, Netero Note)

TestRenderCoversEveryKnownExperiment asserts strings.Contains(page, exp.DisplayName()) for every known experiment. Since render builds each row with exp.DisplayName(), the page contains that exact string by construction whenever the row renders, and a row that fails to render already errors via the missing-description path. The check only restates that the description guard fired; it gives zero coverage of display-name correctness. This is why the fail-open default in CRF-1 passes CI.

Fix: assert that no rendered display name equals the title-cased-key fallback, or replace this line with a golden-file comparison of the full table so both key and display name are pinned.

🤖

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. Replaced the tautological assertion with direct explicit-case coverage and a missing-display-name failure test. 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. The tautological assertion is gone; TestRenderCoversEveryKnownExperiment now asserts the key literal and display-name authorship moved to TestReadDisplayNames against the real source.

🤖

|---------------------------------|-------------------------------|------------------------------------------------------------------------------------------|
| 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.

🤖

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.

🤖

Comment thread scripts/experimentsdocgen/main.go Outdated
if !ok {
return "", xerrors.Errorf("experiment %q has no description comment on its constant", key)
}
_, _ = fmt.Fprintf(&b, "| %s | `%s` | %s |\n", exp.DisplayName(), key, desc)

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-5] Descriptions and display names are interpolated into the Markdown table without escaping |, so a comment containing a pipe corrupts the row. (Hisoka P3, Mafuuu P3, Meruem/Ryosuke/Pariston Note)

fmt.Fprintf(&b, "| %s | \%s` | %s |\n", exp.DisplayName(), key, desc)trusts that no cell contains the delimiter.sentencecollapses newlines viastrings.Fields, so multi-line comments are safe, but a literal |survives. No current comment carries one, so this is latent. The repo already knows the fix and applies it in sibling generators:strings.ReplaceAll(value, "|", "\|")inscripts/gotestsummary/main.go, and scripts/clidocgen/gen.gojoins with|. This generator diverges from that pattern. markdown-table-formatterruns afterward, so the worst case may be a gen-time break rather than a shipped defect, but the established pattern is to escape. Escape|indesc` and defensively in the display name before writing the row.

🤖

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. Table-cell values now escape Markdown pipe delimiters, with coverage. 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. markdownCell escapes | on both display name and description, covered by TestRenderEscapesMarkdownTableCells.

🤖

Comment thread scripts/experimentsdocgen/main.go Outdated

// readDescriptions returns the trailing comment on each Experiment constant,
// keyed by the experiment's string value. The comments are the only
// description the codebase carries for an experiment, and they are not

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-11] The readDescriptions doc repeats the AST-parsing rationale already stated in the package doc. (Gon)

The package doc already says the descriptions live only as trailing comments and are read from the syntax tree; the function doc restates the same why. Put the rationale once at the package doc and let the function doc state only its contract: readDescriptions returns the trailing comment on each Experiment constant, keyed by the experiment's string value.

🤖

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. Reduced readDescriptions to its contract; the package comment retains the AST rationale. 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. readDescriptions is trimmed to its contract and the AST rationale stays on the package doc.

🤖

Comment thread scripts/experimentsdocgen/main.go Outdated

// commentText prefers the trailing comment on the constant's own line and
// falls back to the doc comment above it.
func commentText(spec *ast.ValueSpec) string {

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-12] commentText returns a formatted sentence, not raw comment text, so the name misleads. (Gon)

The function runs its result through sentence(), so callers get capitalized, single-line, period-terminated text. The name promises the raw comment. commentSentence, or moving the sentence() call to the caller, makes the return value match the name.

🤖

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. Renamed commentText to commentSentence. 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. Renamed to commentSentence. Note the sibling readDisplayNames introduced the same name-vs-return mismatch this rename fixed; raised as CRF-21.

🤖

Comment thread scripts/experimentsdocgen/main.go Outdated
Enable one by passing its key to ` + "`coder server`" + `:

` + "```shell" + `
coder server --experiments=example,workspace-usage

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-13] The intro hardcodes example and workspace-usage as sample keys inside a generator whose purpose is to prevent doc drift. (Gon, Ryosuke)

Both are real experiment keys today, so the example is correct now. If either is renamed or removed, the generated intro shows an invalid key while every other line on the page stays source-derived and correct. Not worth a guard now; noting so a future key removal remembers this line.

🤖

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. Replaced the hard-coded sample keys with . 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. The intro uses a generic placeholder.

🤖

Comment thread scripts/experimentsdocgen/main_test.go Outdated

const (
ExperimentExample Experiment = "example" // This isn't used for anything.
ExperimentWrapped Experiment = "wrapped" // Spans a comment

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-14] The wrapped case in TestReadDescriptions claims to test comment wrapping but its comment is a single trailing line. (Bisky)

ExperimentWrapped ... // Spans a comment is one trailing line; a trailing comment cannot span lines. The multi-line collapse it implies is already tested in TestSentence ("collapses a wrapped comment"). Rename it to reflect what it checks (a plain trailing comment) or drop it as redundant with the example row.

🤖

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. Renamed the case to trailing and made it describe a plain trailing comment. 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. The case is renamed to trailing and describes a plain trailing comment.

🤖

_, _ = fmt.Fprintf(&b, "| %s | `%s` | %s |\n", exp.DisplayName(), key, 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.

🤖

@nickvigilante

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

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.

Round 2 is a strong response. Every round-1 finding was addressed, deferred with a ticket, or contested with a real argument, and the fixes are root-cause work rather than symptom patches. Multiple reviewers independently verified the important ones against the code and non-tautological tests: the display-name column now fails closed like the description column (CRF-1), the tautological guard test is gone (CRF-2), pipes are escaped (CRF-5), the safe-list branch is reachable and covered (CRF-6), the doc-comment fallback is gone (CRF-7), sentence uses the shared UTF-8-safe helper (CRF-9), and ast.Preorder replaces the no-op ast.Inspect (CRF-10). The CRF-1 fix is also better than my round-1 suggestion: comparing DisplayName() to the default title-casing would have false-positived on names like "Chat Advisor" that equal their own default, so reading the AST for an explicit case is the correct mechanism. CRF-15 (key-ordered table) is closed: the whole panel judged the author's defense sound, since the Key column an operator actually types stays sorted. CRF-3 (DOCS-905) and CRF-8 (DOCS-907) are accepted as ticketed deferrals.

No P0/P1/P2. New this round: 4 P3, 1 Note, 1 convergent Nit.

The new findings share one theme: the CRF-1 fix hardened the display-name path but the guard is narrower than the description guard it mirrors, and its negative path is not pinned by a test. The gate checks that a case clause mentions the constant, not that an authored, non-empty name is returned (CRF-19), and no test exercises readDisplayNames on a source where a constant lacks its case, so a lenient regression would pass with a green board (CRF-17). CRF-18 is the same shape on the render failure tests: TestRenderFailsOnMissingDescription passes a nil display-name map, so it still errors via the wrong guard even if the description guard were deleted. Separately, CRF-20 is a genuine completeness gap: the page renders only ExperimentsKnown, whose membership is guaranteed by a comment, not a check. I verified against coderd/coderd.go that ReadExperiments appends an unknown-but-named experiment anyway (it logs "ignoring" but still enables it), so a constant left out of ExperimentsKnown is user-enableable yet silently absent from the page. That is the exact drift the generator advertises it prevents, and readDisplayNames already parses the declared set, so the check is nearly free.

Process note: the fix commit 553b35a changed six distinct things (fail-closed display name, pipe escaping, commentSentence rename plus dropped fallback, Capitalize, ast.Preorder) under the subject "harden generated metadata checks" with an empty body. A two-line body listing the closed findings would help a future bisect; the reasoning currently lives only in the PR thread.

As Bisky put it: "the one stone that guards the whole ring was never cut."

🤖 This review was automatically generated with Coder Agents.

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.

🤖

}
}

func TestReadDisplayNames(t *testing.T) {

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-17] No test exercises readDisplayNames on a source where a constant lacks its DisplayName case, so the CRF-1 fail-closed guarantee rests on an untested negative path. (Bisky)

The two halves of the guarantee are each tested but never meet. TestReadDisplayNames runs against the real file where every experiment already has a case, so displayNames[key] is true for all of them and the false path never fires. TestRenderFailsOnMissingDisplayName hands render a hardcoded nil map, bypassing readDisplayNames entirely. If a refactor made readDisplayNames too lenient (mark every experiment constant true regardless of the switch), both existing tests would still pass and the fail-closed guarantee would silently die, resurrecting CRF-1 with a green board. Give readDisplayNames the same fixture treatment readDescriptions gets: two constants, one with a case and one without, assert the first is present and the second absent.

🤖

}
}

func TestRenderFailsOnMissingDescription(t *testing.T) {

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-18] TestRenderFailsOnMissingDescription stays green even if the missing-description guard is deleted, so it does not isolate the behavior it names. (Chopper)

The test passes descriptions={"unrelated":"x"} and displayNames=nil over the full ExperimentsKnown list, then asserts only err != nil. With a nil display-name map, every known key also fails the display-name guard. So if a refactor dropped or weakened the description guard, execution would fall through to the display-name guard, err would still be non-nil, and the test named "fails on missing description" would still pass while no longer testing missing descriptions. Give it a valid displayNames map so only the description guard can fire, and assert strings.Contains(err.Error(), "description"). The sibling TestRenderFailsOnMissingDisplayName is correctly isolated but also asserts only err != nil; add strings.Contains(err.Error(), "display name") so the two failure-path tests cannot satisfy each other's assertion.

🤖


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.)

🤖

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.

🤖

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.

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.

🤖

@nickvigilante

Copy link
Copy Markdown
Contributor Author

Closing this without merging.

This PR reimplements a design the repository already rejected. Commit b23d73d5 (#29113, 2026-09-09), "docs: retire the generated feature stage lists", removed both generated feature-stage indexes from docs/install/releases/feature-stages.md. Its commit message records that the decision, after discussion with David, was not to advertise either list, describing them as "drift surfaces that promise a completeness the docs cannot maintain." It names the superseded proposal #28949 — which generated the list from ExperimentsKnown with descriptions parsed from Go constant comments — and rejects it for making docs track "an internal, unstable surface," including the observation that some of those comments are developer notes rather than user copy.

That is precisely what this PR does, six days later. It also publishes six keys that appear nowhere else in the docs: ai-gateway-seat-exclusion, nats_pubsub, workspace-build-updates, workspace-capable-licensing, workspace-usage, and example.

The ticket predates that decision. I should have checked the history of the page I was linking into before building on it. My error, not the reviewers'.

What carries forward. The exercise did surface a real defect: docs/admin/integrations/oauth2-provider.md and docs/ai-coder/mcp-server.md instruct operators to enable an oauth2 experiment that is not in ExperimentsKnown. Tracked as DOCS-911.

The durable fix is a check rather than a page: validate that every experiment key already named in the docs exists in ExperimentsKnown. That catches the oauth2 class permanently, adds no public exposure, and respects the #29113 decision. DOCS-139 has been re-scoped to that, and will carry the missing ExperimentMCPToolSearch DisplayName() case, which is a genuine bug affecting the product UI as well as docs.

Thanks to the review panel — the findings were sound, and several of them are folded into the replacement work.


Generated with Coder Agents on behalf of @nickvigilante. Linear: DOCS-139

@github-actions github-actions Bot locked and limited conversation to collaborators Sep 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant