constant-docs 0.4.0__py3-none-any.whl
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.
- constant_docs/__init__.py +18 -0
- constant_docs/__main__.py +15 -0
- constant_docs/api.py +968 -0
- constant_docs/auto.py +239 -0
- constant_docs/checks.py +494 -0
- constant_docs/cli.py +1085 -0
- constant_docs/config.py +1158 -0
- constant_docs/coverage.py +165 -0
- constant_docs/decisions.py +303 -0
- constant_docs/document.py +438 -0
- constant_docs/fingerprint.py +27 -0
- constant_docs/globs.py +154 -0
- constant_docs/guides/quickstart.md +124 -0
- constant_docs/guides/readme.md +198 -0
- constant_docs/house-style.md +70 -0
- constant_docs/index.py +210 -0
- constant_docs/kinds.py +304 -0
- constant_docs/paths.py +246 -0
- constant_docs/prompts/architecture.md +61 -0
- constant_docs/prompts/cli-reference.md +40 -0
- constant_docs/prompts/config-reference.md +38 -0
- constant_docs/prompts/errors.md +50 -0
- constant_docs/prompts/log.md +22 -0
- constant_docs/prompts/module.md +39 -0
- constant_docs/prompts/spec.md +68 -0
- constant_docs/state.py +273 -0
- constant_docs-0.4.0.dist-info/METADATA +380 -0
- constant_docs-0.4.0.dist-info/RECORD +31 -0
- constant_docs-0.4.0.dist-info/WHEEL +4 -0
- constant_docs-0.4.0.dist-info/entry_points.txt +3 -0
- constant_docs-0.4.0.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Writing a quickstart
|
|
2
|
+
|
|
3
|
+
A quickstart answers **how do I get this working right now?** Its reader has
|
|
4
|
+
already decided to try it. They want to reach something that works and then
|
|
5
|
+
stop reading.
|
|
6
|
+
|
|
7
|
+
The measure of a quickstart is time to first success, and the way it is failed
|
|
8
|
+
is by being complete. Every branch, caveat and alternative costs every reader
|
|
9
|
+
time, and the ones who needed the caveat were going to read the reference
|
|
10
|
+
anyway.
|
|
11
|
+
|
|
12
|
+
## The shape
|
|
13
|
+
|
|
14
|
+
1. **What they will have at the end**, in one sentence. A reader who does not
|
|
15
|
+
know where they are going cannot tell whether a step went wrong.
|
|
16
|
+
2. **Prerequisites, as a short list**, each checkable in one command. Say what
|
|
17
|
+
version, and how to find out.
|
|
18
|
+
3. **Numbered steps, each one command and one result.** Show what appears.
|
|
19
|
+
Where a step fails in a way a beginner will hit, say what that looks like
|
|
20
|
+
once, in a sentence, not as a troubleshooting section.
|
|
21
|
+
4. **The result, shown.** The file that now exists, the output that now
|
|
22
|
+
appears. Something they can compare against.
|
|
23
|
+
5. **One next step**, and one only. A link to the reference, or the single
|
|
24
|
+
thing most people do second.
|
|
25
|
+
|
|
26
|
+
## Rules
|
|
27
|
+
|
|
28
|
+
- **Every command copyable and runnable in order**, from a clean checkout,
|
|
29
|
+
with nothing skipped. Test it by doing exactly that.
|
|
30
|
+
- **One path.** No "or, if you prefer". Choose for them; the reference carries
|
|
31
|
+
the alternatives.
|
|
32
|
+
- **Real values, not placeholders.** If a placeholder is unavoidable, make it
|
|
33
|
+
obviously fake and say what to put there.
|
|
34
|
+
- **Show the output of anything that produces one.** A reader whose output
|
|
35
|
+
differs needs to know at that step, not three steps later.
|
|
36
|
+
- **Keep it to one screen if you can**, and two at the very most.
|
|
37
|
+
- **Link the reference for commands.** A quickstart that documents flags has
|
|
38
|
+
become a reference with a friendly tone, and it will drift from the real
|
|
39
|
+
one.
|
|
40
|
+
|
|
41
|
+
## What loses the reader
|
|
42
|
+
|
|
43
|
+
Certain vocabulary marks a page as machine-written, and a reader who decides
|
|
44
|
+
that nobody wrote this stops believing what it claims. There is no tolerance
|
|
45
|
+
to spend here — one of these in the opening costs more than the rest of the
|
|
46
|
+
document can earn back. Every entry is a word that sounds like value and
|
|
47
|
+
carries none.
|
|
48
|
+
|
|
49
|
+
- **Significance inflation** — *powerful*, *robust*, *seamless*, *crucial*,
|
|
50
|
+
*transformative*, *game-changing*, *blazingly fast*, *production-ready*.
|
|
51
|
+
Each asserts the conclusion the evidence was there to earn
|
|
52
|
+
- **Vocabulary tells** — *unlock*, *elevate*, *streamline*, *empower*,
|
|
53
|
+
*leverage*, *delve*, *journey*, *landscape*. If it would not be said aloud
|
|
54
|
+
across a desk, it does not get written
|
|
55
|
+
- **Adjective lists** — *fast, secure and developer-friendly*, an emoji on
|
|
56
|
+
each. Claims with no mechanism behind them. Say what the thing does, or cut
|
|
57
|
+
the line
|
|
58
|
+
- **Contrastive negation** — defining a thing by what it is not. *"Not just a
|
|
59
|
+
linter, but a formatter"*. *"A property rather than a roadmap"*. *"It
|
|
60
|
+
proposes instead of removing"*. The shape flatters the writer and makes the
|
|
61
|
+
reader wait a clause for the meaning. Worse, it spreads: once it is in the
|
|
62
|
+
ear, every sentence grows a foil. Say what is true and delete the half that
|
|
63
|
+
says what is not. **Ban `rather than`, `instead of`, `not X but Y` and `X,
|
|
64
|
+
not Y` outright.** A sentence that needs a real contrast survives being
|
|
65
|
+
written as two
|
|
66
|
+
- **Negative definition** — describing a thing by its absences. *"Never calls a model, never reads your
|
|
67
|
+
source, no AST, no import graph."* A list of absences draws the shape of a
|
|
68
|
+
hole. The reader still has to guess the object. Say what it does. Limits earn their own section, where somebody
|
|
69
|
+
hunting for them will find them
|
|
70
|
+
- **Rhetorical triplets** — three parallel clauses that buy rhythm and no
|
|
71
|
+
content. Use three when there are three
|
|
72
|
+
- **The universal address** — *"whether you are a solo developer or a large
|
|
73
|
+
team"*. It is written to nobody, and nobody is who it persuades
|
|
74
|
+
- **Minimisers** — *simply*, *just*, *easily* in front of an instruction. If
|
|
75
|
+
the step is easy, the word adds nothing. If it is hard, the word lies, and
|
|
76
|
+
your reader has already hit the error
|
|
77
|
+
- **Meta-signposting** — *"let us dive in"*, *"it is worth noting"*, *"here is
|
|
78
|
+
the thing"*. A document has no host
|
|
79
|
+
|
|
80
|
+
**Sentences.** The words above are the easy half. What follows makes a page
|
|
81
|
+
tiring to read even when every word in it is right.
|
|
82
|
+
|
|
83
|
+
- **One idea per sentence.** A semicolon or an em-dash joining two complete
|
|
84
|
+
thoughts gives you two sentences wearing one coat. Split them. Aim under
|
|
85
|
+
twenty words and stop at about twenty-five
|
|
86
|
+
- **Verbs, not `is`.** *"Modules are globs and files are bytes, which is what
|
|
87
|
+
makes any language a property here"* hangs on three copulas and lands
|
|
88
|
+
nothing. Find the action and put it in the verb. Where half the sentences on
|
|
89
|
+
a page lean on *is*, *are* or *was*, the page describes states where it
|
|
90
|
+
should describe what happens
|
|
91
|
+
- **Say who does what, and say it first.** Name the actor, then the action.
|
|
92
|
+
*"The tool hashes every file"* beats *"every file is hashed"*. A sentence
|
|
93
|
+
with no actor makes your reader guess one
|
|
94
|
+
- **Subject first.** *"On every write, the tool marks the module"* holds a
|
|
95
|
+
phrase in the air before the reader learns what it modifies. *"The tool
|
|
96
|
+
marks the module on every write"* lands immediately. Anything opening on a
|
|
97
|
+
preposition or a stray clause was usually written backwards, so turn it
|
|
98
|
+
round
|
|
99
|
+
- **Nothing that sounds quotable.** Balanced clauses, an aphorism, a line that
|
|
100
|
+
would fit on a slide: all of it puts rhythm where content belongs. Write the
|
|
101
|
+
sentence that tells somebody the thing
|
|
102
|
+
- **The reader's word for it.** *Prose*, *narrative* and *copy* are what
|
|
103
|
+
writers call writing. Everyone else says *the docs*, *the writing*, or names
|
|
104
|
+
the document. Take the short word over the formal one every time: *use* over
|
|
105
|
+
*utilise*, *buy* over *purchase*, *about* over *approximately*
|
|
106
|
+
|
|
107
|
+
Three mechanical tests, all cheap. Delete the word and read the sentence
|
|
108
|
+
again: if nothing was lost, the word was there to sound like something. Count
|
|
109
|
+
the words against what the sentence actually says and cut until the two match.
|
|
110
|
+
A first draft carries about a third more than it needs, and that third always
|
|
111
|
+
sounds the most like writing. Then read it aloud. Anything you would not say
|
|
112
|
+
to somebody at a desk gets rewritten as what you would have said.
|
|
113
|
+
|
|
114
|
+
## What does not belong
|
|
115
|
+
|
|
116
|
+
- Explanation of why the tool works the way it does. That is the specification
|
|
117
|
+
or the README, and it stops the reader mid-flow
|
|
118
|
+
- Configuration options beyond the one or two the path needs
|
|
119
|
+
- Troubleshooting sections. A reader who has hit an error is no longer doing
|
|
120
|
+
the quickstart; send them to the error catalogue
|
|
121
|
+
- Alternative installation methods, platform variations, or "on Windows,
|
|
122
|
+
instead…" branches. Pick one and link the rest
|
|
123
|
+
- Anything that cannot be run. A quickstart is a script written out in
|
|
124
|
+
sentences, and a step nobody can run is a step nobody trusts
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Writing a README
|
|
2
|
+
|
|
3
|
+
A README answers one question: **should I use this?** How to drive it, and
|
|
4
|
+
what every option does, belong in a reference. A README that tries to be a
|
|
5
|
+
reference becomes the document nobody updates.
|
|
6
|
+
|
|
7
|
+
Your reader decides in about ninety seconds whether to keep going. They
|
|
8
|
+
arrived from a search or a link. They know nothing about the project, and they
|
|
9
|
+
are hunting for a reason to leave. Most of them find one. Everything below
|
|
10
|
+
closes that exit.
|
|
11
|
+
|
|
12
|
+
Persuading that reader and being straight with them are the same job, because
|
|
13
|
+
their scepticism is the audience. Marketing language spends trust: an
|
|
14
|
+
adjective, an exclamation, a decoration. Evidence earns it: a number, a
|
|
15
|
+
mechanism, an admitted limit.
|
|
16
|
+
|
|
17
|
+
## Above the fold
|
|
18
|
+
|
|
19
|
+
Everything that persuades happens in the first screenful. A reader who scrolls
|
|
20
|
+
has already decided to keep going, so the space above that scroll carries
|
|
21
|
+
three things:
|
|
22
|
+
|
|
23
|
+
- **The claim** — what this is, in one line, at its strongest and still true
|
|
24
|
+
- **What it is** — the kind of thing, and where it runs
|
|
25
|
+
- **The problem it removes** — one line, because the reader already feels it
|
|
26
|
+
- **One piece of evidence** — a number, an output, or a demonstration
|
|
27
|
+
|
|
28
|
+
Put a table of contents, a badge row, a history or an architecture note there
|
|
29
|
+
and you spend attention you have not earned.
|
|
30
|
+
|
|
31
|
+
## The shape
|
|
32
|
+
|
|
33
|
+
1. **The claim, in one line.** The strongest true thing the project does,
|
|
34
|
+
written so that someone could disagree with it. Most people read this line.
|
|
35
|
+
Many read only this line, so it carries the whole product: *"documentation
|
|
36
|
+
that writes and maintains itself"*. Name the category and you have
|
|
37
|
+
described a shelf; make a claim and you have described a reason.
|
|
38
|
+
2. **What kind of thing it is, in one line.** A command-line tool, a library,
|
|
39
|
+
an editor extension, a hosted service, a plugin for something. Say which,
|
|
40
|
+
and say where it runs. A reader who cannot tell what they would be installing
|
|
41
|
+
cannot judge any of what follows. It is also the fact a claim carries
|
|
42
|
+
worst: *"documentation that writes and maintains itself"* could be any of
|
|
43
|
+
the six.
|
|
44
|
+
3. **The problem it removes, in one line.** One line. The reader already feels
|
|
45
|
+
the pain, so naming it confirms they are in the right place and nothing
|
|
46
|
+
more. Most READMEs skip it. Usage instructions appear in roughly nine files
|
|
47
|
+
in ten, a statement of purpose in one in four. That gap is what makes one
|
|
48
|
+
sentence here worth so much. Spend two and you bury the claim above it.
|
|
49
|
+
4. **How it works, in three sentences.** What it does, how, and the constraint
|
|
50
|
+
that matters: no key, no network, any language, whatever yours is.
|
|
51
|
+
5. **Proof, before instructions.** Show the thing working before you explain
|
|
52
|
+
how to work it. Use a recorded terminal session, a before and after, or a
|
|
53
|
+
measurement that names what it beats. One is enough. Skip it and you are
|
|
54
|
+
asking for an install on the strength of a paragraph.
|
|
55
|
+
6. **How to install it.** One line, copyable.
|
|
56
|
+
7. **The shortest path to something working.** From nothing to a result they
|
|
57
|
+
can see, in about a minute. One command if possible. Show the output.
|
|
58
|
+
8. **The thing that makes it worth having**, demonstrated. One realistic
|
|
59
|
+
example, complete enough to run.
|
|
60
|
+
9. **What it does not do.** Genuinely — the limits, the cases it will not
|
|
61
|
+
catch. Readers trust this section most, and it is the only one that costs
|
|
62
|
+
you anything to write.
|
|
63
|
+
10. **Links out**: the specification, the reference, the examples, the
|
|
64
|
+
licence.
|
|
65
|
+
|
|
66
|
+
## Rules
|
|
67
|
+
|
|
68
|
+
- **Cut the draft by a third, and cut the opening twice.** Padding hides in
|
|
69
|
+
the first paragraph, where the writer is still warming up and the reader has
|
|
70
|
+
agreed to nothing. Three sentences doing one sentence's work is the
|
|
71
|
+
commonest failure in a README. It is also the most expensive, because you
|
|
72
|
+
spend it on the one screenful everybody sees. Say it once, at its shortest,
|
|
73
|
+
and stop.
|
|
74
|
+
- **A heading says what is in the section.** *"Sixty seconds"* names a
|
|
75
|
+
measurement, so somebody scanning the page learns nothing from it. Name what
|
|
76
|
+
they get or what they will do: *"Install"*, *"Try it"*, *"What it does not
|
|
77
|
+
do"*. People read headings far more often than the paragraphs under them.
|
|
78
|
+
- **Every command example must be runnable as written.** A placeholder that
|
|
79
|
+
looks like a real value is worse than one that obviously is not.
|
|
80
|
+
- **Replace every adjective with a number, a mechanism, or nothing.** *"Fast"*
|
|
81
|
+
is a word the reader has discounted a thousand times. *"Three seconds where
|
|
82
|
+
the tool it replaces takes four minutes"* can be checked, and being
|
|
83
|
+
checkable is what makes it land. Where no number exists, name the mechanism:
|
|
84
|
+
*"no network call"* explains a speed claim without making one. An adjective
|
|
85
|
+
that survives neither test is decoration.
|
|
86
|
+
- **Show output.** A command with no output is a claim; a command with output
|
|
87
|
+
is evidence.
|
|
88
|
+
- **Show it running.** For anything with a terminal or a screen, a recording
|
|
89
|
+
does in five seconds what a paragraph does in thirty. Generate it from a
|
|
90
|
+
script, so a change in behaviour is a re-render. A demo nobody can
|
|
91
|
+
regenerate is a document that goes stale in public. Keep every fact in the
|
|
92
|
+
text as well as the image. A reader with images off, a search index and an
|
|
93
|
+
agent all see the text alone.
|
|
94
|
+
- **Attribute your proof, or drop it.** A named person saying a specific thing
|
|
95
|
+
carries weight. A wall of logos and a count of stars carry none, and readers
|
|
96
|
+
discount them: bought stars move no downloads at all. Ask whether a sceptic
|
|
97
|
+
could check it.
|
|
98
|
+
- **Link to the reference; never restate it.** A command list in a README is a
|
|
99
|
+
second home for the thing that changes most, and it goes stale first because
|
|
100
|
+
nothing checks it.
|
|
101
|
+
- **State limits plainly.** *"It cannot catch X"* buys more trust than any
|
|
102
|
+
amount of enthusiasm, and a reader who finds the limit themselves stops
|
|
103
|
+
believing the rest.
|
|
104
|
+
- **Do not narrate the project's history.** What it used to be is a changelog
|
|
105
|
+
entry.
|
|
106
|
+
|
|
107
|
+
## What loses the reader
|
|
108
|
+
|
|
109
|
+
Certain vocabulary marks a page as machine-written, and a reader who decides
|
|
110
|
+
that nobody wrote this stops believing what it claims. There is no tolerance
|
|
111
|
+
to spend here — one of these in the opening costs more than the rest of the
|
|
112
|
+
document can earn back. Every entry is a word that sounds like value and
|
|
113
|
+
carries none.
|
|
114
|
+
|
|
115
|
+
- **Significance inflation** — *powerful*, *robust*, *seamless*, *crucial*,
|
|
116
|
+
*transformative*, *game-changing*, *blazingly fast*, *production-ready*.
|
|
117
|
+
Each asserts the conclusion the evidence was there to earn
|
|
118
|
+
- **Vocabulary tells** — *unlock*, *elevate*, *streamline*, *empower*,
|
|
119
|
+
*leverage*, *delve*, *journey*, *landscape*. If it would not be said aloud
|
|
120
|
+
across a desk, it does not get written
|
|
121
|
+
- **Adjective lists** — *fast, secure and developer-friendly*, an emoji on
|
|
122
|
+
each. Claims with no mechanism behind them. Say what the thing does, or cut
|
|
123
|
+
the line
|
|
124
|
+
- **Contrastive negation** — defining a thing by what it is not. *"Not just a
|
|
125
|
+
linter, but a formatter"*. *"A property rather than a roadmap"*. *"It
|
|
126
|
+
proposes instead of removing"*. The shape flatters the writer and makes the
|
|
127
|
+
reader wait a clause for the meaning. Worse, it spreads: once it is in the
|
|
128
|
+
ear, every sentence grows a foil. Say what is true and delete the half that
|
|
129
|
+
says what is not. **Ban `rather than`, `instead of`, `not X but Y` and `X,
|
|
130
|
+
not Y` outright.** A sentence that needs a real contrast survives being
|
|
131
|
+
written as two
|
|
132
|
+
- **Negative definition** — describing a thing by its absences. *"Never calls a model, never reads your
|
|
133
|
+
source, no AST, no import graph."* A list of absences draws the shape of a
|
|
134
|
+
hole. The reader still has to guess the object. Say what it does. Limits earn their own section, where somebody
|
|
135
|
+
hunting for them will find them
|
|
136
|
+
- **Rhetorical triplets** — three parallel clauses that buy rhythm and no
|
|
137
|
+
content. Use three when there are three
|
|
138
|
+
- **The universal address** — *"whether you are a solo developer or a large
|
|
139
|
+
team"*. It is written to nobody, and nobody is who it persuades
|
|
140
|
+
- **Minimisers** — *simply*, *just*, *easily* in front of an instruction. If
|
|
141
|
+
the step is easy, the word adds nothing. If it is hard, the word lies, and
|
|
142
|
+
your reader has already hit the error
|
|
143
|
+
- **Meta-signposting** — *"let us dive in"*, *"it is worth noting"*, *"here is
|
|
144
|
+
the thing"*. A document has no host
|
|
145
|
+
|
|
146
|
+
**Sentences.** The words above are the easy half. What follows makes a page
|
|
147
|
+
tiring to read even when every word in it is right.
|
|
148
|
+
|
|
149
|
+
- **One idea per sentence.** A semicolon or an em-dash joining two complete
|
|
150
|
+
thoughts gives you two sentences wearing one coat. Split them. Aim under
|
|
151
|
+
twenty words and stop at about twenty-five
|
|
152
|
+
- **Verbs, not `is`.** *"Modules are globs and files are bytes, which is what
|
|
153
|
+
makes any language a property here"* hangs on three copulas and lands
|
|
154
|
+
nothing. Find the action and put it in the verb. Where half the sentences on
|
|
155
|
+
a page lean on *is*, *are* or *was*, the page describes states where it
|
|
156
|
+
should describe what happens
|
|
157
|
+
- **Say who does what, and say it first.** Name the actor, then the action.
|
|
158
|
+
*"The tool hashes every file"* beats *"every file is hashed"*. A sentence
|
|
159
|
+
with no actor makes your reader guess one
|
|
160
|
+
- **Subject first.** *"On every write, the tool marks the module"* holds a
|
|
161
|
+
phrase in the air before the reader learns what it modifies. *"The tool
|
|
162
|
+
marks the module on every write"* lands immediately. Anything opening on a
|
|
163
|
+
preposition or a stray clause was usually written backwards, so turn it
|
|
164
|
+
round
|
|
165
|
+
- **Nothing that sounds quotable.** Balanced clauses, an aphorism, a line that
|
|
166
|
+
would fit on a slide: all of it puts rhythm where content belongs. Write the
|
|
167
|
+
sentence that tells somebody the thing
|
|
168
|
+
- **The reader's word for it.** *Prose*, *narrative* and *copy* are what
|
|
169
|
+
writers call writing. Everyone else says *the docs*, *the writing*, or names
|
|
170
|
+
the document. Take the short word over the formal one every time: *use* over
|
|
171
|
+
*utilise*, *buy* over *purchase*, *about* over *approximately*
|
|
172
|
+
|
|
173
|
+
Three mechanical tests, all cheap. Delete the word and read the sentence
|
|
174
|
+
again: if nothing was lost, the word was there to sound like something. Count
|
|
175
|
+
the words against what the sentence actually says and cut until the two match.
|
|
176
|
+
A first draft carries about a third more than it needs, and that third always
|
|
177
|
+
sounds the most like writing. Then read it aloud. Anything you would not say
|
|
178
|
+
to somebody at a desk gets rewritten as what you would have said.
|
|
179
|
+
|
|
180
|
+
## What does not belong
|
|
181
|
+
|
|
182
|
+
- An options table, a flag list, or a full command reference — that is a
|
|
183
|
+
reference document, and duplicating it here guarantees the two disagree
|
|
184
|
+
- Installation instructions for six package managers. Pick the one your users
|
|
185
|
+
have; link the rest
|
|
186
|
+
- A roadmap. It ages into a list of the things that did not happen, and it
|
|
187
|
+
helps nobody's ninety-second decision
|
|
188
|
+
- Unattributable proof — a logo wall, a user count, a comparison table scored
|
|
189
|
+
by the author. Named users and named quotations outperform all of it and
|
|
190
|
+
belong high up; an anonymous aggregate belongs nowhere
|
|
191
|
+
- Badges beyond the two that mean something: does it build, and what licence
|
|
192
|
+
- An architecture explanation. A reader deciding whether to adopt does not
|
|
193
|
+
care yet, and one who does care has a specification to read
|
|
194
|
+
- Emoji in headings or feature rows. Decoration is the cheapest thing to fake,
|
|
195
|
+
which is exactly why restraint now reads as a trust signal
|
|
196
|
+
- Apologies, "work in progress" notices, or anything hedging what it does.
|
|
197
|
+
Either it does the thing or the README should say it does not
|
|
198
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Certain vocabulary marks a page as machine-written, and a reader who decides
|
|
2
|
+
that nobody wrote this stops believing what it claims. There is no tolerance
|
|
3
|
+
to spend here — one of these in the opening costs more than the rest of the
|
|
4
|
+
document can earn back. Every entry is a word that sounds like value and
|
|
5
|
+
carries none.
|
|
6
|
+
|
|
7
|
+
- **Significance inflation** — *powerful*, *robust*, *seamless*, *crucial*,
|
|
8
|
+
*transformative*, *game-changing*, *blazingly fast*, *production-ready*.
|
|
9
|
+
Each asserts the conclusion the evidence was there to earn
|
|
10
|
+
- **Vocabulary tells** — *unlock*, *elevate*, *streamline*, *empower*,
|
|
11
|
+
*leverage*, *delve*, *journey*, *landscape*. If it would not be said aloud
|
|
12
|
+
across a desk, it does not get written
|
|
13
|
+
- **Adjective lists** — *fast, secure and developer-friendly*, an emoji on
|
|
14
|
+
each. Claims with no mechanism behind them. Say what the thing does, or cut
|
|
15
|
+
the line
|
|
16
|
+
- **Contrastive negation** — defining a thing by what it is not. *"Not just a
|
|
17
|
+
linter, but a formatter"*. *"A property rather than a roadmap"*. *"It
|
|
18
|
+
proposes instead of removing"*. The shape flatters the writer and makes the
|
|
19
|
+
reader wait a clause for the meaning. Worse, it spreads: once it is in the
|
|
20
|
+
ear, every sentence grows a foil. Say what is true and delete the half that
|
|
21
|
+
says what is not. **Ban `rather than`, `instead of`, `not X but Y` and `X,
|
|
22
|
+
not Y` outright.** A sentence that needs a real contrast survives being
|
|
23
|
+
written as two
|
|
24
|
+
- **Negative definition** — describing a thing by its absences. *"Never calls a model, never reads your
|
|
25
|
+
source, no AST, no import graph."* A list of absences draws the shape of a
|
|
26
|
+
hole. The reader still has to guess the object. Say what it does. Limits earn their own section, where somebody
|
|
27
|
+
hunting for them will find them
|
|
28
|
+
- **Rhetorical triplets** — three parallel clauses that buy rhythm and no
|
|
29
|
+
content. Use three when there are three
|
|
30
|
+
- **The universal address** — *"whether you are a solo developer or a large
|
|
31
|
+
team"*. It is written to nobody, and nobody is who it persuades
|
|
32
|
+
- **Minimisers** — *simply*, *just*, *easily* in front of an instruction. If
|
|
33
|
+
the step is easy, the word adds nothing. If it is hard, the word lies, and
|
|
34
|
+
your reader has already hit the error
|
|
35
|
+
- **Meta-signposting** — *"let us dive in"*, *"it is worth noting"*, *"here is
|
|
36
|
+
the thing"*. A document has no host
|
|
37
|
+
|
|
38
|
+
**Sentences.** The words above are the easy half. What follows makes a page
|
|
39
|
+
tiring to read even when every word in it is right.
|
|
40
|
+
|
|
41
|
+
- **One idea per sentence.** A semicolon or an em-dash joining two complete
|
|
42
|
+
thoughts gives you two sentences wearing one coat. Split them. Aim under
|
|
43
|
+
twenty words and stop at about twenty-five
|
|
44
|
+
- **Verbs, not `is`.** *"Modules are globs and files are bytes, which is what
|
|
45
|
+
makes any language a property here"* hangs on three copulas and lands
|
|
46
|
+
nothing. Find the action and put it in the verb. Where half the sentences on
|
|
47
|
+
a page lean on *is*, *are* or *was*, the page describes states where it
|
|
48
|
+
should describe what happens
|
|
49
|
+
- **Say who does what, and say it first.** Name the actor, then the action.
|
|
50
|
+
*"The tool hashes every file"* beats *"every file is hashed"*. A sentence
|
|
51
|
+
with no actor makes your reader guess one
|
|
52
|
+
- **Subject first.** *"On every write, the tool marks the module"* holds a
|
|
53
|
+
phrase in the air before the reader learns what it modifies. *"The tool
|
|
54
|
+
marks the module on every write"* lands immediately. Anything opening on a
|
|
55
|
+
preposition or a stray clause was usually written backwards, so turn it
|
|
56
|
+
round
|
|
57
|
+
- **Nothing that sounds quotable.** Balanced clauses, an aphorism, a line that
|
|
58
|
+
would fit on a slide: all of it puts rhythm where content belongs. Write the
|
|
59
|
+
sentence that tells somebody the thing
|
|
60
|
+
- **The reader's word for it.** *Prose*, *narrative* and *copy* are what
|
|
61
|
+
writers call writing. Everyone else says *the docs*, *the writing*, or names
|
|
62
|
+
the document. Take the short word over the formal one every time: *use* over
|
|
63
|
+
*utilise*, *buy* over *purchase*, *about* over *approximately*
|
|
64
|
+
|
|
65
|
+
Three mechanical tests, all cheap. Delete the word and read the sentence
|
|
66
|
+
again: if nothing was lost, the word was there to sound like something. Count
|
|
67
|
+
the words against what the sentence actually says and cut until the two match.
|
|
68
|
+
A first draft carries about a third more than it needs, and that third always
|
|
69
|
+
sounds the most like writing. Then read it aloud. Anything you would not say
|
|
70
|
+
to somebody at a desk gets rewritten as what you would have said.
|
constant_docs/index.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Root index assembly for constant-docs.
|
|
2
|
+
|
|
3
|
+
Builds ``index.md`` from module descriptions, enforces token budget,
|
|
4
|
+
and conformance-checks every document under the docs root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from constant_docs.config import Config, is_tool_owned
|
|
17
|
+
from constant_docs.document import (
|
|
18
|
+
DocumentCache,
|
|
19
|
+
DocumentError,
|
|
20
|
+
extension_block,
|
|
21
|
+
replace_preserving_mode,
|
|
22
|
+
)
|
|
23
|
+
from constant_docs.document import load as load_document
|
|
24
|
+
from constant_docs.paths import doc_path_for
|
|
25
|
+
|
|
26
|
+
# The index carries no frontmatter, so this note is how a later run recognises
|
|
27
|
+
# a file as its own before overwriting it. Changing the wording orphans every
|
|
28
|
+
# index already on disk, which then reads as somebody else's file.
|
|
29
|
+
_GENERATED_NOTE = "Auto-generated by `constant-docs`."
|
|
30
|
+
|
|
31
|
+
# Every wording this note has ever had, because recognition has to reach back
|
|
32
|
+
# further than generation does. A 0.1-era repository — the upgrade path
|
|
33
|
+
# `tests/test_compat.py` exists to keep working — carries an index written
|
|
34
|
+
# under the tool's former name, and refusing to overwrite it would tell a user
|
|
35
|
+
# to delete a file the tool itself generated.
|
|
36
|
+
_GENERATED_NOTES = (_GENERATED_NOTE, "Auto-generated by `module-docs`.")
|
|
37
|
+
|
|
38
|
+
REQUIRED_FM_KEYS = [
|
|
39
|
+
"module",
|
|
40
|
+
"source_glob",
|
|
41
|
+
"source_files",
|
|
42
|
+
"source_hash",
|
|
43
|
+
"hash_method",
|
|
44
|
+
"hash_covers",
|
|
45
|
+
"generator",
|
|
46
|
+
"generator_spec",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _estimate_tokens(text: str) -> int:
|
|
51
|
+
"""Rough token estimate — 1 token per 4 characters."""
|
|
52
|
+
return len(text) // 4
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build(config: Config, repo_root: Path) -> str:
|
|
56
|
+
"""Assemble the root index from module descriptions.
|
|
57
|
+
|
|
58
|
+
Returns the markdown content for ``index.md`` as a string.
|
|
59
|
+
"""
|
|
60
|
+
lines: list[str] = [
|
|
61
|
+
"# Module Index\n",
|
|
62
|
+
(f"{_GENERATED_NOTE} Each entry links to the corresponding module document.\n"),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
current_estimate = 2 # for the heading + blank line
|
|
66
|
+
|
|
67
|
+
for mod in config.modules:
|
|
68
|
+
desc = ""
|
|
69
|
+
doc_file = doc_path_for(config, mod.key)
|
|
70
|
+
doc_abs = repo_root / doc_file
|
|
71
|
+
if doc_abs.exists():
|
|
72
|
+
try:
|
|
73
|
+
doc = load_document(doc_abs)
|
|
74
|
+
desc = doc.frontmatter.get("description", "") or ""
|
|
75
|
+
except DocumentError:
|
|
76
|
+
desc = "*(malformed)*"
|
|
77
|
+
|
|
78
|
+
# Relative to the index itself, which lives at <docs_root>/index.md.
|
|
79
|
+
# A repository-relative link here reads as `docs/modules/docs/modules/…`
|
|
80
|
+
# when followed from the index, which resolves nowhere and stays
|
|
81
|
+
# invisible until someone clicks it.
|
|
82
|
+
#
|
|
83
|
+
# A document declared outside the docs root cannot be made relative to
|
|
84
|
+
# it by stripping a prefix it does not have, so the link is computed
|
|
85
|
+
# as a walk between the two and comes out as `../README.md`. The index
|
|
86
|
+
# still lists it: a reader asking what is documented here wants the
|
|
87
|
+
# answer whatever directory the answer lives in.
|
|
88
|
+
rel_link = PurePosixPath(os.path.relpath(doc_file, config.docs_root)).as_posix()
|
|
89
|
+
entry = (
|
|
90
|
+
f"- [`{mod.key}`]({rel_link}): {desc}"
|
|
91
|
+
if desc
|
|
92
|
+
else f"- [`{mod.key}`]({rel_link})"
|
|
93
|
+
)
|
|
94
|
+
lines.append(entry)
|
|
95
|
+
current_estimate += _estimate_tokens(entry)
|
|
96
|
+
|
|
97
|
+
lines.append("") # trailing newline
|
|
98
|
+
|
|
99
|
+
if config.index_token_budget is not None:
|
|
100
|
+
budget = config.index_token_budget
|
|
101
|
+
if current_estimate > budget:
|
|
102
|
+
print(
|
|
103
|
+
f"Warning: index token estimate ({current_estimate}) exceeds "
|
|
104
|
+
f"configured budget ({budget})",
|
|
105
|
+
file=sys.stderr,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return "\n".join(lines) + "\n"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_generated_index(target: Path) -> bool:
|
|
112
|
+
"""Return whether *target* is an index this tool assembled.
|
|
113
|
+
|
|
114
|
+
The index is the one file under the docs root that deliberately carries no
|
|
115
|
+
frontmatter, so `paths.is_ours` cannot recognise it and the generated note
|
|
116
|
+
stands in as the mark. Read conservatively: a file we cannot read is not
|
|
117
|
+
ours, which is the same direction `is_ours` takes and for the same reason.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
head = target.read_text(encoding="utf-8")[:512]
|
|
121
|
+
except (OSError, UnicodeDecodeError):
|
|
122
|
+
return False
|
|
123
|
+
return any(note in head for note in _GENERATED_NOTES)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def write(config: Config, repo_root: Path) -> Path:
|
|
127
|
+
"""Assemble the index and write it to ``<docs_root>/index.md``.
|
|
128
|
+
|
|
129
|
+
Returns the path written. The file is left byte-identical when the
|
|
130
|
+
assembled content already matches, for the same reason document writes
|
|
131
|
+
are: a run that changes nothing must not move a timestamp or an mtime.
|
|
132
|
+
"""
|
|
133
|
+
content = build(config, repo_root)
|
|
134
|
+
target = repo_root / config.docs_root / "index.md"
|
|
135
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
if target.exists() and target.read_bytes() == content.encode():
|
|
137
|
+
return target
|
|
138
|
+
|
|
139
|
+
# The same question `prune` asks before it deletes, asked before a write
|
|
140
|
+
# that is just as unrecoverable. Pointing `docs_root` at an existing
|
|
141
|
+
# `docs/` folder is the documented first step and `index.md` is the most
|
|
142
|
+
# common file in one, so overwriting it lands on first adoption — and no
|
|
143
|
+
# scan would have reported it, because all three skip `index.md` by name.
|
|
144
|
+
if target.exists() and not _is_generated_index(target):
|
|
145
|
+
raise DocumentError(
|
|
146
|
+
f"{target} exists and was not written by constant-docs. The index "
|
|
147
|
+
f"is assembled from scratch on every run, so writing it would "
|
|
148
|
+
f"discard what is there. Move or delete that file, or point "
|
|
149
|
+
f"'docs_root' somewhere the tool owns."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
fd, tmp_path = tempfile.mkstemp(dir=str(target.parent), prefix=".index.md.tmp.")
|
|
153
|
+
try:
|
|
154
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
155
|
+
f.write(content)
|
|
156
|
+
replace_preserving_mode(tmp_path, target)
|
|
157
|
+
except BaseException:
|
|
158
|
+
if os.path.exists(tmp_path):
|
|
159
|
+
os.unlink(tmp_path)
|
|
160
|
+
raise
|
|
161
|
+
return target
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def conformance_check(
|
|
165
|
+
config: Config, repo_root: Path, cache: DocumentCache | None = None
|
|
166
|
+
) -> list[str]:
|
|
167
|
+
"""AC 25 — Verify every ``.md`` under the docs root has valid frontmatter.
|
|
168
|
+
|
|
169
|
+
Returns a list of problematic document paths (relative to repo root).
|
|
170
|
+
|
|
171
|
+
*cache* lets `verify` hand in the documents it has already parsed. Every
|
|
172
|
+
document under the docs root was read once by `plan` and again here, and a
|
|
173
|
+
parse is a read plus a DOTALL regex plus a YAML load.
|
|
174
|
+
"""
|
|
175
|
+
read = (cache or DocumentCache()).load
|
|
176
|
+
docs_root_abs = repo_root / config.docs_root
|
|
177
|
+
issues: list[str] = []
|
|
178
|
+
|
|
179
|
+
if not docs_root_abs.is_dir():
|
|
180
|
+
return issues
|
|
181
|
+
|
|
182
|
+
for md_file in sorted(docs_root_abs.rglob("*.md")):
|
|
183
|
+
if is_tool_owned(md_file, docs_root_abs):
|
|
184
|
+
continue
|
|
185
|
+
try:
|
|
186
|
+
doc = read(md_file)
|
|
187
|
+
doc_type = doc.frontmatter.get("type", "")
|
|
188
|
+
if not doc_type or not isinstance(doc_type, str) or not doc_type.strip():
|
|
189
|
+
issues.append(str(md_file.relative_to(repo_root)))
|
|
190
|
+
continue
|
|
191
|
+
# AC 35 — the keys live inside the namespaced producer-extension
|
|
192
|
+
# block, so a host schema and this tool cannot collide.
|
|
193
|
+
block = extension_block(doc.frontmatter)
|
|
194
|
+
if not block:
|
|
195
|
+
issues.append(
|
|
196
|
+
f"{md_file.relative_to(repo_root)} — missing the "
|
|
197
|
+
"'constant_docs' frontmatter block"
|
|
198
|
+
)
|
|
199
|
+
continue
|
|
200
|
+
for key in REQUIRED_FM_KEYS:
|
|
201
|
+
if key not in block:
|
|
202
|
+
issues.append(
|
|
203
|
+
f"{md_file.relative_to(repo_root)} — missing required "
|
|
204
|
+
f"frontmatter key: {key!r}"
|
|
205
|
+
)
|
|
206
|
+
break
|
|
207
|
+
except (OSError, yaml.YAMLError, DocumentError, ValueError):
|
|
208
|
+
issues.append(str(md_file.relative_to(repo_root)))
|
|
209
|
+
|
|
210
|
+
return issues
|