okf 1.12.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/README.md +94 -466
  4. data/lib/okf/bundle/folder.rb +48 -3
  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 +47 -18
  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 +124 -8
  15. data/lib/okf/cli/catalog.rb +2 -2
  16. data/lib/okf/cli/command.rb +93 -19
  17. data/lib/okf/cli/dirs.rb +1 -1
  18. data/lib/okf/cli/files.rb +2 -2
  19. data/lib/okf/cli/index.rb +3 -3
  20. data/lib/okf/cli/lint.rb +70 -12
  21. data/lib/okf/cli/references.rb +97 -0
  22. data/lib/okf/cli/search.rb +23 -8
  23. data/lib/okf/cli/stats.rb +3 -39
  24. data/lib/okf/cli/tags.rb +6 -43
  25. data/lib/okf/cli/types.rb +1 -1
  26. data/lib/okf/cli/validate.rb +3 -3
  27. data/lib/okf/cli.rb +4 -1
  28. data/lib/okf/concept/file.rb +17 -2
  29. data/lib/okf/concept.rb +362 -10
  30. data/lib/okf/markdown/citations.rb +41 -4
  31. data/lib/okf/markdown/frontmatter.rb +1 -1
  32. data/lib/okf/markdown/links.rb +67 -7
  33. data/lib/okf/path.rb +17 -3
  34. data/lib/okf/render/graph/template.html.erb +173 -41
  35. data/lib/okf/render/graph.rb +11 -3
  36. data/lib/okf/safe_read.rb +50 -0
  37. data/lib/okf/server/app.rb +47 -15
  38. data/lib/okf/server/hub.rb +1 -1
  39. data/lib/okf/skill/SKILL.md +14 -12
  40. data/lib/okf/skill/playbooks/curate.md +8 -3
  41. data/lib/okf/skill/playbooks/doctor.md +3 -1
  42. data/lib/okf/skill/playbooks/maintain.md +7 -6
  43. data/lib/okf/skill/playbooks/menu.md +5 -4
  44. data/lib/okf/skill/playbooks/migrate.md +31 -8
  45. data/lib/okf/skill/playbooks/produce.md +16 -9
  46. data/lib/okf/skill/playbooks/search.md +2 -2
  47. data/lib/okf/skill/reference/SPEC.md +739 -187
  48. data/lib/okf/skill/reference/authoring.md +154 -35
  49. data/lib/okf/skill/reference/cli.md +160 -44
  50. data/lib/okf/skill/templates/attested-computation.md +41 -0
  51. data/lib/okf/skill/templates/concept.md +13 -6
  52. data/lib/okf/skill/templates/root-index.md +1 -1
  53. data/lib/okf/version.rb +1 -1
  54. data/lib/okf.rb +23 -2
  55. metadata +7 -3
  56. data/CODE_OF_CONDUCT.md +0 -10
@@ -54,10 +54,30 @@ module OKF
54
54
  @bundle.hubs
55
55
  end
56
56
 
57
+ def directories
58
+ @bundle.directories
59
+ end
60
+
57
61
  def directory_index
58
62
  @bundle.directory_index
59
63
  end
60
64
 
65
+ def stats
66
+ @bundle.stats
67
+ end
68
+
69
+ def tag_groups(by:, entries: nil)
70
+ @bundle.tag_groups(by: by, entries: entries)
71
+ end
72
+
73
+ # The §6.3 inventory — this handle's half is the disk: the reader models
74
+ # only markdown, so listing `references/` here is how a .py attester or a
75
+ # .sql computation becomes visible at all. The pure model gets the
76
+ # manifest and answers the rest (citers, dangling pointers).
77
+ def references
78
+ Bundle::References.build(@bundle, files: reference_files)
79
+ end
80
+
61
81
  # Every log.md with its content, root scope first — read live from disk so a
62
82
  # just-appended entry shows without a reload; the reserved snapshot is the
63
83
  # fallback if the file has since vanished. Shared by `okf render`'s bake
@@ -104,7 +124,18 @@ module OKF
104
124
  Concept::File.read(root: @root, path: path)
105
125
  end
106
126
 
