@agentsfleet/orly 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,14 +4,16 @@
4
4
  > any narrower guide such as `CHANGELOG_VOICE.md` or before editing documentation
5
5
  > about the dispatch model.
6
6
 
7
- These rules govern customer-facing `agentsfleet` documentation. They keep pages
7
+ These rules govern any documentation orly writes or renders. They keep pages
8
8
  short, accurate, and usable by a first-day reader. Compliance is binary. A
9
9
  rejection cites the Documentation (DOC) rule identifier that failed.
10
10
 
11
11
  ## Scope and precedence
12
12
 
13
- - **DOC-S1 — Published pages.** A standalone Markdown JSX (MDX) file listed in
14
- `docs.json` follows every applicable rule in this document.
13
+ - **DOC-S1 — Any page orly writes.** Every Markdown or Markdown JSX (MDX) file
14
+ orly writes, renders, or materialises follows every applicable rule in this
15
+ document. A published page listed in `docs.json`, a rule page installed into
16
+ a repository, a skill page, and a `README.md` are all in scope.
15
17
  - **DOC-S2 — Generated API pages.** OpenAPI source follows the reference-page
16
18
  intent through summaries, descriptions, parameters, examples, responses, and
17
19
  stable errors. Generated pages do not need MDX front matter.
@@ -26,6 +28,9 @@ rejection cites the Documentation (DOC) rule identifier that failed.
26
28
  - **DOC-S6 — Source wins.** Runtime code is the source of truth. OpenAPI and the
27
29
  command-line interface follow runtime behavior. Published pages follow all
28
30
  three.
31
+ - **DOC-S7 — Records stay as written.** A historical record keeps its original
32
+ wording. Spec records under `docs/v1/` and closed changelog entries are read,
33
+ never rewritten to a later rule.
29
34
 
30
35
  ## Required front matter
31
36
 
@@ -110,10 +115,36 @@ Cut these on sight. They read as machine prose and they hide the point.
110
115
  | Fake-profound kicker | a closing metaphor or mic-drop | End on the last concrete sentence. |
111
116
  | Summary recap | "In conclusion", "Overall" | Stop. The reader was just there. |
112
117
 
113
- Also banned, on top of DOC-05 and DOC-07: delve, foster, elevate, harness,
114
- supercharge, embark, ever-evolving, tapestry, realm, paradigm shift, game
115
- changer. Em dashes are not a rhythm crutch: none in short copy, at most two per
116
- long page, and only where they beat a comma or a period.
118
+ Also banned, on top of DOC-05 and DOC-07: delve, foster, elevate, supercharge,
119
+ embark, ever-evolving, tapestry, realm, paradigm shift, game changer, deep
120
+ dive, blazing fast, streamline, empower, journey, landscape, testament,
121
+ pivotal, notably, moreover, furthermore, myriad, plethora, aforementioned,
122
+ meticulous, boasts, cornerstone, best-in-class, industry-leading,
123
+ state-of-the-art, unparalleled, effortless, turnkey, battle-tested,
124
+ rock-solid, and innovative.
125
+
126
+ DOC-05 covers "utilize" as well as "utilise". The checker always enforced
127
+ both; this page named only one. A rule readers cannot read is a rule that
128
+ drifts.
129
+
130
+ "contract" and "phase" are banned too. That ban is not new here: the operating
131
+ model states it and names the replacements, so read it there rather than
132
+ twice. Use Prototype, Milestone, Workstream, Section, Dimension, and Batch for
133
+ the hierarchy, "stages" for lifecycle steps, and "rules" for what `AGENTS.md`
134
+ enforces. A real commercial agreement keeps "contract" only where no clearer
135
+ term exists.
136
+
137
+ "harness" is not banned. It is this product's own word for what orly installs,
138
+ and it reaches every consumer through the rendered `AGENTS.md`. A word the
139
+ product uses for itself is vocabulary, not slop.
140
+
141
+ Em dashes are not a rhythm crutch: none in short copy, at most two per long
142
+ page, and only where they beat a comma or a period. The rule corpus is exempt
143
+ from that budget — `dispatch/`, `core/`, `audits/`, `skills/`, `AGENTS.md`,
144
+ `AGENTS.orly.md`, `SOUL.md`, and any `RULES.md`. Those pages are dense
145
+ reference text where the dash separates a claim from its consequence. Measured
146
+ across the corpus the budget flagged 36 of 39 pages, which is a rule mis-fit,
147
+ not 36 defects. They keep every other rule.
117
148
 
