okf 1.13.0 → 2.0.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +195 -0
  3. data/README.md +25 -6
  4. data/lib/okf/bundle/folder.rb +27 -1
  5. data/lib/okf/bundle/graph.rb +12 -3
  6. data/lib/okf/bundle/linter.rb +470 -47
  7. data/lib/okf/bundle/reader.rb +10 -4
  8. data/lib/okf/bundle/references.rb +111 -0
  9. data/lib/okf/bundle/row_filter.rb +53 -0
  10. data/lib/okf/bundle/search.rb +20 -2
  11. data/lib/okf/bundle/validator/result.rb +6 -3
  12. data/lib/okf/bundle/validator.rb +267 -26
  13. data/lib/okf/bundle/writer.rb +1 -1
  14. data/lib/okf/bundle.rb +105 -4
  15. data/lib/okf/cli/catalog.rb +1 -1
  16. data/lib/okf/cli/command.rb +27 -8
  17. data/lib/okf/cli/files.rb +1 -1
  18. data/lib/okf/cli/index.rb +1 -1
  19. data/lib/okf/cli/lint.rb +70 -12
  20. data/lib/okf/cli/references.rb +97 -0
  21. data/lib/okf/cli/search.rb +2 -1
  22. data/lib/okf/cli/stats.rb +3 -39
  23. data/lib/okf/cli/tags.rb +6 -43
  24. data/lib/okf/cli/types.rb +1 -1
  25. data/lib/okf/cli/validate.rb +3 -3
  26. data/lib/okf/cli.rb +4 -1
  27. data/lib/okf/concept.rb +362 -10
  28. data/lib/okf/markdown/citations.rb +41 -4
  29. data/lib/okf/markdown/frontmatter.rb +1 -1
  30. data/lib/okf/markdown/links.rb +67 -7
  31. data/lib/okf/render/graph/template.html.erb +173 -41
  32. data/lib/okf/render/graph.rb +11 -3
  33. data/lib/okf/server/app.rb +47 -15
  34. data/lib/okf/server/hub.rb +1 -1
  35. data/lib/okf/skill/SKILL.md +14 -12
  36. data/lib/okf/skill/playbooks/curate.md +8 -3
  37. data/lib/okf/skill/playbooks/doctor.md +3 -1
  38. data/lib/okf/skill/playbooks/maintain.md +7 -6
  39. data/lib/okf/skill/playbooks/menu.md +5 -4
  40. data/lib/okf/skill/playbooks/migrate.md +31 -8
  41. data/lib/okf/skill/playbooks/produce.md +16 -9
  42. data/lib/okf/skill/playbooks/search.md +2 -2
  43. data/lib/okf/skill/reference/SPEC.md +739 -187
  44. data/lib/okf/skill/reference/authoring.md +154 -35
  45. data/lib/okf/skill/reference/cli.md +155 -42
  46. data/lib/okf/skill/templates/attested-computation.md +41 -0
  47. data/lib/okf/skill/templates/concept.md +13 -6
  48. data/lib/okf/skill/templates/root-index.md +1 -1
  49. data/lib/okf/version.rb +1 -1
  50. data/lib/okf.rb +22 -2
  51. metadata +5 -1
@@ -2,10 +2,10 @@
2
2
 
3
3
  The spec ([SPEC.md](SPEC.md)) tells you what is *legal*. This file is what is
4
4
  *good* — the modelling judgment that turns a pile of conformant files into
5
- knowledge worth consuming. Read it before `produce` or `maintain`, and keep the §9
6
- conformance rules in mind (parseable frontmatter, a non-empty `type`, and
7
- well-formed reserved files — the hard rules in [SKILL.md](../SKILL.md)); everything
8
- else is guidance a consumer must tolerate.
5
+ knowledge worth consuming. Read it before `produce` or `maintain`, and keep the
6
+ §11 conformance rules in mind (parseable frontmatter, a non-empty `type`, and
7
+ well-formed reserved files — the hard rules in [SKILL.md](../SKILL.md));
8
+ everything else is guidance a consumer must tolerate.
9
9
 
10
10
  ## What each SPEC section governs
11
11
 