107
- # Materialize the in-memory bundle to disk (Writer validates §9 before
127
+ # The raw markdown bytes for one concept id read once through the same
128
+ # containment guard as #concept, but without the parse #concept pays for,
129
+ # so a caller that wants the file verbatim (never a re-serialized copy)
130
+ # does one read, not a read plus a discarded frontmatter parse. nil when no
131
+ # concept has that id; raises Path::Error if the file has become a symlink
132
+ # escaping the root, and the reader's own SystemCallError if it has gone.
133
+ def concept_source(id)
134
+ path = @bundle.paths_by_id[id] or return nil
135
+ Concept::File.new(root: @root, path: path).read
136
+ end
137
+
138
+ # Materialize the in-memory bundle to disk (Writer validates §11 before
108
139
  # publishing, so a malformed bundle is never written).
109
140
  def save(overwrite: false)
110
141
  Writer.call(
@@ -124,6 +155,16 @@ module OKF
124
155
 
125
156
  private
126
157
 
158
+ # Bundle-relative paths of every file under references/, sorted. A glob
159
+ # on a directory that does not exist is simply empty — no references/ and
160
+ # an empty references/ are the same inventory.
161
+ def reference_files
162
+ Dir.glob(File.join(@root, "references", "**", "*"))
163
+ .select { |path| File.file?(path) }
164
+ .map { |path| Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s }
165
+ .sort
166
+ end
167
+
127
168
  def reserved_hash(basename)
128
169
  @bundle.reserved
129
170
  .select { |entry| File.basename(entry.path) == basename }
@@ -131,8 +172,12 @@ module OKF
131
172
  end
132
173
 
133
174
  def log_content(path)
134
- File.read(File.join(@root, path), encoding: "UTF-8")
135
- rescue SystemCallError
175
+ # Live, but through the same containment as every other read: a log.md
176
+ # that was a real file at boot and is a symlink out of the root now falls
177
+ # back to the boot snapshot rather than serving the target — the same
178
+ # answer a vanished file gets, since an escape is a file we must not read.
179
+ SafeRead.read!(@root, File.join(@root, path))
180
+ rescue SystemCallError, Path::Error
136
181
  @bundle.reserved_content(path)
137
182
  end
138
183
  end
@@ -20,7 +20,7 @@ module OKF
20
20
  attr_reader :nodes, :edges, :type_index, :tag_index
21
21
 
22
22
  def self.build(bundle, minimal: false, body: true)
23
- # Best-effort (§9): a malformed concept never reaches here — the reader keeps
23
+ # Best-effort (§11): a malformed concept never reaches here — the reader keeps
24
24
  # it in bundle.unparseable — so the rest of the bundle still renders. Inspect
25
25
  # bundle.unparseable to detect skips.
26
26
  concepts = bundle.concepts
@@ -51,10 +51,19 @@ module OKF
51
51
  # Edges resolve by *path* — a markdown link is a file path — then map that path
52
52
  # to the concept living there and use its id, so a frontmatter `id` that differs
53
53
  # from the path still lands the edge on the right node.
54
+ # Body links and sources[].resource entries feed the same resolver: §5.1
55
+ # says a `resource` naming another concept is a derivation edge that
56
+ # "already exists in the bundle graph", and this is what makes that true —
57
+ # it is also what keeps a migrated bundle's graph equal to its v0.1 twin's,
58
+ # since a `# Citations` in-bundle link stops being a body link the moment
59
+ # it is lifted into frontmatter. URLs and scope descriptors resolve to
60
+ # nothing; an unresolvable path is broken_source's to report.
54
61
  def self.edges_for(concepts, id_by_path, root)
55
62
  seen = Set.new
56
63
  concepts.each_with_object([]) do |concept, edges|
57
- Markdown::Links.extract(concept.body).each do |raw|
64
+ targets = Markdown::Links.extract(concept.body) +
65
+ concept.sources.map { |source| source["resource"] }.compact
66
+ targets.each do |raw|
58
67
  resolved = Markdown::Links.resolve(raw, from: concept.path, bundle: root)
59
68
  next if resolved.nil?
60
69
 
@@ -86,7 +95,7 @@ module OKF
86
95
  concept.tags.is_a?(Array) ? concept.tags : []
87
96
  end
88
97
 
89
- # Blank, not just nil: §9.2 makes a whitespace-only `type` as non-conformant
98
+ # Blank, not just nil: §11 condition 2 makes a whitespace-only `type` as non-conformant
90
99
  # as a missing one (the validator says so with the same OKF.blank?), so the
91
100
  # index must not sort them into different buckets. Otherwise `type: " "`
92
101
  # earns its own row, labelled with spaces, next to Untyped.