okf 1.13.0 → 2.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +222 -0
- data/README.md +34 -11
- data/lib/okf/bundle/folder.rb +27 -1
- data/lib/okf/bundle/graph.rb +12 -3
- data/lib/okf/bundle/linter.rb +470 -47
- data/lib/okf/bundle/reader.rb +10 -4
- data/lib/okf/bundle/references.rb +111 -0
- data/lib/okf/bundle/row_filter.rb +53 -0
- data/lib/okf/bundle/search.rb +20 -2
- data/lib/okf/bundle/validator/result.rb +6 -3
- data/lib/okf/bundle/validator.rb +267 -26
- data/lib/okf/bundle/writer.rb +1 -1
- data/lib/okf/bundle.rb +105 -4
- data/lib/okf/cli/catalog.rb +1 -1
- data/lib/okf/cli/command.rb +27 -8
- data/lib/okf/cli/files.rb +1 -1
- data/lib/okf/cli/index.rb +1 -1
- data/lib/okf/cli/lint.rb +70 -12
- data/lib/okf/cli/references.rb +97 -0
- data/lib/okf/cli/search.rb +2 -1
- data/lib/okf/cli/stats.rb +3 -39
- data/lib/okf/cli/tags.rb +6 -43
- data/lib/okf/cli/types.rb +1 -1
- data/lib/okf/cli/validate.rb +3 -3
- data/lib/okf/cli.rb +24 -3
- data/lib/okf/concept.rb +362 -10
- data/lib/okf/markdown/citations.rb +41 -4
- data/lib/okf/markdown/frontmatter.rb +1 -1
- data/lib/okf/markdown/links.rb +67 -7
- data/lib/okf/render/graph/template.html.erb +173 -41
- data/lib/okf/render/graph.rb +11 -3
- data/lib/okf/server/app.rb +47 -15
- data/lib/okf/server/hub.rb +1 -1
- data/lib/okf/skill/SKILL.md +28 -18
- data/lib/okf/skill/playbooks/curate.md +8 -3
- data/lib/okf/skill/playbooks/doctor.md +3 -1
- data/lib/okf/skill/playbooks/maintain.md +10 -7
- data/lib/okf/skill/playbooks/menu.md +5 -4
- data/lib/okf/skill/playbooks/migrate.md +31 -8
- data/lib/okf/skill/playbooks/produce.md +20 -11
- data/lib/okf/skill/playbooks/search.md +2 -2
- data/lib/okf/skill/reference/SPEC.md +739 -187
- data/lib/okf/skill/reference/authoring.md +164 -48
- data/lib/okf/skill/reference/cli/checks.md +171 -0
- data/lib/okf/skill/reference/cli/graph.md +49 -0
- data/lib/okf/skill/reference/cli/map.md +98 -0
- data/lib/okf/skill/reference/cli/registry.md +70 -0
- data/lib/okf/skill/reference/cli/search.md +130 -0
- data/lib/okf/skill/reference/cli/serve.md +83 -0
- data/lib/okf/skill/reference/cli/views.md +59 -0
- data/lib/okf/skill/reference/cli.md +44 -501
- data/lib/okf/skill/reference/spec-map.md +32 -0
- data/lib/okf/skill/templates/attested-computation.md +41 -0
- data/lib/okf/skill/templates/concept.md +13 -6
- data/lib/okf/skill/templates/root-index.md +1 -1
- data/lib/okf/version.rb +1 -1
- data/lib/okf.rb +22 -2
- metadata +13 -1
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
# Authoring OKF well — the craft
|
|
2
2
|
|
|
3
|
-
The spec ([SPEC.md](SPEC.md)) tells you what is *legal
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## What each SPEC section governs
|
|
11
|
-
|
|
12
|
-
Consult the right section on demand instead of re-reading all of [SPEC.md](SPEC.md):
|
|
13
|
-
|
|
14
|
-
| § | Governs | Reach for it when |
|
|
15
|
-
|---|---------|-------------------|
|
|
16
|
-
| §3 | bundle structure, reserved filenames | laying out directories |
|
|
17
|
-
| §4 | concept documents & frontmatter | writing or validating a concept |
|
|
18
|
-
| §5 / §5.3 | cross-links; **broken links are tolerated** | linking; judging a "broken" link |
|
|
19
|
-
| §6 | index files & progressive disclosure | orienting; writing or synthesizing an index |
|
|
20
|
-
| §7 | log files | recording history |
|
|
21
|
-
| §8 | citations & provenance | any external or empirical claim |
|
|
22
|
-
| §9 | conformance — the hard gate | what `validate` may and may not reject |
|
|
23
|
-
| §11 | versioning (`okf_version`) | the root index's one allowed field |
|
|
3
|
+
The spec ([SPEC.md](SPEC.md)) tells you what is *legal*, and
|
|
4
|
+
[spec-map.md](spec-map.md) says which § settles which question. This file is
|
|
5
|
+
what is *good* — the modelling judgment that turns a pile of conformant files into
|
|
6
|
+
knowledge worth consuming. Read it before `produce` or `maintain`, and keep the
|
|
7
|
+
§11 conformance rules in mind (parseable frontmatter, a non-empty `type`, and
|
|
8
|
+
well-formed reserved files — the hard rules in [SKILL.md](../SKILL.md));
|
|
9
|
+
everything else is guidance a consumer must tolerate.
|
|
24
10
|
|
|
25
11
|
## Modelling principles
|
|
26
12
|
|
|
@@ -44,7 +30,9 @@ responsibility. Keep a **small, consistent, descriptive** vocabulary per bundle
|
|
|
44
30
|
(`Service`, `Dataset`, `Metric`, `Decision`, `Playbook`, `Runbook`, …). Reusing
|
|
45
31
|
types across files is what makes the graph legible; inventing a new type per file
|
|
46
32
|
makes `type` meaningless. Before adding a new type, check what the bundle already
|
|
47
|
-
uses.
|
|
33
|
+
uses. One type the spec *does* name: `Attested Computation` (§10) is a contract,
|
|
34
|
+
not a suggestion — use it exactly when the concept carries a sanctioned
|
|
35
|
+
computation, and use the exact spelling.
|
|
48
36
|
|
|
49
37
|
### Tags are the connective axis — curate them like a vocabulary <!-- rule:okf-tag-vocabulary -->
|
|
50
38
|
`type` says what a concept *is*; the directory says where it *lives*; `tags` are
|
|
@@ -62,15 +50,53 @@ Lay out directories by what the knowledge is *about* (`services/`, `datasets/`,
|
|
|
62
50
|
`decisions/`), not by concept type. The directory tree is itself knowledge — it
|
|
63
51
|
shows a reader how the system decomposes, and it usually mirrors the shape of the
|
|
64
52
|
codebase or the org. A `types/`-first layout scatters related concepts and buries
|
|
65
|
-
the domain.
|
|
53
|
+
the domain. Non-concept assets a concept points at — run instructions, attester
|
|
54
|
+
code, computation files — live under `references/` by convention (§6.3): a naming
|
|
55
|
+
convention, not a requirement, and the tools never require it.
|
|
56
|
+
|
|
57
|
+
### Hidden files are outside the bundle <!-- rule:okf-no-hidden-concepts -->
|
|
58
|
+
The reader excludes dot-prefixed files and every path under a dot-prefixed
|
|
59
|
+
directory — the Unix hidden-file convention, kept deliberately: a project root
|
|
60
|
+
often carries an installed skill (`.claude/`), templates (`.github/`) or other
|
|
61
|
+
markdown that is not knowledge, and reading a directory must not pull those in
|
|
62
|
+
as concepts. §3's taxonomy is read as covering the visible tree. The practical
|
|
63
|
+
rule: never author a concept under a hidden directory — no verb will ever see
|
|
64
|
+
it, and nothing will warn you.
|
|
65
|
+
|
|
66
|
+
### A path-valued field pointing at `references/` needs its leading `/` <!-- check:broken_attestation_ref -->
|
|
67
|
+
§6.2 gives `resource`, `sources[].resource`, `computation`, `executor.resource`
|
|
68
|
+
and `attester.resource` the same three forms as a link: a URL, a bundle-relative
|
|
69
|
+
path **beginning with `/`**, or a path relative to the concept. So a concept at
|
|
70
|
+
`metrics/revenue.md` writing `attester: { resource: references/attesters/rev.py }`
|
|
71
|
+
is naming `metrics/references/attesters/rev.py`, not the bundle's `references/`
|
|
72
|
+
tree — the trap being that §6.3's own example, and the SPEC's Appendix, spell
|
|
73
|
+
these paths bare from a nested concept. Write `/references/…` and the field
|
|
74
|
+
resolves from the bundle root wherever the concept sits, which is the same reason
|
|
75
|
+
[links](#links-are-untyped-on-purpose) prefer the absolute form. `lint` catches
|
|
76
|
+
the `.md` cases (`broken_attestation_ref`, `broken_source`); a `.sql` or `.py`
|
|
77
|
+
target is not a concept, so no lint check sees it — `okf references` is the
|
|
78
|
+
surface that does: it lists the `references/` tree from disk with every pointer
|
|
79
|
+
that misses, and names the leading-`/` fix when that is the miss.
|
|
80
|
+
|
|
81
|
+
### A frontmatter `id` renames the concept, not its home <!-- rule:okf-id-extension -->
|
|
82
|
+
§2 defines the Concept ID as the file's path with `.md` removed, full stop.
|
|
83
|
+
This gem additionally honors a frontmatter `id:` as an override — an okf
|
|
84
|
+
extension, not spec, so a bundle leaning on it is trading portability for the
|
|
85
|
+
alias. If you pin one, know the recorded split: links still resolve by path,
|
|
86
|
+
so edges land correctly; the identity views (catalog, hubs, search, `--dir`)
|
|
87
|
+
follow the id, because the edges do; the physical views (`index`, `dirs`,
|
|
88
|
+
stats' `by_dir`) keep the file where it lives, because an index is a physical
|
|
89
|
+
listing. A pinned id that disagrees with the path therefore makes the two
|
|
90
|
+
families answer differently about where the concept is — prefer the default,
|
|
91
|
+
and rename the file when a concept needs a new name.
|
|
66
92
|
|
|
67
93
|
### `resource` is the bridge to reality <!-- rule:okf-resource-bridge -->
|
|
68
94
|
Set `resource` (a canonical URI) **only** when a concept *is* a real, addressable
|
|
69
95
|
asset — a table (`bigquery://…`), a service repo, a dashboard, an endpoint. Its
|
|
70
96
|
presence is what lets `maintain` find every concept affected by a changed asset in
|
|
71
|
-
one `okf search <dir> <uri>` call. Abstract concepts — a decision, a principle, a
|
|
72
|
-
definition — have no resource, and **omitting it is meaningful**, not
|
|
73
|
-
not invent placeholder URIs.
|
|
97
|
+
one `okf search <dir> <uri>` call. Abstract concepts — a decision, a principle, a
|
|
98
|
+
metric definition — have no resource, and **omitting it is meaningful**, not
|
|
99
|
+
laziness. Do not invent placeholder URIs.
|
|
74
100
|
|
|
75
101
|
### Links are untyped on purpose <!-- rule:okf-untyped-links -->
|
|
76
102
|
A markdown link asserts only "these two relate." The *kind* of relationship —
|
|
@@ -78,15 +104,61 @@ depends-on, supersedes, derived-from, owns — lives in the **prose around the
|
|
|
78
104
|
link**, never in a made-up typed-edge syntax. Write the sentence that explains the
|
|
79
105
|
relationship and put the link inside it. Prefer absolute bundle-relative targets
|
|
80
106
|
(`/services/auth-api.md`) so links survive file moves. A link to a concept that
|
|
81
|
-
does not exist yet is fine — it is not-yet-written knowledge (§
|
|
82
|
-
backlog will surface it as demand.
|
|
107
|
+
does not exist yet is fine — it is not-yet-written knowledge (§6.1), and `lint`'s
|
|
108
|
+
backlog will surface it as demand. One more edge you get for free: a
|
|
109
|
+
`sources[].resource` naming another concept **is** a graph edge (§5.1's lineage),
|
|
110
|
+
so recording provenance is also linking.
|
|
83
111
|
|
|
84
|
-
### Provenance
|
|
112
|
+
### Provenance lives in `sources`, attribution in footnotes (§5.1) <!-- check:uncited_external -->
|
|
85
113
|
Any external or empirical claim — a latency number, an approval, a quota, a
|
|
86
|
-
"because X team decided Y" — should
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
114
|
+
"because X team decided Y" — should be traceable to a `sources:` entry. Each
|
|
115
|
+
entry carries at least a `resource` (a URL, a bundle-relative path, or a scope
|
|
116
|
+
descriptor like `all queries in project X`), and optionally an `id`, `title`,
|
|
117
|
+
and the credibility signals `author` (an actor), `usage_count` (a liveness
|
|
118
|
+
signal, never a score), and `last_modified` (`YYYY-MM-DD`). To attribute a
|
|
119
|
+
*specific claim*, give the source an `id` and cite it with a markdown footnote
|
|
120
|
+
whose label is that id: `sharded daily.[^ga4-schema]` — keyed, not positional,
|
|
121
|
+
so a reordered list cannot misattribute silently. `lint`'s provenance category
|
|
122
|
+
checks the join in both directions (`unattributed_claim`, `unused_source`).
|
|
123
|
+
Uncited claims are exactly how a bundle decays into folklore nobody trusts.
|
|
124
|
+
("Citations" is the legacy v0.1 spelling: provenance lived in a body
|
|
125
|
+
`# Citations` list, which v0.2 retires — §13.1 keeps it readable, and `lint`'s
|
|
126
|
+
Migration findings tell you what to move where.)
|
|
127
|
+
|
|
128
|
+
### Say who did it, in §7's spelling <!-- check:unprefixed_actor -->
|
|
129
|
+
Every `by` — `generated.by`, `verified[].by` — takes an actor in one of §7's
|
|
130
|
+
three forms: `<producer>/<version>` (an automated producer, e.g.
|
|
131
|
+
`reference_agent/gemini-2.5-pro`), `human:<id>`, or `process:<id>`. The one
|
|
132
|
+
producer MUST: content a person hand-authored **or confirmed** is marked
|
|
133
|
+
`human:<id>` — §5.3 derives the trust tier from exactly this prefix, so a human
|
|
134
|
+
sign-off written `by: owner` silently reads as *machine-confirmed*, the exact
|
|
135
|
+
downgrade the tier system exists to prevent (`lint`'s `unprefixed_actor` nudges
|
|
136
|
+
it). `sources[].author` is looser on purpose: §7 does not govern it, and the
|
|
137
|
+
SPEC's own examples use `team:<id>` there — any honest attribution works.
|
|
138
|
+
|
|
139
|
+
### Trust is derived, never stored (§5.2/§5.3) <!-- rule:okf-trust-derived -->
|
|
140
|
+
`generated` records how the current content was produced (`by` REQUIRED within;
|
|
141
|
+
`at` an ISO 8601 datetime); `verified` lists who confirmed it against its
|
|
142
|
+
sources — they stay separate because who *wrote* a concept need not be who
|
|
143
|
+
*checked* it. Consumers derive the tier: no `verified` ⇒ unverified; machine
|
|
144
|
+
actors only ⇒ machine-confirmed; any `human:` verifier ⇒ human-reviewed. Never
|
|
145
|
+
write a tier or a credibility score into frontmatter — it would be subjective,
|
|
146
|
+
unportable, and stale the moment the next verification lands. And never invent
|
|
147
|
+
provenance: a concept whose history you do not know is honestly unverified.
|
|
148
|
+
Prefer **block style** for these mappings (`generated:` with indented
|
|
149
|
+
`by:`/`at:`): the flow spelling (`{ by: human:x, at: …T10:00:00Z }`) is legal
|
|
150
|
+
YAML, but the colons inside its values trip older libyaml parsers, and block
|
|
151
|
+
style is the spelling every parser accepts.
|
|
152
|
+
|
|
153
|
+
### Lifecycle: `status` and `stale_after` (§5.4/§5.5) <!-- rule:okf-lifecycle -->
|
|
154
|
+
`status` is one of `draft | stable | deprecated`; absent means `stable`, so
|
|
155
|
+
declare it only when it says something. `stale_after` is an absolute
|
|
156
|
+
`YYYY-MM-DD` — the author's own "do not trust me past this date", stale **on**
|
|
157
|
+
the day itself. Use it for knowledge with a known shelf life (a quota, a
|
|
158
|
+
migration window); `lint`'s `expired` check reports the ones whose date has
|
|
159
|
+
passed. It is a declared expiry, distinct from the `--stale-after` *flag*,
|
|
160
|
+
which is a reader-supplied age cutoff — see [cli/checks.md](cli/checks.md),
|
|
161
|
+
rule `okf-two-clocks`.
|
|
90
162
|
|
|
91
163
|
### Capture the non-obvious — not what code already says <!-- rule:okf-non-obvious -->
|
|
92
164
|
A bundle that restates function signatures or config keys goes stale the moment
|
|
@@ -98,19 +170,61 @@ to it rather than duplicating it.
|
|
|
98
170
|
|
|
99
171
|
### Write for both readers at once <!-- rule:okf-dual-audience -->
|
|
100
172
|
Use structural markdown so an agent can extract deterministically and a human can
|
|
101
|
-
skim: headings, tables, fenced code, lists. Conventional headings a reader
|
|
102
|
-
are `# Schema` (field/column tables), `# Examples`, and
|
|
103
|
-
|
|
104
|
-
whenever it aids consumption.
|
|
173
|
+
skim: headings, tables, fenced code, lists. Conventional headings a reader
|
|
174
|
+
expects are `# Schema` (field/column tables), `# Examples`, and — on an Attested
|
|
175
|
+
Computation only — `# Computation` (§10.3). Fill recommended frontmatter —
|
|
176
|
+
`title`, `description`, `tags`, `generated` — whenever it aids consumption.
|
|
177
|
+
|
|
178
|
+
### Attested computations are contracts (§10) <!-- rule:okf-computation-untouched -->
|
|
179
|
+
A concept of type `Attested Computation` carries a sanctioned computation:
|
|
180
|
+
`runtime` is REQUIRED (it fixes what `parameters` mean), the computation is
|
|
181
|
+
provided **exactly one way** — a body `# Computation` fence *or* a
|
|
182
|
+
`computation:` path, never both — and `executor`/`attester` name how it runs
|
|
183
|
+
and how a run is checked. The one new MUST NOT in v0.2 binds the *consumer*: an
|
|
184
|
+
agent MAY bind values for the declared `parameters` and MUST NOT author or edit
|
|
185
|
+
the computation itself. A "fixed" query silently stops being the one a person
|
|
186
|
+
sanctioned; if it is wrong, tell a human. The
|
|
187
|
+
[attested-computation template](../templates/attested-computation.md) carries
|
|
188
|
+
the whole contract.
|
|
105
189
|
|
|
106
190
|
### Reserved files <!-- rule:okf-reserved-files -->
|
|
107
191
|
`index.md` is a directory listing and carries **no frontmatter** — with one
|
|
108
192
|
exception: the **bundle-root** `index.md` is the only index that may carry
|
|
109
|
-
frontmatter, and it may carry *only* `okf_version: "0.
|
|
110
|
-
flags any other key there
|
|
111
|
-
Never use these names for
|
|
112
|
-
|
|
113
|
-
|
|
193
|
+
frontmatter, and it may carry *only* `okf_version: "0.2"` (§8 states the
|
|
194
|
+
exception inline; `validate` flags any other key there under §11 condition 3).
|
|
195
|
+
`log.md` is an ISO-dated change history, newest first. Never use these names for
|
|
196
|
+
concepts. Templates: [concept](../templates/concept.md),
|
|
197
|
+
[attested computation](../templates/attested-computation.md),
|
|
198
|
+
nested [index](../templates/index.md), bundle-root
|
|
199
|
+
[root-index](../templates/root-index.md), [log](../templates/log.md).
|
|
200
|
+
|
|
201
|
+
### The log records what shipped, not how it shipped <!-- rule:okf-log-durable-only -->
|
|
202
|
+
`log.md` carries durable knowledge and shipped behavior — never the process that
|
|
203
|
+
produced them. A bug fixed in a release is an entry; the review rounds that found
|
|
204
|
+
it are not. A capability that shipped is an entry; the iterations it took to
|
|
205
|
+
stabilize it before it shipped are not. When a change taught a lesson, the lesson
|
|
206
|
+
belongs in the concept it is about, stated as a principle, and the log entry
|
|
207
|
+
*points* at that concept instead of re-narrating the rounds — a reader finds it
|
|
208
|
+
where the subject lives, not by reading history.
|
|
209
|
+
|
|
210
|
+
The bar is what a reader six months out needs: *what changed and why it matters*,
|
|
211
|
+
never *how many passes it took to get there*. Watch for the shape this invites —
|
|
212
|
+
the newest entries sit at the top where every reader lands, so a stretch of work
|
|
213
|
+
stabilized by iteration accretes "round N found M defects" exactly where a durable
|
|
214
|
+
summary belongs.
|
|
215
|
+
|
|
216
|
+
## Migrating from v0.1 (§13)
|
|
217
|
+
|
|
218
|
+
A v0.1 bundle is consumable forever — §13.1 sanctions reading `timestamp` as
|
|
219
|
+
`generated.at` and a `# Citations` list as `sources` — so migration is
|
|
220
|
+
convenience, not rescue. Exactly two spellings retire: `timestamp` (move the
|
|
221
|
+
value under `generated: { by: <actor>, at: … }` — the actor is the one thing no
|
|
222
|
+
tool can derive; ask, or write `human:<maintainer>` when the history is human)
|
|
223
|
+
and the body `# Citations` section (lift each item into `sources`, add `id`s
|
|
224
|
+
and `[^id]` footnotes where the body cites, delete the section). `lint`'s two
|
|
225
|
+
Migration findings name the files; the [migrate playbook](../playbooks/migrate.md)
|
|
226
|
+
walks the rewrite, and `okf lint <dir> --only legacy_timestamp,legacy_citations
|
|
227
|
+
--fail-on info` is the mechanical done-check.
|
|
114
228
|
|
|
115
229
|
## Playbooks
|
|
116
230
|
|
|
@@ -119,7 +233,7 @@ verb (search, produce, migrate, maintain, consume, curate, doctor), routed by th
|
|
|
119
233
|
Commands table in [SKILL.md](../SKILL.md). The Closeout below is their shared
|
|
120
234
|
finishing gate.
|
|
121
235
|
|
|
122
|
-
## Closeout — the finishing gate
|
|
236
|
+
## Closeout — the finishing gate <!-- rule:okf-closeout-gate -->
|
|
123
237
|
|
|
124
238
|
`produce` step 6 and `maintain` steps 4–7 both land here: before calling an
|
|
125
239
|
authoring task done, walk this once. It is the repo's "turn every task into a check
|
|
@@ -129,10 +243,12 @@ grep can't:
|
|
|
129
243
|
- **Index enumerations** — every `index.md` that lists what you added, renamed, or
|
|
130
244
|
removed is updated; re-run `okf index` and eyeball each listing against reality.
|
|
131
245
|
Easy to skip, expensive to miss — this is the check that was missing.
|
|
132
|
-
- **`log.md`** — a dated entry, newest first
|
|
133
|
-
-
|
|
134
|
-
- **`
|
|
135
|
-
|
|
136
|
-
|
|
246
|
+
- **`log.md`** — a dated entry, newest first, and durable only (rule
|
|
247
|
+
`okf-log-durable-only` above).
|
|
248
|
+
- **`generated.at`** bumped on the concepts you touched (and `generated.by` says
|
|
249
|
+
who touched them — you, in §7's spelling).
|
|
250
|
+
- **`validate`** — zero §11 errors.
|
|
251
|
+
- **`lint`** — cheap findings cleared; `expired` reports out of the box, and
|
|
252
|
+
`--stale-after` adds the reader-side age cutoff when you want one.
|
|
137
253
|
- **`loose` review + tag curation** — the two semantic passes (maintain steps 6–7);
|
|
138
254
|
worth a pass in `produce` too on a non-trivial bundle.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Checking a bundle — `validate`, `lint`, `loose`, `references`
|
|
2
|
+
|
|
3
|
+
Kind: reference. Answers: what makes a bundle non-conformant, what each lint
|
|
4
|
+
check means and the severity it is pinned at, which of the two clocks a flag
|
|
5
|
+
reads, and which dangling pointers each surface can and cannot see.
|
|
6
|
+
|
|
7
|
+
The shared contract — `@slug` refs, exit codes, `--json`, the filters — is in
|
|
8
|
+
[cli.md](../cli.md) and is not repeated here.
|
|
9
|
+
|
|
10
|
+
## validate — the hard gate (§11)
|
|
11
|
+
|
|
12
|
+
Implements the spec's §11 conformance definition exactly (its words are in
|
|
13
|
+
[SPEC.md](../SPEC.md), routed by [spec-map.md](../spec-map.md)):
|
|
14
|
+
|
|
15
|
+
- **§11 cond. 1** every non-reserved file has a parseable YAML frontmatter block;
|
|
16
|
+
- **§11 cond. 2** every such block has a non-empty `type`;
|
|
17
|
+
- **§11 cond. 3** any `index.md`/`log.md` present follows §8/§9 (a nested
|
|
18
|
+
`index.md` has no frontmatter, a root `index.md` carries only `okf_version`,
|
|
19
|
+
`log.md` date headings are ISO `YYYY-MM-DD`).
|
|
20
|
+
|
|
21
|
+
`ERROR`s are the three conditions above; the bundle is non-conformant until every
|
|
22
|
+
one is fixed. `warn`s are soft — missing recommended fields, non-list tags, an
|
|
23
|
+
unparseable timestamp, **broken cross-links, which §6.1 explicitly tolerates**,
|
|
24
|
+
the shape of every §5/§10 family (`generated` not a mapping, a non-integer
|
|
25
|
+
`usage_count`, a `stale_after` that is not `YYYY-MM-DD`, a missing `runtime` on
|
|
26
|
+
an Attested Computation, …), and an `okf_version` the gem does not know (read
|
|
27
|
+
best-effort under §12; an absent one never warns). Absence of an optional family
|
|
28
|
+
is never a fault — a pure v0.1 bundle validates with zero warnings.
|
|
29
|
+
|
|
30
|
+
In `--json`, every warning carries `check` (a stable id) and `source` — `spec`
|
|
31
|
+
when the SPEC's own words state the rule, `convention` for a shape this gem asks
|
|
32
|
+
for beyond them (`verified[].by` presence, integer `usage_count`, a per-entry
|
|
33
|
+
`usage_window` mapping, `parameters[].name`, `executor`/`attester` `resource`).
|
|
34
|
+
Gate on `source` when you want only the spec-normative set; errors keep their
|
|
35
|
+
two-key `{ path, message }` shape. Fix warnings when cheap; never block on them.
|
|
36
|
+
Use `--json` in CI.
|
|
37
|
+
|
|
38
|
+
## lint — curation quality (advisory)
|
|
39
|
+
|
|
40
|
+
Asks the complementary question to `validate`: not "is this legal OKF?" but "is
|
|
41
|
+
this well-curated, navigable, trustworthy?" — precisely over the things §11
|
|
42
|
+
forbids `validate` from rejecting. It has its own report, never emits
|
|
43
|
+
conformance errors, and **exits `0` even with findings** unless you pass
|
|
44
|
+
`--fail-on warn` (exit 1 on any `warn` finding) or `--fail-on info` (exit 1 on
|
|
45
|
+
any finding at all).
|
|
46
|
+
|
|
47
|
+
**Severity is API.** Every check has a pinned level — `warn` or `info` — and
|
|
48
|
+
machine consumers gate on it, so the levels below are stable, not advisory. A
|
|
49
|
+
finding you want to gate on that is `info` gets `--fail-on info` (usually with
|
|
50
|
+
`--only`), never a hope that its severity changes. <!-- rule:okf-severity-is-api -->
|
|
51
|
+
|
|
52
|
+
Eight categories, each backed by individual checks (severity in brackets):
|
|
53
|
+
|
|
54
|
+
- **Reachability** — `orphan` [warn], `not_in_index` [warn],
|
|
55
|
+
`disconnected_component` [info], `unlinked` [info]
|
|
56
|
+
- **Backlog** — `missing_concept` [info], `broken_index_entry` [warn]
|
|
57
|
+
- **Completeness** — `stub` [info], `missing_title` [info],
|
|
58
|
+
`missing_description` [info], `missing_generated` [info] (quiet on either
|
|
59
|
+
spelling — a legacy `timestamp` still counts as a recorded change)
|
|
60
|
+
- **Freshness** — `expired` [info] (§5.5: past the concept's own declared
|
|
61
|
+
`stale_after`, on the day itself), `stale` [warn] (older than the
|
|
62
|
+
reader-supplied `--stale-after` cutoff, keyed on `generated_at`)
|
|
63
|
+
- **Provenance** — `uncited_external` [info] (external body links and no
|
|
64
|
+
sources, in either spelling), `broken_source` [warn] (an in-bundle `.md`
|
|
65
|
+
source target that names no concept; URLs and scope descriptors are out of
|
|
66
|
+
scope, and a non-`.md` asset is out of reach — the reader models concepts,
|
|
67
|
+
so lint never sees the file; `okf references` is the view that checks those
|
|
68
|
+
pointers), `unattributed_claim` [warn] (a footnote
|
|
69
|
+
no `sources[].id` answers — it *misattributes* a claim, which is why it
|
|
70
|
+
outranks its join-twin), `unused_source` [info] (a keyed source no footnote
|
|
71
|
+
cites — slack, not a defect), `unprefixed_actor` [info] (a `verified[].by`
|
|
72
|
+
outside §7's three forms reads as machine-confirmed; a `generated.by`
|
|
73
|
+
outside them feeds no tier but leaves a reader unable to tell a person
|
|
74
|
+
from a process; info so it informs, never blocks). A missing `generated.by` is the *validator's* warning —
|
|
75
|
+
REQUIRED-within is shape, not curation — so lint never double-reports it
|
|
76
|
+
- **Attestation** — `incomplete_computation` [warn] (an Attested Computation
|
|
77
|
+
providing its computation neither way, or both ways — §10.3 says a
|
|
78
|
+
`computation:` path is used *instead of* the body fence),
|
|
79
|
+
`broken_attestation_ref` [warn] (on an `Attested Computation`, a
|
|
80
|
+
`computation`, `executor.resource` or `attester.resource` naming an
|
|
81
|
+
in-bundle `.md` that is not there — a contract no consumer can follow; the
|
|
82
|
+
keys are read only on that type, since §4.1 lets any other concept use them
|
|
83
|
+
for its own purpose). Its reach is exactly the `.md` files: URLs are out
|
|
84
|
+
of scope, and a `.sql` or `.py` target is invisible to *every* check here,
|
|
85
|
+
because the linter reads the concept model and the model carries only
|
|
86
|
+
markdown — `okf references` is the surface that sees those files and reports
|
|
87
|
+
a pointer that misses, whatever the extension. Remember §6.2 reads a bare
|
|
88
|
+
`references/…` as relative to the concept, so from a nested concept it wants
|
|
89
|
+
the leading `/`
|
|
90
|
+
- **Migration** — `legacy_timestamp` [info], `legacy_citations` [info]: one
|
|
91
|
+
finding per bundle naming the files still in a retired v0.1 spelling, with
|
|
92
|
+
the rewrite instructions in the message. Info on purpose — §13 says a v0.1
|
|
93
|
+
bundle is consumable forever, so `--fail-on warn` must not turn red on one.
|
|
94
|
+
A migration campaign gates explicitly:
|
|
95
|
+
`okf lint <dir> --only legacy_timestamp,legacy_citations --fail-on info`,
|
|
96
|
+
exit 1 until clean.
|
|
97
|
+
- **Hygiene** — `duplicate_title` [info], `unused_reference_def` [info],
|
|
98
|
+
`undefined_reference` [warn], `self_link` [info], `log_order` [info] (§9
|
|
99
|
+
reads a log newest-first; disorder is slack, never a §11 error)
|
|
100
|
+
|
|
101
|
+
`--only` / `--except` filter by the **individual check names above**, not the
|
|
102
|
+
category labels — `okf lint <dir> --only orphan,stub` works; `--only reachability`
|
|
103
|
+
is an error. Two knobs tune specific checks: `--min-body N` sets the `stub` body
|
|
104
|
+
threshold in characters (default 50), and `--stale-after DUR` sets the `stale`
|
|
105
|
+
cutoff — a duration like `90d` or `12w`, or an ISO date like `2026-01-01` (a bare
|
|
106
|
+
number is rejected).
|
|
107
|
+
|
|
108
|
+
**Two different clocks, one unlucky name.** The `--stale-after` *flag* and the
|
|
109
|
+
`stale_after:` *frontmatter field* are different mechanisms that happen to share
|
|
110
|
+
a spelling. The flag is the **reader's** age cutoff: "flag anything not touched
|
|
111
|
+
since DUR", keyed on `generated_at`, feeding the `stale` check. The field is the
|
|
112
|
+
**author's** declared expiry: "do not trust this past DATE", feeding the
|
|
113
|
+
`expired` check. Never read one as the other, and never show them adjacent
|
|
114
|
+
without the distinction. <!-- rule:okf-two-clocks -->
|
|
115
|
+
|
|
116
|
+
**The clock is explicit.** `expired` compares against a day the CLI supplies —
|
|
117
|
+
today by default, or `--today YYYY-MM-DD` for a reproducible report (CI wants
|
|
118
|
+
this). The pure library runs no clock check unless handed `today:`, and every
|
|
119
|
+
clock-gated check that was selected but could not run is *named* in
|
|
120
|
+
`stats.skipped_checks` (the human report prints one `skipped:` line) — a gate
|
|
121
|
+
that is sometimes absent and does not confess converts "unchecked" into
|
|
122
|
+
"checked and fine".
|
|
123
|
+
|
|
124
|
+
The report's stats carry the bundle's posture too: `trust` (the §5.3 tier
|
|
125
|
+
distribution, in the hyphenated wire spelling) and `status` (effective-status
|
|
126
|
+
frequency).
|
|
127
|
+
|
|
128
|
+
`lint --json` is the structured substrate you consume to reason about the two
|
|
129
|
+
things lint deliberately does **not** compute — contradictions and *semantic*
|
|
130
|
+
staleness — which need understanding of meaning.
|
|
131
|
+
|
|
132
|
+
## loose — files with no graph connections (by folder)
|
|
133
|
+
|
|
134
|
+
Lists the **loose** files — concepts with graph **degree 0**: no cross-links in
|
|
135
|
+
*or* out — grouped by folder. It is a focused, folder-organized view over `lint`'s
|
|
136
|
+
`unlinked` check (`okf loose <dir>` ≈ `okf lint <dir> --only unlinked`, regrouped),
|
|
137
|
+
for the "which files float in the graph?" question. Advisory: **exits `0`**; `--json`
|
|
138
|
+
emits `{ bundle, count, loose: [{ id, title, dir }] }`.
|
|
139
|
+
|
|
140
|
+
**Loose ≠ orphan** — the trap. `lint`'s `orphan` is about *reachability*, and an
|
|
141
|
+
`index.md` listing makes a file reachable, so an indexed file is never an orphan.
|
|
142
|
+
But an index listing is **not a graph edge**: a file can be listed in an index yet
|
|
143
|
+
have no cross-links, so it floats in the graph while `lint` reports it as reachable.
|
|
144
|
+
`loose`/`unlinked` catch exactly that gap. A loose file is not automatically a
|
|
145
|
+
defect — a terminal leaf (a backlog item, a spec reference) can be loose by design;
|
|
146
|
+
`loose` surfaces the set so you can judge intent (see the
|
|
147
|
+
[maintain playbook](../../playbooks/maintain.md)).
|
|
148
|
+
<!-- rule:okf-loose-not-orphan -->
|
|
149
|
+
|
|
150
|
+
## references — the `references/` inventory (§6.3)
|
|
151
|
+
|
|
152
|
+
Lists every file under `references/` — including the non-markdown ones no other
|
|
153
|
+
verb can see, since the concept model carries only markdown — with which
|
|
154
|
+
concepts cite each file through the §6.2 path-valued fields (`resource`,
|
|
155
|
+
`sources[].resource`, `computation`, `executor.resource`, `attester.resource`),
|
|
156
|
+
plus every pointer into `references/` that resolves to nothing. Advisory:
|
|
157
|
+
**exits `0`** even with dangling pointers — the findings are the output. JSON:
|
|
158
|
+
`{ bundle, dangling, count, references: [{ path, dir, kind, referenced_by }] }`,
|
|
159
|
+
with `--fields`/`--except` projecting the rows. A file that is itself a concept
|
|
160
|
+
(§6.3 allows both) is marked `kind: "concept"`; body links are the graph's
|
|
161
|
+
business and are not counted here.
|
|
162
|
+
|
|
163
|
+
**The dangling list is where §6.2's bare-path trap surfaces.** A bare
|
|
164
|
+
`references/attesters/rev.py` written from `metrics/` resolves relative to the
|
|
165
|
+
concept — `metrics/references/attesters/rev.py`, nothing — and when the
|
|
166
|
+
leading-slash spelling would have hit, the entry says so:
|
|
167
|
+
`/references/attesters/rev.py exists — missing leading slash?`. Reach is any
|
|
168
|
+
extension, which is exactly what `broken_source` and `broken_attestation_ref`
|
|
169
|
+
cannot offer (their exemptions above), so run it wherever a bundle carries
|
|
170
|
+
attester code or computation files.
|
|
171
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# `graph` — the raw structure
|
|
2
|
+
|
|
3
|
+
Kind: reference. Answers: what a full dump costs and how to plan a traversal
|
|
4
|
+
without paying it, plus the two rankings that give `refine` its evidence —
|
|
5
|
+
`--hubs` by concept, `--traffic` by directory.
|
|
6
|
+
|
|
7
|
+
The shared contract — `@slug` refs, exit codes, `--json` — is in
|
|
8
|
+
[cli.md](../cli.md) and is not repeated here.
|
|
9
|
+
|
|
10
|
+
Prints the node/edge graph. `--json` emits a machine-readable dump — the
|
|
11
|
+
`bundle`/`slug` head every view carries, then `nodes` (with
|
|
12
|
+
`id`/`type`/`title`/`description`/`tags` **and, by default, every `body`** — the
|
|
13
|
+
part that dominates the bytes on a real bundle) plus `edges` — you can pipe into
|
|
14
|
+
other analysis. A concept with a missing *or blank* `type` indexes under
|
|
15
|
+
`Untyped`: §11 condition 2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
|
|
16
|
+
drops each node's body, and `--minimal` ships only `id`/`title` plus the type/tag
|
|
17
|
+
indexes — the lean shape the `server` page boots from. Reach for the full dump
|
|
18
|
+
only when the task truly consumes every body; for one question, the
|
|
19
|
+
[search verb](search.md) is orders cheaper.
|
|
20
|
+
|
|
21
|
+
`--hubs` swaps the dump for the **inbound ranking**: every concept with at
|
|
22
|
+
least one inbound link, ranked by inbound degree, each with its links grouped
|
|
23
|
+
by *source top-level dir* (`core/status ×3 flows 2, billing 1`) — the evidence for
|
|
24
|
+
[refine](../../playbooks/refine.md)'s hub origin test ("is this hub well-homed?").
|
|
25
|
+
A source at the bundle root counts under `(root)`. JSON: `{ bundle, count,
|
|
26
|
+
hubs: [{ id, top_dir, inbound, by_top_dir: { <top_dir>: n } }] }`. Advisory read, exit 0;
|
|
27
|
+
`--minimal`/`--no-body` shape node payloads and change nothing here.
|
|
28
|
+
|
|
29
|
+
`--traffic` asks the same question one grain coarser: **directories**, not
|
|
30
|
+
concepts. Every concept collapses into the directory it lives in and every link
|
|
31
|
+
between two directories collapses into one weighted arc, so a bundle's wiring
|
|
32
|
+
becomes a table you can read — measured on one 47-concept bundle, 227 links
|
|
33
|
+
collapsed into 50 arcs, of which the fitted cut draws 22. Each row carries the
|
|
34
|
+
directory's traffic split three ways
|
|
35
|
+
(`internal` / `out` / `in`) plus **cohesion**, its internal share of the total:
|
|
36
|
+
the evidence for [refine](../../playbooks/refine.md)'s container test, where
|
|
37
|
+
`--hubs` only ever answered about concepts. Rows lead with the lowest cohesion,
|
|
38
|
+
so the directories with a case to answer come first, and a directory with no
|
|
39
|
+
traffic at all prints `—` rather than a `0%` it did not earn.
|
|
40
|
+
|
|
41
|
+
`--cut N` is the least arc weight drawn. It defaults to a value **fitted to the
|
|
42
|
+
bundle** — enough arcs for roughly 1.5 per directory, floored at 8 — because a
|
|
43
|
+
fixed weight cannot serve both ends: measured at weight 3 across ten bundles it
|
|
44
|
+
left 2 arcs on one and 136 on another. The JSON says which you got. Cohesion is
|
|
45
|
+
computed over *every* arc and never the drawn ones, so tightening the cut
|
|
46
|
+
changes the picture and never the evidence. JSON: `{ bundle, cut, fitted, dirs:
|
|
47
|
+
[{ dir, parent, count, subtree, internal, out, in, cohesion }], arcs: [{ source,
|
|
48
|
+
target, weight }], total_arcs }` — a fraction of the full dump (2.6 KB against
|
|
49
|
+
27 KB on that bundle), and the shape rather than the contents. Advisory, exit 0.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Orienting — `index` and `dirs`
|
|
2
|
+
|
|
3
|
+
Kind: reference. Answers: which view to run first on a bundle you do not know,
|
|
4
|
+
how `--dir`, `--depth` and the ancestor chain compose, what a synthesized
|
|
5
|
+
listing means, and how to keep the map from paging the whole bundle.
|
|
6
|
+
|
|
7
|
+
The shared contract — `@slug` refs, exit codes, `--json`, the filters — is in
|
|
8
|
+
[cli.md](../cli.md) and is not repeated here.
|
|
9
|
+
|
|
10
|
+
## index — the progressive-disclosure map (§8)
|
|
11
|
+
|
|
12
|
+
The "orient before you read" view, and the read verb that sees the layer the
|
|
13
|
+
concept views can't: `index.md` files are reserved/structural, so
|
|
14
|
+
`catalog`/`files`/… never show them (in the browser, the Indexes tab and
|
|
15
|
+
folder clicks render this same map). `okf index <dir>` prints one entry per directory
|
|
16
|
+
that holds concepts or carries an `index.md`, root first — the authored index body
|
|
17
|
+
(frontmatter stripped), a `type`/`tag` rollup over the concepts that live directly
|
|
18
|
+
there, its child directories, and the concept listing. Run it first when picking up
|
|
19
|
+
an existing bundle: it is the cheapest high-signal orientation, and it surfaces
|
|
20
|
+
enumeration drift a grep can't (you can't grep for a listing entry that is *missing*).
|
|
21
|
+
|
|
22
|
+
`--dir PATH` narrows to a directory **and everything below it**, and is
|
|
23
|
+
**repeatable** — `--dir model --dir format` shows both; `root` (or `.`) names the
|
|
24
|
+
bundle root, unless the bundle really has a `root/` directory, which owns the
|
|
25
|
+
word. A `--dir` also brings the **chain from the root down to it**, so a branch is
|
|
26
|
+
never shown adrift of the authored context that says what it is — the root
|
|
27
|
+
`index.md`'s prose first among it. Those rows print with a leading `↑` and carry
|
|
28
|
+
`ancestor: true`; `--no-ancestors` drops them. Ascent and descent are separate
|
|
29
|
+
axes, so `--depth` never bounds the chain: `--dir X --depth 0` is X alone, plus
|
|
30
|
+
how you get to X. A `--dir` that names nothing gains no chain — a lone root row
|
|
31
|
+
would read as a partial answer to a query that matched nothing.
|
|
32
|
+
|
|
33
|
+
`--depth N` bounds how far below the starting point the map reaches
|
|
34
|
+
(the `--dir` when one is given, else the bundle root), counted **relatively**:
|
|
35
|
+
`--depth 1` is the top of the tree, `--dir X --depth 1` is one branch of it, and
|
|
36
|
+
the pair walks down a level at a time.
|
|
37
|
+
|
|
38
|
+
**On a bundle of any size the map is unreadable whole** — every directory is a
|
|
39
|
+
section, and even `--no-body` keeps one listing row per *concept* — so narrow
|
|
40
|
+
rather than paging it: `okf dirs` is the orientation, `--dir <branch> --depth 1`
|
|
41
|
+
is the step down into it,
|
|
42
|
+
and `--except body,listing` on top of either is the lean JSON skeleton. Full
|
|
43
|
+
`index` output on a few hundred concepts runs to hundreds of KB; the same map at
|
|
44
|
+
`--depth 1` is a couple of KB.
|
|
45
|
+
|
|
46
|
+
`--no-body` drops the prose to a
|
|
47
|
+
skeleton (headers, rollups, child pointers). For a directory that has concepts but
|
|
48
|
+
**no `index.md`**, the listing is **synthesized** from the concepts' descriptions
|
|
49
|
+
and tagged `(no index.md)` — §8 explicitly permits synthesizing a map on the fly.
|
|
50
|
+
|
|
51
|
+
It is a **read view**: advisory, always exit 0. A synthesized directory is a
|
|
52
|
+
*signal* (a map worth writing), never a defect — `index` emits no lint findings and
|
|
53
|
+
never fails a bundle. JSON: `{ bundle, count, directories: [{ dir, index_path,
|
|
54
|
+
present, synthesized, count, types, tags, subdirs, body, listing: [{ id, title,
|
|
55
|
+
description, type, tags }] }] }` — `ancestor` marks a row that is there to place
|
|
56
|
+
the branch rather than to answer about it.
|
|
57
|
+
|
|
58
|
+
## dirs — the bundle's clusters and their sizes
|
|
59
|
+
|
|
60
|
+
`okf dirs <dir>` lists every directory the bundle has — the ones holding
|
|
61
|
+
concepts, the ones carrying an `index.md`, and the empty intermediates that only
|
|
62
|
+
exist to connect the tree — with the number of concepts living **directly** in
|
|
63
|
+
each and the number in its **subtree**. A cluster *is* a directory here, so this
|
|
64
|
+
is the view that tells you what `--dir` can be pointed at and how much sits
|
|
65
|
+
behind each choice.
|
|
66
|
+
|
|
67
|
+
Two numbers, because one cannot answer the question. `count` is direct, so the
|
|
68
|
+
column sums to the bundle's concept total and a dir holding only sub-directories
|
|
69
|
+
reads `0` rather than a hidden rollup. `subtree` is defined as *exactly what
|
|
70
|
+
`--dir <that row>` returns*, so the row and the flag can never disagree — which
|
|
71
|
+
is also why the root's subtree is its own direct count (`.` is a prefix of
|
|
72
|
+
nothing). Without it a truncated listing is all zeroes at the top of a deep tree,
|
|
73
|
+
which is where you most need to know where the mass is. The human table shows the
|
|
74
|
+
second column only where some dir actually nests.
|
|
75
|
+
|
|
76
|
+
`--dir PATH` (repeatable) narrows to a directory and its subtree, and brings the
|
|
77
|
+
**chain up to the root** with it so the branch is placed rather than shown
|
|
78
|
+
adrift — those rows are marked `↑`, carry `ancestor: true`, and stay out of
|
|
79
|
+
`total` (`--no-ancestors` drops them). `--depth N` keeps only N levels below the
|
|
80
|
+
starting point — the `--dir` when one is given,
|
|
81
|
+
the bundle root otherwise. Relative, not absolute, so `--dir a/b --depth 1`
|
|
82
|
+
reads "a/b and one level under it" without your first working out how deep `a/b`
|
|
83
|
+
is. `--depth 0` is the starting point alone. A `--depth` that is not a whole
|
|
84
|
+
number is a usage error (exit 2).
|
|
85
|
+
|
|
86
|
+
**This is the first command to run on a bundle you do not know** — the same first
|
|
87
|
+
move [SKILL.md](../../SKILL.md) prescribes. `okf dirs <dir>` is one row per
|
|
88
|
+
directory, so its size tracks the tree rather than the concept count: it tells
|
|
89
|
+
you the shape and where the weight sits, `--depth 1` trims it further on a deep
|
|
90
|
+
bundle, and you then descend with `okf index --dir`, one level at a time.
|
|
91
|
+
|
|
92
|
+
The root prints `(root)` and stores `.` — the split every grouped view keeps, so
|
|
93
|
+
a table and its `--json` never disagree about which spelling is the data. JSON:
|
|
94
|
+
`{ bundle, total, count, dirs: [{ dir, ancestor, count, subtree, subdirs }] }`,
|
|
95
|
+
root first. `count` is rows printed, chain included; `total` sums the direct
|
|
96
|
+
counts of the rows you actually asked for, which is what keeps a row's `subtree`
|
|
97
|
+
equal to the `total` that `--dir` on that row returns.
|
|
98
|
+
|