@@ -15,12 +15,18 @@ Consult the right section on demand instead of re-reading all of [SPEC.md](SPEC.
15
15
  |---|---------|-------------------|
16
16
  | §3 | bundle structure, reserved filenames | laying out directories |
17
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 |
18
+ | §5.1 | provenance `sources` and its credibility signals | any external or empirical claim |
19
+ | §5.2 / §5.3 | trust `generated`, `verified`, and the derived tiers | recording who wrote or confirmed content |
20
+ | §5.4 / §5.5 | lifecycle `status`, `stale_after` | marking drafts, deprecations, expiries |
21
+ | §6 / §6.1 | cross-links; **broken links are tolerated** | linking; judging a "broken" link |
22
+ | §6.2 / §6.3 | path-valued fields; the `references/` convention | pointing at non-concept assets |
23
+ | §7 | the actor convention | filling any `by` |
24
+ | §8 | index files & progressive disclosure | orienting; writing or synthesizing an index |
25
+ | §9 | log files | recording history |
26
+ | §10 | attested computations | a concept that *is* a sanctioned computation |
27
+ | §11 | conformance — the hard gate | what `validate` may and may not reject |
28
+ | §12 | versioning (`okf_version`) | the root index's one allowed field |
29
+ | §13 | changes from v0.1 | migrating a bundle; reading an unmigrated one |
24
30
 
25
31
  ## Modelling principles
26
32
 
@@ -44,7 +50,9 @@ responsibility. Keep a **small, consistent, descriptive** vocabulary per bundle
44
50
  (`Service`, `Dataset`, `Metric`, `Decision`, `Playbook`, `Runbook`, …). Reusing
45
51
  types across files is what makes the graph legible; inventing a new type per file
46
52
  makes `type` meaningless. Before adding a new type, check what the bundle already
47
- uses.
53
+ uses. One type the spec *does* name: `Attested Computation` (§10) is a contract,
54
+ not a suggestion — use it exactly when the concept carries a sanctioned
55
+ computation, and use the exact spelling.
48
56
 
49
57
  ### Tags are the connective axis — curate them like a vocabulary <!-- rule:okf-tag-vocabulary -->
50
58
  `type` says what a concept *is*; the directory says where it *lives*; `tags` are
@@ -62,15 +70,53 @@ Lay out directories by what the knowledge is *about* (`services/`, `datasets/`,
62
70
  `decisions/`), not by concept type. The directory tree is itself knowledge — it
63
71
  shows a reader how the system decomposes, and it usually mirrors the shape of the
64
72
  codebase or the org. A `types/`-first layout scatters related concepts and buries
65
- the domain.
73
+ the domain. Non-concept assets a concept points at — run instructions, attester
74
+ code, computation files — live under `references/` by convention (§6.3): a naming
75
+ convention, not a requirement, and the tools never require it.
76
+
77
+ ### Hidden files are outside the bundle <!-- rule:okf-no-hidden-concepts -->
78
+ The reader excludes dot-prefixed files and every path under a dot-prefixed
79
+ directory — the Unix hidden-file convention, kept deliberately: a project root
80
+ often carries an installed skill (`.claude/`), templates (`.github/`) or other
81
+ markdown that is not knowledge, and reading a directory must not pull those in
82
+ as concepts. §3's taxonomy is read as covering the visible tree. The practical
83
+ rule: never author a concept under a hidden directory — no verb will ever see
84
+ it, and nothing will warn you.
85
+
86
+ ### A path-valued field pointing at `references/` needs its leading `/` <!-- check:broken_attestation_ref -->
87
+ §6.2 gives `resource`, `sources[].resource`, `computation`, `executor.resource`
88
+ and `attester.resource` the same three forms as a link: a URL, a bundle-relative
89
+ path **beginning with `/`**, or a path relative to the concept. So a concept at
90
+ `metrics/revenue.md` writing `attester: { resource: references/attesters/rev.py }`
91
+ is naming `metrics/references/attesters/rev.py`, not the bundle's `references/`
92
+ tree — the trap being that §6.3's own example, and the SPEC's Appendix, spell
93
+ these paths bare from a nested concept. Write `/references/…` and the field
94
+ resolves from the bundle root wherever the concept sits, which is the same reason
95
+ [links](#links-are-untyped-on-purpose) prefer the absolute form. `lint` catches
96
+ the `.md` cases (`broken_attestation_ref`, `broken_source`); a `.sql` or `.py`
97
+ target is not a concept, so no lint check sees it — `okf references` is the
98
+ surface that does: it lists the `references/` tree from disk with every pointer
99
+ that misses, and names the leading-`/` fix when that is the miss.
100
+
101
+ ### A frontmatter `id` renames the concept, not its home <!-- rule:okf-id-extension -->
102
+ §2 defines the Concept ID as the file's path with `.md` removed, full stop.
103
+ This gem additionally honors a frontmatter `id:` as an override — an okf
104
+ extension, not spec, so a bundle leaning on it is trading portability for the
105
+ alias. If you pin one, know the recorded split: links still resolve by path,
106
+ so edges land correctly; the identity views (catalog, hubs, search, `--dir`)
107
+ follow the id, because the edges do; the physical views (`index`, `dirs`,
108
+ stats' `by_dir`) keep the file where it lives, because an index is a physical
109
+ listing. A pinned id that disagrees with the path therefore makes the two
110
+ families answer differently about where the concept is — prefer the default,
111
+ and rename the file when a concept needs a new name.
66
112
 
67
113
  ### `resource` is the bridge to reality <!-- rule:okf-resource-bridge -->
68
114
  Set `resource` (a canonical URI) **only** when a concept *is* a real, addressable
69
115
  asset — a table (`bigquery://…`), a service repo, a dashboard, an endpoint. Its
70
116
  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 metric
72
- definition — have no resource, and **omitting it is meaningful**, not laziness. Do
73
- not invent placeholder URIs.
117
+ one `okf search <dir> <uri>` call. Abstract concepts — a decision, a principle, a
118
+ metric definition — have no resource, and **omitting it is meaningful**, not
119
+ laziness. Do not invent placeholder URIs.
74
120
 
75
121
  ### Links are untyped on purpose <!-- rule:okf-untyped-links -->
76
122
  A markdown link asserts only "these two relate." The *kind* of relationship —
@@ -78,15 +124,60 @@ depends-on, supersedes, derived-from, owns — lives in the **prose around the
78
124
  link**, never in a made-up typed-edge syntax. Write the sentence that explains the
79
125
  relationship and put the link inside it. Prefer absolute bundle-relative targets
80
126
  (`/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 (§5.3), and `lint`'s
82
- backlog will surface it as demand.
127
+ does not exist yet is fine — it is not-yet-written knowledge (§6.1), and `lint`'s
128
+ backlog will surface it as demand. One more edge you get for free: a
129
+ `sources[].resource` naming another concept **is** a graph edge (§5.1's lineage),
130
+ so recording provenance is also linking.
83
131
 
84
- ### Provenance is what makes knowledge trustworthy8) <!-- check:uncited_external -->
132
+ ### Provenance lives in `sources`, attribution in footnotes 5.1) <!-- check:uncited_external -->
85
133
  Any external or empirical claim — a latency number, an approval, a quota, a
86
- "because X team decided Y" — should carry a citation to its source under a
87
- `# Citations` heading. Uncited claims are exactly how a bundle decays into folklore
88
- nobody trusts. `lint`'s provenance category exists to catch missing and broken
89
- citations; write them as you go so you never have to reconstruct them.
134
+ "because X team decided Y" — should be traceable to a `sources:` entry. Each
135
+ entry carries at least a `resource` (a URL, a bundle-relative path, or a scope
136
+ descriptor like `all queries in project X`), and optionally an `id`, `title`,
137
+ and the credibility signals `author` (an actor), `usage_count` (a liveness
138
+ signal, never a score), and `last_modified` (`YYYY-MM-DD`). To attribute a
139
+ *specific claim*, give the source an `id` and cite it with a markdown footnote
140
+ whose label is that id: `sharded daily.[^ga4-schema]` — keyed, not positional,
141
+ so a reordered list cannot misattribute silently. `lint`'s provenance category
142
+ checks the join in both directions (`unattributed_claim`, `unused_source`).
143
+ Uncited claims are exactly how a bundle decays into folklore nobody trusts.
144
+ ("Citations" is the legacy v0.1 spelling: provenance lived in a body
145
+ `# Citations` list, which v0.2 retires — §13.1 keeps it readable, and `lint`'s
146
+ Migration findings tell you what to move where.)
147
+
148
+ ### Say who did it, in §7's spelling <!-- check:unprefixed_actor -->
149
+ Every `by` — `generated.by`, `verified[].by` — takes an actor in one of §7's
150
+ three forms: `<producer>/<version>` (an automated producer, e.g.
151
+ `reference_agent/gemini-2.5-pro`), `human:<id>`, or `process:<id>`. The one
152
+ producer MUST: content a person hand-authored **or confirmed** is marked
153
+ `human:<id>` — §5.3 derives the trust tier from exactly this prefix, so a human
154
+ sign-off written `by: owner` silently reads as *machine-confirmed*, the exact
155
+ downgrade the tier system exists to prevent (`lint`'s `unprefixed_actor` nudges
156
+ it). `sources[].author` is looser on purpose: §7 does not govern it, and the
157
+ SPEC's own examples use `team:<id>` there — any honest attribution works.
158
+
159
+ ### Trust is derived, never stored (§5.2/§5.3) <!-- rule:okf-trust-derived -->
160
+ `generated` records how the current content was produced (`by` REQUIRED within;
161
+ `at` an ISO 8601 datetime); `verified` lists who confirmed it against its
162
+ sources — they stay separate because who *wrote* a concept need not be who
163
+ *checked* it. Consumers derive the tier: no `verified` ⇒ unverified; machine
164
+ actors only ⇒ machine-confirmed; any `human:` verifier ⇒ human-reviewed. Never
165
+ write a tier or a credibility score into frontmatter — it would be subjective,
166
+ unportable, and stale the moment the next verification lands. And never invent
167
+ provenance: a concept whose history you do not know is honestly unverified.
168
+ Prefer **block style** for these mappings (`generated:` with indented
169
+ `by:`/`at:`): the flow spelling (`{ by: human:x, at: …T10:00:00Z }`) is legal
170
+ YAML, but the colons inside its values trip older libyaml parsers, and block
171
+ style is the spelling every parser accepts.
172
+
173
+ ### Lifecycle: `status` and `stale_after` (§5.4/§5.5) <!-- rule:okf-lifecycle -->
174
+ `status` is one of `draft | stable | deprecated`; absent means `stable`, so
175
+ declare it only when it says something. `stale_after` is an absolute
176
+ `YYYY-MM-DD` — the author's own "do not trust me past this date", stale **on**
177
+ the day itself. Use it for knowledge with a known shelf life (a quota, a
178
+ migration window); `lint`'s `expired` check reports the ones whose date has
179
+ passed. It is a declared expiry, distinct from the `--stale-after` *flag*,
180
+ which is a reader-supplied age cutoff — see [cli.md](cli.md).
90
181
 
91
182
  ### Capture the non-obvious — not what code already says <!-- rule:okf-non-obvious -->
92
183
  A bundle that restates function signatures or config keys goes stale the moment
@@ -98,19 +189,46 @@ to it rather than duplicating it.
98
189
 
99
190
  ### Write for both readers at once <!-- rule:okf-dual-audience -->
100
191
  Use structural markdown so an agent can extract deterministically and a human can
101
- skim: headings, tables, fenced code, lists. Conventional headings a reader expects
102
- are `# Schema` (field/column tables), `# Examples`, and `# Citations`. Fill
103
- recommended frontmatter`title`, `description`, `tags`, `timestamp` (ISO 8601)
104
- whenever it aids consumption.
192
+ skim: headings, tables, fenced code, lists. Conventional headings a reader
193
+ expects are `# Schema` (field/column tables), `# Examples`, and on an Attested
194
+ Computation only`# Computation` (§10.3). Fill recommended frontmatter
195
+ `title`, `description`, `tags`, `generated` — whenever it aids consumption.
196
+
197
+ ### Attested computations are contracts (§10) <!-- rule:okf-computation-untouched -->
198
+ A concept of type `Attested Computation` carries a sanctioned computation:
199
+ `runtime` is REQUIRED (it fixes what `parameters` mean), the computation is
200
+ provided **exactly one way** — a body `# Computation` fence *or* a
201
+ `computation:` path, never both — and `executor`/`attester` name how it runs
202
+ and how a run is checked. The one new MUST NOT in v0.2 binds the *consumer*: an
203
+ agent MAY bind values for the declared `parameters` and MUST NOT author or edit
204
+ the computation itself. A "fixed" query silently stops being the one a person
205
+ sanctioned; if it is wrong, tell a human. The
206
+ [attested-computation template](../templates/attested-computation.md) carries
207
+ the whole contract.
105
208
 
106
209
  ### Reserved files <!-- rule:okf-reserved-files -->
107
210
  `index.md` is a directory listing and carries **no frontmatter** — with one
108
211
  exception: the **bundle-root** `index.md` is the only index that may carry
109
- frontmatter, and it may carry *only* `okf_version: "0.1"` (§11; `validate` §9.3
110
- flags any other key there). `log.md` is an ISO-dated change history, newest first.
111
- Never use these names for concepts. Templates:
112
- [concept](../templates/concept.md), nested [index](../templates/index.md),
113
- bundle-root [root-index](../templates/root-index.md), [log](../templates/log.md).
212
+ frontmatter, and it may carry *only* `okf_version: "0.2"` (§8 states the
213
+ exception inline; `validate` flags any other key there under §11 condition 3).
214
+ `log.md` is an ISO-dated change history, newest first. Never use these names for
215
+ concepts. Templates: [concept](../templates/concept.md),
216
+ [attested computation](../templates/attested-computation.md),
217
+ nested [index](../templates/index.md), bundle-root
218
+ [root-index](../templates/root-index.md), [log](../templates/log.md).
219
+
220
+ ## Migrating from v0.1 (§13)
221
+
222
+ A v0.1 bundle is consumable forever — §13.1 sanctions reading `timestamp` as
223
+ `generated.at` and a `# Citations` list as `sources` — so migration is
224
+ convenience, not rescue. Exactly two spellings retire: `timestamp` (move the
225
+ value under `generated: { by: <actor>, at: … }` — the actor is the one thing no
226
+ tool can derive; ask, or write `human:<maintainer>` when the history is human)
227
+ and the body `# Citations` section (lift each item into `sources`, add `id`s
228
+ and `[^id]` footnotes where the body cites, delete the section). `lint`'s two
229
+ Migration findings name the files; the [migrate playbook](../playbooks/migrate.md)
230
+ walks the rewrite, and `okf lint <dir> --only legacy_timestamp,legacy_citations
231
+ --fail-on info` is the mechanical done-check.
114
232
 
115
233
  ## Playbooks
116
234
 
@@ -130,9 +248,10 @@ grep can't:
130
248
  removed is updated; re-run `okf index` and eyeball each listing against reality.
131
249
  Easy to skip, expensive to miss — this is the check that was missing.
132
250
  - **`log.md`** — a dated entry, newest first.
133
- - **Timestamps** bumped on the concepts you touched.
134
- - **`validate`**zero §9 errors.
135
- - **`lint`** — cheap findings cleared; pass `--stale-after` when concepts carry
136
- timestamps (freshness is off by default).
251
+ - **`generated.at`** bumped on the concepts you touched (and `generated.by` says
252
+ who touched them you, in §7's spelling).
253
+ - **`validate`** — zero §11 errors.
254
+ - **`lint`** cheap findings cleared; `expired` reports out of the box, and
255
+ `--stale-after` adds the reader-side age cutoff when you want one.
137
256
  - **`loose` review + tag curation** — the two semantic passes (maintain steps 6–7);
138
257
  worth a pass in `produce` too on a non-trivial bundle.
@@ -65,7 +65,7 @@ or `okf index @` work from any directory, no path recall needed.
65
65
 
66
66
  **Exit codes:** `0` success · `1` non-conformant bundle (or a `lint --fail-on`
67
67
  threshold crossed) · `2` usage error. `graph`, `server`, and `render` are best-effort
68
- 9): a file the reader cannot use — frontmatter that will not parse, or a file it
68
+ 11): a file the reader cannot use — frontmatter that will not parse, or a file it
69
69
  cannot open at all — is skipped and noted on stderr, never fatal. The note counts;
70
70
  `validate` names each file and why.
71
71
 
@@ -74,43 +74,95 @@ cannot open at all — is skipped and noted on stderr, never fatal. The note cou
74
74
  or a mix — and it is a usage error (exit 2), never a silent answer about the first.
75
75
  To ask the same question of several bundles, ask `search`, or ask each in turn.
76
76
 
77
- ## validate — the hard gate (§9)
77
+ ## validate — the hard gate (§11)
78
78
 
79
- Implements the spec's §9 conformance definition exactly:
79
+ Implements the spec's §11 conformance definition exactly:
80
80
 
81
- - **§9.1** every non-reserved file has a parseable YAML frontmatter block;
82
- - **§9.2** every such block has a non-empty `type`;
83
- - **§9.3** any `index.md`/`log.md` present follows §67 (a nested `index.md` has
84
- no frontmatter, a root `index.md` carries only `okf_version`, `log.md` date
85
- headings are ISO `YYYY-MM-DD`).
81
+ - **§11 cond. 1** every non-reserved file has a parseable YAML frontmatter block;
82
+ - **§11 cond. 2** every such block has a non-empty `type`;
83
+ - **§11 cond. 3** any `index.md`/`log.md` present follows §89 (a nested
84
+ `index.md` has no frontmatter, a root `index.md` carries only `okf_version`,
85
+ `log.md` date headings are ISO `YYYY-MM-DD`).
86
86
 
87
87
  `ERROR`s are the three conditions above; the bundle is non-conformant until every
88
88
  one is fixed. `warn`s are soft — missing recommended fields, non-list tags, an
89
- unparseable timestamp, and **broken cross-links, which §5.3 explicitly tolerates**.
90
- Fix warnings when cheap; never block on them. Use `--json` in CI.
89
+ unparseable timestamp, **broken cross-links, which §6.1 explicitly tolerates**,
90
+ the shape of every §5/§10 family (`generated` not a mapping, a non-integer
91
+ `usage_count`, a `stale_after` that is not `YYYY-MM-DD`, a missing `runtime` on
92
+ an Attested Computation, …), and an `okf_version` the gem does not know (read
93
+ best-effort under §12; an absent one never warns). Absence of an optional family
94
+ is never a fault — a pure v0.1 bundle validates with zero warnings.
95
+
96
+ In `--json`, every warning carries `check` (a stable id) and `source` — `spec`
97
+ when the SPEC's own words state the rule, `convention` for a shape this gem asks
98
+ for beyond them (`verified[].by` presence, integer `usage_count`, a per-entry
99
+ `usage_window` mapping, `parameters[].name`, `executor`/`attester` `resource`).
100
+ Gate on `source` when you want only the spec-normative set; errors keep their
101
+ two-key `{ path, message }` shape. Fix warnings when cheap; never block on them.
102
+ Use `--json` in CI.
91
103
 
92
104
  ## lint — curation quality (advisory)
93
105
 
94
106
  Asks the complementary question to `validate`: not "is this legal OKF?" but "is
95
- this well-curated, navigable, trustworthy?" — precisely over the things §9 forbids
96
- `validate` from rejecting. It has its own report, never emits conformance errors,
97
- and **exits `0` even with findings** unless you pass `--fail-on warn`.
98
-
99
- Six conceptual categories, each backed by individual checks (names in parens):
100
-
101
- - **reachability** orphans, concepts not in any index, disconnected islands,
102
- and unlinked (degree-0) files
103
- (`orphan`, `not_in_index`, `disconnected_component`, `unlinked`)
104
- - **backlog** demand-ranked missing concepts (linked-to but absent), broken index entries
105
- (`missing_concept`, `broken_index_entry`)
106
- - **completeness** stubs, missing `title` / `description` / `timestamp`
107
- (`stub`, `missing_title`, `missing_description`, `missing_timestamp`)
108
- - **freshness** — concepts older than a cutoff (`stale`) — **only computed when you
109
- pass `--stale-after`; a plain `okf lint` never reports staleness at all**
110
- - **provenance** — uncited external claims, broken citations, spec §8
111
- (`uncited_external`, `broken_citation`)
112
- - **hygiene** duplicate titles, unused/undefined reference links, self-links
113
- (`duplicate_title`, `unused_reference_def`, `undefined_reference`, `self_link`)
107
+ this well-curated, navigable, trustworthy?" — precisely over the things §11
108
+ forbids `validate` from rejecting. It has its own report, never emits
109
+ conformance errors, and **exits `0` even with findings** unless you pass
110
+ `--fail-on warn` (exit 1 on any `warn` finding) or `--fail-on info` (exit 1 on
111
+ any finding at all).
112
+
113
+ **Severity is API.** Every check has a pinned level — `warn` or `info` — and
114
+ machine consumers gate on it, so the levels below are stable, not advisory. A
115
+ finding you want to gate on that is `info` gets `--fail-on info` (usually with
116
+ `--only`), never a hope that its severity changes.
117
+
118
+ Eight categories, each backed by individual checks (severity in brackets):
119
+
120
+ - **Reachability** — `orphan` [warn], `not_in_index` [warn],
121
+ `disconnected_component` [info], `unlinked` [info]
122
+ - **Backlog** — `missing_concept` [info], `broken_index_entry` [warn]
123
+ - **Completeness** — `stub` [info], `missing_title` [info],
124
+ `missing_description` [info], `missing_generated` [info] (quiet on either
125
+ spelling a legacy `timestamp` still counts as a recorded change)
126
+ - **Freshness** — `expired` [info] (§5.5: past the concept's own declared
127
+ `stale_after`, on the day itself), `stale` [warn] (older than the
128
+ reader-supplied `--stale-after` cutoff, keyed on `generated_at`)
129
+ - **Provenance** — `uncited_external` [info] (external body links and no
130
+ sources, in either spelling), `broken_source` [warn] (an in-bundle `.md`
131
+ source target that names no concept; URLs and scope descriptors are out of
132
+ scope, and a non-`.md` asset is out of reach — the reader models concepts,
133
+ so lint never sees the file; `okf references` is the view that checks those
134
+ pointers), `unattributed_claim` [warn] (a footnote
135
+ no `sources[].id` answers — it *misattributes* a claim, which is why it
136
+ outranks its join-twin), `unused_source` [info] (a keyed source no footnote
137
+ cites — slack, not a defect), `unprefixed_actor` [info] (a `verified[].by`
138
+ outside §7's three forms reads as machine-confirmed; a `generated.by`
139
+ outside them feeds no tier but leaves a reader unable to tell a person
140
+ from a process; info so it informs, never blocks). A missing `generated.by` is the *validator's* warning —
141
+ REQUIRED-within is shape, not curation — so lint never double-reports it
142
+ - **Attestation** — `incomplete_computation` [warn] (an Attested Computation
143
+ providing its computation neither way, or both ways — §10.3 says a
144
+ `computation:` path is used *instead of* the body fence),
145
+ `broken_attestation_ref` [warn] (on an `Attested Computation`, a
146
+ `computation`, `executor.resource` or `attester.resource` naming an
147
+ in-bundle `.md` that is not there — a contract no consumer can follow; the
148
+ keys are read only on that type, since §4.1 lets any other concept use them
149
+ for its own purpose). Its reach is exactly the `.md` files: URLs are out
150
+ of scope, and a `.sql` or `.py` target is invisible to *every* check here,
151
+ because the linter reads the concept model and the model carries only
152
+ markdown — `okf references` is the surface that sees those files and reports
153
+ a pointer that misses, whatever the extension. Remember §6.2 reads a bare
154
+ `references/…` as relative to the concept, so from a nested concept it wants
155
+ the leading `/`
156
+ - **Migration** — `legacy_timestamp` [info], `legacy_citations` [info]: one
157
+ finding per bundle naming the files still in a retired v0.1 spelling, with
158
+ the rewrite instructions in the message. Info on purpose — §13 says a v0.1
159
+ bundle is consumable forever, so `--fail-on warn` must not turn red on one.
160
+ A migration campaign gates explicitly:
161
+ `okf lint <dir> --only legacy_timestamp,legacy_citations --fail-on info`,
162
+ exit 1 until clean.
163
+ - **Hygiene** — `duplicate_title` [info], `unused_reference_def` [info],
164
+ `undefined_reference` [warn], `self_link` [info], `log_order` [info] (§9
165
+ reads a log newest-first; disorder is slack, never a §11 error)
114
166
 
115
167
  `--only` / `--except` filter by the **individual check names above**, not the
116
168
  category labels — `okf lint <dir> --only orphan,stub` works; `--only reachability`
@@ -119,6 +171,26 @@ threshold in characters (default 50), and `--stale-after DUR` sets the `stale`
119
171
  cutoff — a duration like `90d` or `12w`, or an ISO date like `2026-01-01` (a bare
120
172
  number is rejected).
121
173
 
174
+ **Two different clocks, one unlucky name.** The `--stale-after` *flag* and the
175
+ `stale_after:` *frontmatter field* are different mechanisms that happen to share
176
+ a spelling. The flag is the **reader's** age cutoff: "flag anything not touched
177
+ since DUR", keyed on `generated_at`, feeding the `stale` check. The field is the
178
+ **author's** declared expiry: "do not trust this past DATE", feeding the
179
+ `expired` check. Never read one as the other, and never show them adjacent
180
+ without the distinction.
181
+
182
+ **The clock is explicit.** `expired` compares against a day the CLI supplies —
183
+ today by default, or `--today YYYY-MM-DD` for a reproducible report (CI wants
184
+ this). The pure library runs no clock check unless handed `today:`, and every
185
+ clock-gated check that was selected but could not run is *named* in
186
+ `stats.skipped_checks` (the human report prints one `skipped:` line) — a gate
187
+ that is sometimes absent and does not confess converts "unchecked" into
188
+ "checked and fine".
189
+
190
+ The report's stats carry the bundle's posture too: `trust` (the §5.3 tier
191
+ distribution, in the hyphenated wire spelling) and `status` (effective-status
192
+ frequency).
193
+
122
194
  `lint --json` is the structured substrate you consume to reason about the two
123
195
  things lint deliberately does **not** compute — contradictions and *semantic*
124
196
  staleness — which need understanding of meaning.
@@ -150,8 +222,10 @@ as Ruby regular expressions with `--regexp`/`-e` (an invalid pattern is a usage
150
222
  error, exit 2). `--fuzzy` forgives typos; pairing it with `-e` is a usage error,
151
223
  since a pattern is matched literally rather than by edit distance.
152
224
  `--in a,b` restricts the searched fields (title, id, tags, type, description,
153
- body); the shared `--type/--dir/--tag` filters narrow the candidates *first*,
154
- so a search scoped by what `index` taught you stays surgical.
225
+ sources, body `sources` is each entry's title and resource joined, so a
226
+ migrated bundle keeps the recall its `# Citations` body text used to give it);
227
+ the shared `--type/--dir/--tag/--status/--trust` filters narrow the candidates
228
+ *first*, so a search scoped by what `index` taught you stays surgical.
155
229
 
156
230
  **The default is exact, so an exact query means what it looks like.** A phrase in
157
231
  one argument (`"dedup key"`), a dotted version (`7.2.0`), an underscored
@@ -261,7 +335,7 @@ same-id concepts from different bundles become indistinguishable. The retrieval
261
335
  map first, finder second, bodies last — is the
262
336
  [search playbook](../playbooks/search.md).
263
337
 
264
- ## index — the progressive-disclosure map (§6)
338
+ ## index — the progressive-disclosure map (§8)
265
339
 
266
340
  The "orient before you read" view, and the read verb that sees the layer the
267
341
  concept views can't: `index.md` files are reserved/structural, so
@@ -300,7 +374,7 @@ and `--except body,listing` on top of either is the lean JSON skeleton. Full
300
374
  `--no-body` drops the prose to a
301
375
  skeleton (headers, rollups, child pointers). For a directory that has concepts but
302
376
  **no `index.md`**, the listing is **synthesized** from the concepts' descriptions
303
- and tagged `(no index.md)` — §6 explicitly permits synthesizing a map on the fly.
377
+ and tagged `(no index.md)` — §8 explicitly permits synthesizing a map on the fly.
304
378
 
305
379
  It is a **read view**: advisory, always exit 0. A synthesized directory is a
306
380
  *signal* (a map worth writing), never a defect — `index` emits no lint findings and
@@ -357,11 +431,22 @@ verbs reproduce them on the CLI so an agent can read a bundle without a browser.
357
431
  All are advisory reads (exit 0) sharing one data source (per-concept metadata plus
358
432
  in/out link degree). Add `--json` to any for a machine substrate.
359
433
 
360
- - **`catalog`** — every concept with its metadata (type, status, tags, timestamp,
361
- in/out link degree, description), grouped by top-level dir (`dir` on every row
362
- carries the full path, `top_dir` the first segment). The "what's here, in
363
- detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type, description,
364
- tags, timestamp, status, backlog_ref, dir, top_dir, links_out, links_in }] }`.
434
+ - **`catalog`** — every concept with its metadata (type, status, trust, tags,
435
+ provenance, in/out link degree, description), grouped by top-level dir (`dir`
436
+ on every row carries the full path, `top_dir` the first segment). The "what's
437
+ here, in detail" view. JSON: `{ bundle, count, concepts: [{ id, title, type,
438
+ description, tags, generated_at, generated_by, generated, trust, status,
439
+ stale_after, sources, backlog_ref, dir, top_dir, links_out, links_in }] }`.
440
+ Four of those deserve a sentence: `generated` is the raw boolean ("does the
441
+ document *declare* a generated mapping"), which is what tells hand-written
442
+ apart from v0.1-with-timestamp — `generated_at` alone conflates them, because
443
+ §13.1 lifts a legacy `timestamp` into it. `trust` is the derived §5.3 tier as
444
+ a hyphenated literal (`unverified` | `machine-confirmed` | `human-reviewed`) —
445
+ compare against exactly those. `status` is the *declared* value, `null` when
446
+ absent (the row never fabricates frontmatter; the `--status` filter is what
447
+ applies the §5.4 default). `sources` is a count. Temporal fields render
448
+ ISO 8601 (`stale_after` as `YYYY-MM-DD`). The `timestamp` column is retired —
449
+ `--fields timestamp` is a usage error naming the valid fields.
365
450
  - **`files`** — the folder tree: each concept's filename + title, grouped by
366
451
  directory. The "how it's organised" view. JSON: `{ bundle, count, files: [{ path,
367
452
  id, dir, type, title, description }] }`.
@@ -389,8 +474,14 @@ in/out link degree). Add `--json` to any for a machine substrate.
389
474
  can address.
390
475
 
391
476
  The four list views narrow with the same filters the browser panels offer —
392
- `--type TYPE`, `--dir PATH`, `--tag TAG`; each takes the ones orthogonal to
393
- itself (`tags` can't filter by tag). Matching is case-insensitive; `--type` and
477
+ `--type TYPE`, `--dir PATH`, `--tag TAG`, `--status STATUS`, `--trust TIER`
478
+ (`search` takes them too); each takes the ones orthogonal to itself (`tags`
479
+ can't filter by tag). `--status` matches the *effective* status (absent reads
480
+ `stable`, §5.4) and `--trust` the derived tier, either spelling
481
+ (`machine-confirmed` or `machine_confirmed`) — on a v0.1 bundle
482
+ `--status stable` and `--trust unverified` match everything, which is §13.1
483
+ reading, not an error, and an unknown value matches nothing at exit 0.
484
+ Matching is case-insensitive; `--type` and
394
485
  `--tag` are exact, `--dir` takes the named directory **and everything below it**
395
486
  (`--dir platform` reaches `platform/services/api`). A concept at the bundle root
396
487
  lives in `.`, which `--dir` also accepts as plain `root` (no shell quoting) —
@@ -411,6 +502,28 @@ plus whatever the other flag selected: an answer to neither question.
411
502
  Reach for `stats` first to size a bundle, `catalog`/`files` to enumerate it, `tags`
412
503
  to find thematic clusters — all without standing up the server.
413
504
 
505
+ ## references — the `references/` inventory (§6.3)
506
+
507
+ Lists every file under `references/` — including the non-markdown ones no other
508
+ verb can see, since the concept model carries only markdown — with which
509
+ concepts cite each file through the §6.2 path-valued fields (`resource`,
510
+ `sources[].resource`, `computation`, `executor.resource`, `attester.resource`),
511
+ plus every pointer into `references/` that resolves to nothing. Advisory:
512
+ **exits `0`** even with dangling pointers — the findings are the output. JSON:
513
+ `{ bundle, dangling, count, references: [{ path, dir, kind, referenced_by }] }`,
514
+ with `--fields`/`--except` projecting the rows. A file that is itself a concept
515
+ (§6.3 allows both) is marked `kind: "concept"`; body links are the graph's
516
+ business and are not counted here.
517
+
518
+ **The dangling list is where §6.2's bare-path trap surfaces.** A bare
519
+ `references/attesters/rev.py` written from `metrics/` resolves relative to the
520
+ concept — `metrics/references/attesters/rev.py`, nothing — and when the
521
+ leading-slash spelling would have hit, the entry says so:
522
+ `/references/attesters/rev.py exists — missing leading slash?`. Reach is any
523
+ extension, which is exactly what `broken_source` and `broken_attestation_ref`
524
+ cannot offer (their exemptions above), so run it wherever a bundle carries
525
+ attester code or computation files.
526
+
414
527
  ## server — interactive graph server
415
528
 
416
529
  Starts a local HTTP server (`okf server <dir>`; `-p`/`--port`, default 8808, and
@@ -429,7 +542,7 @@ select — depth 1 is the flat view, and a flat bundle is offered no control. Th
429
542
  UI too: the Files view carries **Files | Indexes** tabs — the Indexes tab
430
543
  lists the log first (the chronological index), then every `index.md` — and
431
544
  folder nodes in file-tree mode and directory boxes in cluster mode open a
432
- directory's §6 map in the inspector (authored, or synthesized when none
545
+ directory's §8 map in the inspector (authored, or synthesized when none
433
546
  exists). Links to an `index.md`, `log.md`, or bare directory navigate instead
434
547
  of dead-ending, and the log is fetched fresh on every read, so a
435
548
  just-appended entry shows without a restart. `?view=index` jumps straight to
@@ -552,7 +665,7 @@ Prints the node/edge graph. `--json` emits a machine-readable dump — the
552
665
  `id`/`type`/`title`/`description`/`tags` **and, by default, every `body`** — the
553
666
  part that dominates the bytes on a real bundle) plus `edges` — you can pipe into
554
667
  other analysis. A concept with a missing *or blank* `type` indexes under
555
- `Untyped`: §9.2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
668
+ `Untyped`: §11 condition 2 rejects both identically, so both land in one bucket. To *plan* a traversal, structure is all you need: `--no-body`
556
669
  drops each node's body, and `--minimal` ships only `id`/`title` plus the type/tag
557
670
  indexes — the lean shape the `server` page boots from. Reach for the full dump
558
671
  only when the task truly consumes every body; for one question, the
@@ -0,0 +1,41 @@
1
+ ---
2
+ type: Attested Computation
3
+ title: <What the computation answers, e.g. Revenue for fiscal year>
4
+ description: <Single sentence: what this computes and per whose definition.>
5
+ tags: [<tag>]
6
+ runtime: <REQUIRED — what runs it and what parameters mean: bigquery, dbt, python, …>
7
+ parameters:
8
+ - { name: <name>, type: <type>, required: true }
9
+ executor:
10
+ resource: <path to run instructions, e.g. /references/skills/run-on-bq.md>
11
+ receipt: [<field>, <field>]
12
+ attester:
13
+ resource: <path to the deterministic check, e.g. /references/attesters/check.py>
14
+ generated:
15
+ by: <actor per §7>
16
+ at: <ISO 8601>
17
+ sources:
18
+ - id: <stable-key>
19
+ title: <the definition this computation implements>
20
+ resource: <url or path>
21
+ ---
22
+
23
+ # Computation
24
+
25
+ <The sanctioned computation, in one fenced block — OR set `computation: <path>`
26
+ in the frontmatter instead and omit this section entirely (§10.3: a path is
27
+ used *instead of* the fence; providing both leaves a consumer two candidate
28
+ computations and no rule for which was sanctioned).>
29
+
30
+ ```<language>
31
+ <the computation>
32
+ ```
33
+
34
+ <Prose around it may cite sources per claim.[^<stable-key>]>
35
+
36
+ <!-- rule:okf-computation-untouched -->
37
+ <!-- §10.3, v0.2's only new MUST NOT: an agent MAY bind values for the declared
38
+ `parameters` and MUST NOT author or edit the computation itself. The whole
39
+ point of the type is that the computation is sanctioned — reviewed by a
40
+ person — and an improvised or "fixed" query silently loses that status.
41
+ If the computation is wrong, say so to a human; do not repair it. -->
@@ -4,12 +4,23 @@ title: <Human-readable display name>
4
4
  description: <Single sentence summarizing the concept.>
5
5
  resource: <Canonical URI of the underlying asset — omit for abstract concepts>
6
6
  tags: [<tag>, <tag>]
7
- timestamp: <ISO 8601, e.g. 2026-06-14T10:00:00Z>
7
+ generated:
8
+ by: <actor per §7 — human:<id>, process:<id>, or <producer>/<version>>
9
+ at: <ISO 8601, e.g. 2026-06-14T10:00:00Z>
10
+ sources:
11
+ - id: <stable-key-the-body-cites>
12
+ title: <source title>
13
+ resource: <url, bundle-relative path, or scope descriptor>
14
+ # Optional §5 families, when they apply:
15
+ # verified: [{ by: <actor>, at: <ISO 8601> }] — who confirmed the content (a bare mapping reads as a one-element list)
16
+ # status: draft | stable | deprecated — absent means stable
17
+ # stale_after: YYYY-MM-DD — stale on the day itself
8
18
  ---
9
19
 
10
20
  # Overview
11
21
 
12
- <What this concept is and why it matters.>
22
+ <What this concept is and why it matters. Attribute each empirical claim to a
23
+ source by its id: "The events table is sharded daily.[^<stable-key-the-body-cites>]">
13
24
 
14
25
  # Schema
15
26
 
@@ -18,7 +29,3 @@ timestamp: <ISO 8601, e.g. 2026-06-14T10:00:00Z>
18
29
  | Field | Type | Description |
19
30
  |-------|------|-------------|
20
31
  | | | |
21
-
22
- # Citations
23
-
24
- [1] [<source title>](<url>)
@@ -1,5 +1,5 @@
1
1
  ---
2
- okf_version: "0.1"
2
+ okf_version: "0.2"
3
3
  ---
4
4
 
5
5
  # <Directory / Group Heading>
data/lib/okf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OKF
4
- VERSION = "1.13.0"
4
+ VERSION = "2.0.0"
5
5
  end