118
149
  ## Content
119
150
 
@@ -9,12 +9,12 @@ regenerates this file and fails on any difference. To update it, run
9
9
  | `docs/LOGGING_STANDARD.md` | `LOG` → `logging.sh` | 10 | 13 | 26/26 | mechanical |
10
10
  | `docs/REST_API_DESIGN_GUIDELINES.md` | — | 0 | 0 | 0/74 | mechanical |
11
11
  | `docs/SCHEMA_CONVENTIONS.md` | — | 0 | 0 | 0/16 | mechanical |
12
- | `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/20 | latent |
12
+ | `docs/DOCUMENTATION_RULES.md` | — | 0 | 0 | 0/22 | latent |
13
13
  | `docs/LIFECYCLE_PATTERNS.md` | — | 0 | 0 | 0/22 | mechanical |
14
14
  | `docs/CHANGELOG_VOICE.md` | — | 0 | 0 | 0/3 | latent |
15
15
  | `docs/VERIFY_TIERS.md` | — | 0 | 0 | 0/4 | uncited |
16
16
  | `docs/greptile-learnings/RULES.md` | — | 0 | 0 | 0/113 | mechanical |
17
- | **corpus** | | | | **26/278** | |
17
+ | **corpus** | | | | **26/280** | |
18
18
 
19
19
  ## How to read this
20
20
 
@@ -0,0 +1,29 @@
1
+ # Slop fixture
2
+
3
+ This page is the fail fixture for the documentation-language check. Every
4
+ violation below is deliberate, and the test asserts the check finds each one.
5
+
6
+ We leverage a robust and seamless pipeline to execute the request, and the
7
+ result is a powerful experience that lets you delve into a realm of insight
8
+ without ever once thinking about the mechanics of the thing you are using.
9
+ This sentence exists to be long. This one too. And a fourth, to break DOC-03.
10
+
11
+ An em dash — in short copy — is over budget by itself.
12
+
13
+ ### Skipped level
14
+
15
+ The heading above jumps from H1 to H3.
16
+
17
+ # Second level-one heading
18
+
19
+ `make harness-verify` and `provision-env-1password` are command names, not
20
+ banned words. The check must leave both alone.
21
+
22
+ | leverage | robust | seamless |
23
+ |---|---|---|
24
+ | a table cell is exempt | so is this | and this |
25
+
26
+ ```bash
27
+ # a fenced block is exempt: execute the robust and seamless pipeline
28
+ orly gate verify
29
+ ```
package/llms.txt ADDED
@@ -0,0 +1,223 @@
1
+ # orly — agent instructions
2
+
3
+ > You are an autonomous coding agent. This file is the authoritative setup and
4
+ > operating guide for orly. Read it in full before running any command.
5
+ > Do not infer behaviour from README.md; that page is written for humans.
6
+
7
+ orly installs two things into a repository: rules you must read before editing,
8
+ and gate scripts wired into git hooks that fail the commit when a rule was
9
+ ignored. Hosts supported: Claude Code, Codex, OpenCode, Amp.
10
+
11
+ Package: `@agentsfleet/orly` (npm)
12
+ Source: https://github.com/agentsfleet/orly
13
+ License: MIT
14
+
15
+ ---
16
+
17
+ ## 1. Preconditions
18
+
19
+ Verify all of these before acting. If a check fails, stop and report it to the
20
+ user rather than working around it.
21
+
22
+ | Check | Command | Required |
23
+ |---|---|---|
24
+ | bun present | `bun --version` | ≥ 1.4.0 |
25
+ | git present | `git --version` | any |
26
+ | inside a repository | `git rev-parse --show-toplevel` | must succeed |
27
+ | working tree clean | `git status --porcelain` | should be empty |
28
+
29
+ Do not install bun, git, or any package manager yourself unless the user asks.
30
+
31
+ ---
32
+
33
+ ## 2. Install sequence
34
+
35
+ Run from the repository root. Execute these in order and stop at the first
36
+ non-zero exit.
37
+
38
+ ```bash
39
+ # 1. preview — writes nothing
40
+ bunx @agentsfleet/orly init --dry-run
41
+
42
+ # 2. install
43
+ bunx @agentsfleet/orly init
44
+
45
+ # 3. confirm installed state matches what orly would write today
46
+ bunx @agentsfleet/orly doctor
47
+ ```
48
+
49
+ Then commit the result. Teammates receive the rules on clone.
50
+
51
+ IMPORTANT: git does not clone hooks. Each checkout must run `orly init` once.
52
+ If a human reports that gates are not firing, this is the first thing to check.
53
+
54
+ ---
55
+
56
+ ## 3. What the install writes
57
+
58
+ | Path | Contents | Owner |
59
+ |---|---|---|
60
+ | `AGENTS.md` | your host auto-loads this | the user — never rewrite it |
61
+ | `AGENTS.orly.md` | generated rules: safety, dispatch router, lifecycle | orly — rewritten on update |
62
+ | `dispatch/*.md` | one rule page per kind of work | orly |
63
+ | `audits/*.sh` | the deterministic gates | orly |
64
+ | `docs/*.md` | the standards those rules cite | orly |
65
+ | `.claude/skills/`, `.agents/skills/`, `.opencode/skills/` | skills materialised per host | orly |
66
+ | `.githooks/` | pre-commit and pre-push, wired to `orly gate` | orly |
67
+ | `.oracle/orly.json` | packs, gate commands, install record | shared — edit the commands |
68
+
69
+ `orly init` seeds `.oracle/orly.json` with gate commands found in `Makefile` or
70
+ `package.json`. If commands are missing, ask the user which ones to add. Do not
71
+ invent check commands.
72
+
73
+ ---
74
+
75
+ ## 4. Read order before you edit anything
76
+
77
+ 1. `AGENTS.md` — the user's own rules. **These win on any conflict.**
78
+ 2. `AGENTS.orly.md` — the generated rules and the dispatch router.
79
+ 3. The `dispatch/*.md` page that matches the kind of file you are about to edit.
80
+ 4. Any `docs/*.md` standard those pages cite.
81
+
82
+ Re-read step 3 for each new file kind you touch in the same task.
83
+
84
+ ---
85
+
86
+ ## 5. The task lifecycle
87
+
88
+ The spec is a file on disk. Its directory is the status. Only the named stages
89
+ move it. `orly gate pr` reads where it ended up.
90
+
91
+ ```
92
+ docs/v1/pending/ spec written, committed on main
93
+ │
94
+ │ CHORE(open) branch cut, baseline recorded, NO CODE until it commits
95
+ ▼
96
+ docs/v1/active/ PLAN → EXECUTE → CONFORM → VERIFY → REVIEW → DOCUMENT → COMMIT
97
+ │
98
+ │ CHORE(close) session notes, orphan sweep
99
+ ▼
100
+ docs/v1/done/ orly gate pr green → Pull Request opens
101
+ ```
102
+
103
+ Rules for you:
104
+
105
+ - Write the spec yourself before any code exists.
106
+ - Do not write code before `CHORE(open)` has committed.
107
+ - Do not skip a stage. The hooks run gates on every commit and push.
108
+ - A red gate returns you to EXECUTE. Fix the cause; do not retry unchanged.
109
+
110
+ ---
111
+
112
+ ## 6. Failure decision table
113
+
114
+ | Situation | Do this |
115
+ |---|---|
116
+ | `orly gate` fails a check | return to EXECUTE, fix the cause, re-run `orly gate` |
117
+ | same gate fails twice on the same cause | stop, report the gate name and output to the user |
118
+ | `orly init` refuses a file | orly did not write that file. Report it. Do **not** pass `--force` on your own initiative |
119
+ | hooks not firing | run `git config core.hooksPath` and confirm `orly init` was run in this checkout |
120
+ | gate command missing from `.oracle/orly.json` | ask the user for the command; do not invent one |
121
+ | `orly gate pr` red and the failure is genuinely inapplicable | ask the user; only they authorise `orly override` |
122
+ | gstack `/review` unavailable | continue; orly records the skipped review in the PR notes |
123
+
124
+ Overrides are recorded as an empty commit that rides into the Pull Request:
125
+
126
+ ```bash
127
+ bunx @agentsfleet/orly override <criterion> --reason "<why>"
128
+ ```
129
+
130
+ Never record an override without an explicit instruction from the user, and
131
+ never with a placeholder reason.
132
+
133
+ ---
134
+
135
+ ## 7. Hard rules
136
+
137
+ - Never rewrite `AGENTS.md`. orly adds one delimited pointer block; leave the
138
+ rest alone.
139
+ - Never edit or delete files under `.githooks/` or `audits/` to make a gate pass.
140
+ - Never pass `--force` or `--no-hooks` unless the user asks for it by name.
141
+ - Never disable a check in `.oracle/orly.json` to get green.
142
+ - When the user's `AGENTS.md` and orly's rules disagree, the user's file wins.
143
+ - A refused run changes nothing. Report the refusal; do not retry with flags.
144
+
145
+ ---
146
+
147
+ ## 8. Command reference
148
+
149
+ | Command | Effect |
150
+ |---|---|
151
+ | `orly init` | write rules, gates, skills, and hooks |
152
+ | `orly init --dry-run` | print what would be written; change nothing |
153
+ | `orly update` | re-materialise at a newer engine version |
154
+ | `orly update --with <pack>` | add an opt-in pack, recorded for every clone |
155
+ | `orly gate` | run declared checks in order, stopping at the first failure |
156
+ | `orly gate pr` | the Pull Request criteria check |
157
+ | `orly override <criterion> --reason <why>` | record a gate exception (user-authorised only) |
158
+ | `orly doctor` | diff installed state against what orly would write today |
159
+
160
+ ---
161
+
162
+ ## 9. Packs
163
+
164
+ Nine language packs are selected from file extensions in the user's source.
165
+ orly scans four directories deep and skips `node_modules`, `target`, `.venv`,
166
+ and other dependency trees.
167
+
168
+ `.zig` → `language.zig` · `.ts`/`.tsx` → `language.typescript` ·
169
+ `.js`/`.jsx` → `language.javascript` · `.rs` → `language.rust` ·
170
+ `.go` → `language.go` · `.py` → `language.python` · `.sh` → `language.shell` ·
171
+ `.mdx` → `language.mdx` · `.sql` → `domain.sql`
172
+
173
+ Seven install in every repository: `universal.authoring`, `domain.http`,
174
+ `domain.auth`, `domain.documentation`, `domain.changelog`,
175
+ `workflow.specifications`, `workflow.skills`.
176
+
177
+ Three install only when `.oracle/orly.json` names them: `workflow.governance`
178
+ (editing orly's own rules), `product.agentsfleet` (agentsfleet-specific audits
179
+ and docs), `persona.indy` (tone and address handles only, no files).
180
+
181
+ Skills provided by `workflow.skills`, per repository: `orly-spec-new`,
182
+ `orly-babysit-prs`, `orly-write-unit-test`, `orly-write-integration-test`.
183
+ General-purpose skills come from gstack, per agent host — orly neither installs
184
+ nor manages gstack.
185
+
186
+ ---
187
+
188
+ ## 10. Working on orly itself
189
+
190
+ Only relevant if the task is a change to this repository.
191
+
192
+ ```bash
193
+ git clone git@github.com:agentsfleet/orly.git && cd orly
194
+ git config core.hooksPath .githooks
195
+ bun install --frozen-lockfile
196
+ make audit
197
+ ```
198
+
199
+ - `make audit` is the bar: typecheck and unit tests, render determinism, and
200
+ gate fixtures (each gate proved against one passing and one failing case).
201
+ Runs in parallel, under half a minute.
202
+ - Coverage floor is 90% lines. Below it, the workflow fails.
203
+ - `make install-evals` runs real installs into throwaway repositories. Run on
204
+ demand; CI runs them on every pull request and release.
205
+ - Render orly's own rules with `bin/orly update --no-hooks`. The `--no-hooks`
206
+ flag is required here because this checkout hand-wrote its `.githooks/`.
207
+ - Release: merge to `main` with a new `package.json` version. That publishes,
208
+ tags, and cuts the GitHub release. No second command.
209
+
210
+ ---
211
+
212
+ ## 11. Self-check prompt
213
+
214
+ After install, verify your own understanding by answering this from the
215
+ repository, without guessing:
216
+
217
+ ```
218
+ Read AGENTS.orly.md. Name which of the last ten commits would have tripped a
219
+ gate, and the rule that caught each one.
220
+ ```
221
+
222
+ If you cannot answer it from the installed files, re-read section 4 before
223
+ continuing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsfleet/orly",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Repository-scoped engineering harness: renders agent rules, materialises the gates that enforce them, and proves the PR boundary — for any coding agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,6 +51,7 @@
51
51
  "src/",
52
52
  "registry.json",
53
53
  "AGENTS.md",
54
+ "llms.txt",
54
55
  "SOUL.md"
55
56
  ],
56
57
  "publishConfig": {
package/src/criteria.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  openQuestions, productClarity, specBaseline, specDeferrals, specDimensions,
9
9
  specGate, specMoved, specOrdering,
10
10
  } from "./criteria_spec";
11
+ import { documentationSurfaces, scanSurfaces } from "./doc_rules";
11
12
  import { isObject, JsonObject, objectValue, OrlyError, RulesModel } from "./model";
12
13
  import { readConfigSync, RepoConfig } from "./config";
13
14
  import { classifyBranch, SurfaceReport } from "./surfaces";
@@ -30,6 +31,10 @@ const GIT_TREE = "git.tree";
30
31
  const GIT_PUSHED = "git.pushed";
31
32
  const REPO_CONFIG = "repo.config";
32
33
  const DOCS_UPDATED = "docs.updated";
34
+ const DOCS_LANGUAGE = "docs.language";
35
+ // How many findings a report names before it stops listing them. A criterion
36
+ // line is a summary, not a linter dump.
37
+ const NAMED_FINDINGS = 3;
33
38
 
34
39
  const FAST_TIER = "fast";
35
40
  const SLOW_TIER = "slow";
@@ -44,7 +49,7 @@ const SLOW_COMMANDS = ["verify.integration", "verify.memory"];
44
49
  // pr = can this ship (whole-branch checks + the slow suites).
45
50
  export function criteriaFor(gate: string, context: CriterionContext): Criterion[] {
46
51
  if (gate === "work") return [gitBranch(), gitTree(), repositoryConfig()];
47
- if (gate === "verify") return [specDimensions(), ...commandCriteria(context, FAST_TIER)];
52
+ if (gate === "verify") return [specDimensions(), docsLanguage(), ...commandCriteria(context, FAST_TIER)];
48
53
  if (gate === "pr") {
49
54
  return [
50
55
  gitTree(), gitPushed(), specGate(), openQuestions(), productClarity(), specDimensions(),
@@ -139,6 +144,34 @@ function docsUpdated(): Criterion {
139
144
  });
140
145
  }
141
146
 
147
+ // docs/DOCUMENTATION_RULES.md, read back against the pages orly renders here.
148
+ // The scope rule is the document's own: any documentation orly writes or
149
+ // renders, README.md included.
150
+ //
151
+ // REPORTING ONLY, and deliberately so. The rule document landed against a
152
+ // corpus that predates it, and the corpus does not obey it yet: this
153
+ // repository alone carries hundreds of pre-existing findings. A red gate on
154
+ // day one would be switched off by the end of the week, and grandfathering
155
+ // them into a baseline file would be a list of chores wearing a gate's badge.
156
+ // So the check reports what it found and leaves the verdict green until a
157
+ // human has decided how the debt gets paid. Partial mechanization stated
158
+ // honestly beats a red that means nothing (the same call audits/doc-read.sh
159
+ // makes when its record is missing). Flipping this to a gate is a one-line
160
+ // change: return ok on `findings.length === 0`.
161
+ function docsLanguage(): Criterion {
162
+ return criterion(DOCS_LANGUAGE, (context) => {
163
+ const surfaces = documentationSurfaces(context.root);
164
+ if (surfaces.length === 0) return { ok: true, detail: "no documentation surface here" };
165
+ const findings = scanSurfaces(context.root, surfaces);
166
+ if (findings.length === 0) return { ok: true, detail: `${surfaces.length} page(s) clean` };
167
+ const named = findings.slice(0, NAMED_FINDINGS).map((finding) => `${finding.file}:${finding.line} ${finding.code}`);
168
+ return {
169
+ ok: true,
170
+ detail: `${findings.length} finding(s) across ${surfaces.length} page(s), reporting only: ${named.join(", ")}`,
171
+ };
172
+ });
173
+ }
174
+
142
175
  // The branch is classified once per inspection and cached on the context.
143
176
  function report(context: CriterionContext, surfaces: JsonObject | undefined): SurfaceReport {
144
177
  context.surfaces ??= classifyBranch(context.root, surfaces);
@@ -0,0 +1,101 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+
5
+ import { DOC_DASH, DOC_HEADING, DOC_PARAGRAPH, DOC_SENTENCE, DOC_WORD, documentationSurfaces, scanDocument } from "./doc_rules";
6
+
7
+ const ROOT = resolve(import.meta.dir, "..");
8
+ const SLOP_FIXTURE = "fixtures/doc-slop.md";
9
+ const PAGE = "page.md";
10
+
11
+ // The pass fixture: one clean page written to the rules it is checked against.
12
+ // Every line here is prose the check must leave alone, so a false positive
13
+ // anywhere in the reader turns this test red.
14
+ const CLEAN = `# A clean page
15
+
16
+ orly writes this page. The check reads it and finds nothing.
17
+
18
+ ## What it leaves alone
19
+
20
+ Command names keep their banned word: run \`make harness-verify\` first.
21
+ A link keeps its text and drops its [target](https://example.com/execute).
22
+
23
+ | leverage | robust |
24
+ |---|---|
25
+ | a table cell is exempt | so is this |
26
+
27
+ \`\`\`bash
28
+ # fenced code is exempt: execute the robust pipeline
29
+ orly gate verify
30
+ \`\`\`
31
+
32
+ The EXECUTE stage is a name, not a verb. A rule may quote “leverage” to ban it.
33
+
34
+ <!-- lint-ignore: DOCWORD — this paragraph names the banned words to ban them -->
35
+ Also banned: delve, foster, elevate, harness, tapestry, realm.
36
+ `;
37
+
38
+ function codes(text: string): string[] {
39
+ return scanDocument(PAGE, text).map((finding) => finding.code);
40
+ }
41
+
42
+ describe("scanDocument", () => {
43
+ test("passes a clean page", () => {
44
+ expect(scanDocument(PAGE, CLEAN)).toEqual([]);
45
+ });
46
+
47
+ test("fails the slop fixture on every mechanical rule", () => {
48
+ const found = codes(readFileSync(join(ROOT, SLOP_FIXTURE), "utf8"));
49
+
50
+ expect(found).toContain(DOC_WORD);
51
+ expect(found).toContain(DOC_DASH);
52
+ expect(found).toContain(DOC_SENTENCE);
53
+ expect(found).toContain(DOC_PARAGRAPH);
54
+ expect(found).toContain(DOC_HEADING);
55
+ });
56
+
57
+ test("names the banned word it found", () => {
58
+ const findings = scanDocument(PAGE, "# Page\n\nWe leverage it.\n");
59
+
60
+ expect(findings).toHaveLength(1);
61
+ expect(findings[0]?.message).toContain("leverage");
62
+ expect(findings[0]?.line).toBe(3);
63
+ });
64
+
65
+ test("counts an over-budget page by its overage", () => {
66
+ const findings = scanDocument(PAGE, "# Page\n\nOne — two — three.\n").filter((finding) => finding.code === DOC_DASH);
67
+
68
+ expect(findings[0]?.weight).toBe(2);
69
+ });
70
+
71
+ test("reads a lowercase product name as a sentence start", () => {
72
+ const paragraph = "# Page\n\nOne here. orly two. bun three. git four.\n";
73
+
74
+ expect(codes(paragraph)).toEqual([DOC_PARAGRAPH]);
75
+ });
76
+
77
+ test("leaves a hyphenated command name alone", () => {
78
+ expect(codes("# Page\n\nRun provision-env-1password before the hook.\n")).toEqual([]);
79
+ });
80
+
81
+ test("counts a code span as one word, not as a hole", () => {
82
+ const findings = scanDocument(PAGE, "# Page\n\nThe hooks run on every commit and push. `orly gate pr` then refuses it.\n");
83
+
84
+ expect(findings).toEqual([]);
85
+ });
86
+
87
+ test("requires exactly one level-one heading", () => {
88
+ expect(codes("## Only a level two\n\nProse.\n")).toContain(DOC_HEADING);
89
+ expect(codes("# One\n\nProse.\n\n# Two\n\nMore prose.\n")).toContain(DOC_HEADING);
90
+ });
91
+ });
92
+
93
+ describe("documentationSurfaces", () => {
94
+ test("covers the readme and the pages orly renders", () => {
95
+ const surfaces = documentationSurfaces(ROOT);
96
+
97
+ expect(surfaces).toContain("README.md");
98
+ expect(surfaces).toContain("docs/DOCUMENTATION_RULES.md");
99
+ expect(surfaces.every((relative) => relative.endsWith(".md"))).toBeTrue();
100
+ });
101
+ });