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
data/lib/okf/markdown/links.rb
CHANGED
|
@@ -4,7 +4,7 @@ module OKF
|
|
|
4
4
|
module Markdown
|
|
5
5
|
# Markdown cross-link extraction and resolution — the single source of truth for
|
|
6
6
|
# "which concepts does this body point at". Shared by OKF::Bundle::Graph (to build edges)
|
|
7
|
-
# and OKF::Bundle::Validator (to warn on broken cross-links, §
|
|
7
|
+
# and OKF::Bundle::Validator (to warn on broken cross-links, §6.1), so both agree on what
|
|
8
8
|
# counts as a link and where it resolves.
|
|
9
9
|
module Links
|
|
10
10
|
FENCE = /\A(```|~~~)/.freeze
|
|
@@ -14,13 +14,34 @@ module OKF
|
|
|
14
14
|
# blanked before scanning — the inline analogue of FENCE.
|
|
15
15
|
CODE_SPAN = /(`+).*?\1/.freeze
|
|
16
16
|
# Inline link [text](target) or [text](target "title"); (?<!!) skips images.
|
|
17
|
-
|
|
17
|
+
# group 1 = the link text, group 2 = the target: extract() wants only the
|
|
18
|
+
# target, Citations.entries carries the text into a source's `title` —
|
|
19
|
+
# one grammar, one regex.
|
|
20
|
+
INLINE_LINK = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/.freeze
|
|
18
21
|
# Reference-style use: full [text][label] or collapsed [label][]; (?<!!) skips
|
|
19
22
|
# images. group 1 = text/label, group 2 = the explicit label (empty if collapsed).
|
|
20
23
|
REFERENCE_LINK = /(?<!!)\[([^\]]*)\]\[([^\]]*)\]/.freeze
|
|
21
24
|
# Reference definition: [label]: target (optionally followed by a "title").
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
# A label may not begin with `^`: that is a footnote definition (§5.1
|
|
26
|
+
# per-claim attribution), which would otherwise read as a reference
|
|
27
|
+
# definition nothing ever uses.
|
|
28
|
+
DEFINITION = /\A[ \t]{0,3}\[([^\^\]][^\]]*)\]:[ \t]*(\S+)/.freeze
|
|
29
|
+
# In-prose footnote reference [^label] — §5.1 joins it on a sources[].id;
|
|
30
|
+
# (?<!!) skips an image whose alt text happens to start with a caret.
|
|
31
|
+
FOOTNOTE_REFERENCE = /(?<!!)\[\^([^\]\s]+)\]/.freeze
|
|
32
|
+
# A footnote definition line ([^label]: prose) — never a reference.
|
|
33
|
+
FOOTNOTE_DEFINITION = /\A[ \t]{0,3}\[\^([^\]\s]+)\]:/.freeze
|
|
34
|
+
# The URI scheme grammar, in one source string the citation item
|
|
35
|
+
# regexes compose from — schemes are case-insensitive (RFC 3986), and
|
|
36
|
+
# answering the case question in two places had HTTP:// counted as
|
|
37
|
+
# provenance by Citations and as prose by this module.
|
|
38
|
+
SCHEME_NAME = "[a-zA-Z][a-zA-Z0-9+.-]*"
|
|
39
|
+
SCHEME = %r{\A#{SCHEME_NAME}://}.freeze
|
|
40
|
+
# mailto has no ://, so SCHEME cannot see it — and a scheme name is as
|
|
41
|
+
# case-insensitive here as everywhere else. This guard sat inline and
|
|
42
|
+
# case-sensitive in three places; `MAILTO:user@example.md` then passed
|
|
43
|
+
# both gates and resolved as a relative path.
|
|
44
|
+
MAILTO = /\Amailto:/i.freeze
|
|
24
45
|
|
|
25
46
|
module_function
|
|
26
47
|
|
|
@@ -33,7 +54,7 @@ module OKF
|
|
|
33
54
|
definitions = reference_definitions(text)
|
|
34
55
|
found = []
|
|
35
56
|
each_prose_line(text) do |line|
|
|
36
|
-
|
|
57
|
+
line.scan(INLINE_LINK) { |_text, target| found << target }
|
|
37
58
|
line.scan(REFERENCE_LINK).each do |label, explicit|
|
|
38
59
|
key = (explicit.empty? ? label : explicit).strip.downcase
|
|
39
60
|
target = definitions[key]
|
|
@@ -55,6 +76,34 @@ module OKF
|
|
|
55
76
|
definitions
|
|
56
77
|
end
|
|
57
78
|
|
|
79
|
+
# The distinct footnote labels referenced in prose (§5.1), in document
|
|
80
|
+
# order. A definition's own leading token is not a reference — a source
|
|
81
|
+
# cited only by `[^a]:` itself is still uncited — but the prose *after*
|
|
82
|
+
# it is prose like any other: `[^a]: see also [^b]` cites b, and skipping
|
|
83
|
+
# the whole line made that citation invisible to both provenance checks.
|
|
84
|
+
# Labels are deduplicated so one unmatched label yields one finding, not
|
|
85
|
+
# one per use.
|
|
86
|
+
def footnote_references(text)
|
|
87
|
+
labels = []
|
|
88
|
+
each_prose_line(text) do |line|
|
|
89
|
+
line.sub(FOOTNOTE_DEFINITION, "").scan(FOOTNOTE_REFERENCE) { |captures| labels << captures.first }
|
|
90
|
+
end
|
|
91
|
+
labels.uniq
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The footnote labels a body *defines* (`[^label]: …`), deduplicated. A
|
|
95
|
+
# label with a definition is a self-contained GFM content footnote;
|
|
96
|
+
# §5.1's keyed attribution never reserves the whole label space, so the
|
|
97
|
+
# provenance checks treat only undefined, unmatched labels as dangling.
|
|
98
|
+
def footnote_definitions(text)
|
|
99
|
+
labels = []
|
|
100
|
+
each_prose_line(text) do |line|
|
|
101
|
+
match = FOOTNOTE_DEFINITION.match(line)
|
|
102
|
+
labels << match[1] if match
|
|
103
|
+
end
|
|
104
|
+
labels.uniq
|
|
105
|
+
end
|
|
106
|
+
|
|
58
107
|
# Yield each line outside a fenced code block, with inline code spans blanked.
|
|
59
108
|
# Both exclusions mirror the rendered document: fenced and inline code are
|
|
60
109
|
# literal text, so a link written inside them is not a cross-link.
|
|
@@ -79,9 +128,20 @@ module OKF
|
|
|
79
128
|
# @param bundle [String] path to the bundle root
|
|
80
129
|
def resolve(raw, from:, bundle:)
|
|
81
130
|
target = raw.to_s.split("#", 2).first.to_s
|
|
82
|
-
return nil if target.empty? || target.end_with?("/")
|
|
83
|
-
return nil if target.match?(SCHEME) || target.start_with?("mailto:")
|
|
84
131
|
return nil unless target.end_with?(".md")
|
|
132
|
+
|
|
133
|
+
resolve_path(raw, from: from, bundle: bundle)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The path arithmetic under #resolve without its +.md+ gate — the resolver
|
|
137
|
+
# for §6.2's path-valued frontmatter fields (resource, sources[].resource,
|
|
138
|
+
# computation, executor.resource, attester.resource), which accept any
|
|
139
|
+
# file. Body cross-links stay .md-only through #resolve; keeping the gate
|
|
140
|
+
# there and not here is what stops the two rules from trading places.
|
|
141
|
+
def resolve_path(raw, from:, bundle:)
|
|
142
|
+
target = raw.to_s.split("#", 2).first.to_s
|
|
143
|
+
return nil if target.empty? || target.end_with?("/")
|
|
144
|
+
return nil if target.match?(SCHEME) || target.match?(MAILTO)
|
|
85
145
|
return target.sub(%r{\A/+}, "") if target.start_with?("/")
|
|
86
146
|
|
|
87
147
|
bundle_abs = File.expand_path(bundle)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<meta property="og:site_name" content="OKF">
|
|
9
9
|
<meta property="og:title" content="<%= og_title %>">
|
|
10
10
|
<meta property="og:description" content="<%= og_desc %>">
|
|
11
|
-
<meta property="og:image" content="https://okfgem.com/og-demo-
|
|
11
|
+
<meta property="og:image" content="https://okfgem.com/og-demo-v5.png">
|
|
12
12
|
<meta property="og:image:type" content="image/png">
|
|
13
13
|
<meta property="og:image:width" content="1200">
|
|
14
14
|
<meta property="og:image:height" content="630">
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<meta name="twitter:card" content="summary_large_image">
|
|
17
17
|
<meta name="twitter:title" content="<%= og_title %>">
|
|
18
18
|
<meta name="twitter:description" content="<%= og_desc %>">
|
|
19
|
-
<meta name="twitter:image" content="https://okfgem.com/og-demo-
|
|
19
|
+
<meta name="twitter:image" content="https://okfgem.com/og-demo-v5.png">
|
|
20
20
|
<meta name="twitter:image:alt" content="An interactive Open Knowledge Format knowledge graph.">
|
|
21
21
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='24' fill='%231a1a1a'/><polygon points='38,44 62,44 50,82' fill='%237a0a1e'/><polygon points='18,44 38,44 50,82' fill='%23a8112c'/><polygon points='62,44 82,44 50,82' fill='%23a8112c'/><polygon points='35,28 18,44 38,44' fill='%23dc1e3c'/><polygon points='65,28 82,44 62,44' fill='%23dc1e3c'/><polygon points='35,28 65,28 62,44 38,44' fill='%23f43f5e'/><polygon points='36,29 49,29 42,42' fill='%23fff' opacity='.38'/><polygon points='35,28 65,28 82,44 50,82 18,44' fill='none' stroke='%23ff6b7f' stroke-width='2' stroke-linejoin='round'/></svg>">
|
|
22
22
|
<script>/* Resolve theme before first paint so there is no flash. */
|
|
@@ -246,7 +246,31 @@
|
|
|
246
246
|
background:var(--line-2);border:1px solid var(--line);border-radius:7px;padding:3px 9px}
|
|
247
247
|
.badge .dot{width:8px;height:8px;border-radius:50%}
|
|
248
248
|
.status{font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;padding:2px 7px;border-radius:5px;color:var(--muted);background:var(--line-2)}
|
|
249
|
+
/* §5.4's three, plus `shipped` — which this project used before the spec named
|
|
250
|
+
a vocabulary, and which stays styled because §4.1 lets a producer use any
|
|
251
|
+
value and requires a consumer to tolerate it. `stable` gets no rule because
|
|
252
|
+
it is never rendered: an absent status defaults to it, so badging it would
|
|
253
|
+
put a meaningless chip on every concept of every bundle. */
|
|
254
|
+
.status.draft{color:var(--warn);background:color-mix(in srgb,var(--warn) 14%,transparent)}
|
|
255
|
+
.status.deprecated{color:var(--accent-ink);background:color-mix(in srgb,var(--accent) 12%,transparent)}
|
|
249
256
|
.status.shipped{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}
|
|
257
|
+
/* §5.3's trust tiers — the third visual channel, after type colour and status.
|
|
258
|
+
Deliberately quieter than status: how much to believe a concept is a question
|
|
259
|
+
a reader asks *of* the card, where a deprecation is the card shouting. */
|
|
260
|
+
.tier{font-size:10.5px;font-weight:600;letter-spacing:.02em;padding:2px 7px;border-radius:5px;
|
|
261
|
+
color:var(--muted);background:var(--line-2);border:1px solid var(--line)}
|
|
262
|
+
.tier.machine-confirmed{color:var(--accent-ink);background:color-mix(in srgb,var(--accent) 12%,transparent);border-color:transparent}
|
|
263
|
+
.tier.human-reviewed{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent);border-color:transparent}
|
|
264
|
+
/* Past the expiry its own author declared (§5.5), judged against the viewer's
|
|
265
|
+
clock. Warn-coloured wherever it appears — a card's meta row, the trust line. */
|
|
266
|
+
.stale{color:var(--warn)}
|
|
267
|
+
/* The inspector's trust line: the chips under a concept's description. */
|
|
268
|
+
.meta-trust{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:8px}
|
|
269
|
+
/* The two text members of the line, as opposed to the .tier/.status badges
|
|
270
|
+
that carry their own size. `.stale` has only a colour globally, so
|
|
271
|
+
without this it rendered at the panel's inherited size beside them. */
|
|
272
|
+
.meta-trust .gen,.meta-trust .stale{font-size:11.5px}
|
|
273
|
+
.meta-trust .gen{color:var(--faint)}
|
|
250
274
|
.tag{background:var(--line-2);border:1px solid var(--line);border-radius:6px;padding:2px 8px;font-size:11px;color:var(--muted)}
|
|
251
275
|
|
|
252
276
|
/* ── graph view ── */
|
|
@@ -1034,6 +1058,12 @@
|
|
|
1034
1058
|
<div class="fgroup"><h4>Types</h4><div class="fchips" id="cat-ftypes"></div></div>
|
|
1035
1059
|
<div class="fgroup"><h4>Dirs</h4><div class="fchips" id="cat-fdirs"></div></div>
|
|
1036
1060
|
<div class="fgroup"><h4>Tags</h4><div class="fchips" id="cat-ftags"></div></div>
|
|
1061
|
+
<!-- §5.3/§5.4. Counted off the catalog rather than a fixed vocabulary,
|
|
1062
|
+
and hidden entirely when the bundle says nothing about either — a
|
|
1063
|
+
v0.1 bundle's whole trust posture is not worth a row of chips
|
|
1064
|
+
reading "unverified 9". -->
|
|
1065
|
+
<div class="fgroup" id="cat-fstatus-group" hidden><h4>Status</h4><div class="fchips" id="cat-fstatus"></div></div>
|
|
1066
|
+
<div class="fgroup" id="cat-ftrust-group" hidden><h4>Trust</h4><div class="fchips" id="cat-ftrust"></div></div>
|
|
1037
1067
|
</div>
|
|
1038
1068
|
</aside>
|
|
1039
1069
|
</section>
|
|
@@ -1170,7 +1200,7 @@
|
|
|
1170
1200
|
view switching full-text search
|
|
1171
1201
|
catalog files
|
|
1172
1202
|
tags stats
|
|
1173
|
-
theme / fullscreen §
|
|
1203
|
+
theme / fullscreen §8 map payload · §9 log payload
|
|
1174
1204
|
command palette the search bridge
|
|
1175
1205
|
concept preview (touch) mobile drawer + sheet
|
|
1176
1206
|
keyboard deep links · first visit
|
|
@@ -1224,18 +1254,22 @@ const SEARCH_ENDPOINT=<%= search_endpoint_json %>;
|
|
|
1224
1254
|
cannot use — and the panel reads that null to explain itself. */
|
|
1225
1255
|
const MANAGE_ROOT=<%= manage_root_json %>, MANAGE_TOKEN=<%= manage_token_json %>;
|
|
1226
1256
|
/* null when served live (`okf server`) — the getters below fetch the endpoints
|
|
1227
|
-
above. `okf render` injects a payload {catalog,index,logs,bodies}, and every
|
|
1228
|
-
getter resolves from it instead (the /node/meta
|
|
1229
|
-
catalog), so one file needs no server and the endpoint consts above go inert. */
|
|
1257
|
+
above. `okf render` injects a payload {catalog,index,logs,bodies,sources}, and every
|
|
1258
|
+
getter resolves from it instead (the /node/meta trust line composed from the
|
|
1259
|
+
catalog row), so one file needs no server and the endpoint consts above go inert. */
|
|
1230
1260
|
const EMBED=<%= embed_json %>;
|
|
1231
1261
|
/* Full-text search state (MiniSearch, lazy — built on first search). One ranked
|
|
1232
1262
|
index behind the search box for the graph, catalog and files views. It indexes
|
|
1233
|
-
title/id/type/tags/description in every mode, plus each concept body
|
|
1234
|
-
the page already holds
|
|
1235
|
-
searches
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1263
|
+
title/id/type/tags/description in every mode, plus each concept body and its
|
|
1264
|
+
source text wherever the page already holds them: `okf render` bakes both in,
|
|
1265
|
+
so a static file searches them offline; the live server keeps bodies lazy and
|
|
1266
|
+
carries only a source *count* on a catalog row, so its index stays
|
|
1267
|
+
metadata-only until a backend body index arrives. The asymmetry is
|
|
1268
|
+
pre-existing and deliberate — spending body-sized bytes on every /catalog
|
|
1269
|
+
fetch to serve one view is the trade the payload already refuses for `body`
|
|
1270
|
+
(see .okf/capabilities/search.md). Same 7.2.0 build and config as the Ruby
|
|
1271
|
+
port, so a Ruby-built index and this one rank identically. */
|
|
1272
|
+
const FT_FIELDS=['title','id','type','tags','description'].concat(EMBED?['body','sources']:[]);
|
|
1239
1273
|
let _MiniSearch=null,ftIndex=null,ftBuilding=null;
|
|
1240
1274
|
const descOf={};
|
|
1241
1275
|
const MIN=<%= OKF::Render::Graph::MIN_SIZE %>, MAX=<%= OKF::Render::Graph::MAX_SIZE %>;
|
|
@@ -1522,7 +1556,7 @@ interceptMdLinks(sideBody,()=>shownId,select,r=>r.kind==='index'?showDir(r.dir):
|
|
|
1522
1556
|
interceptMdLinks(document.getElementById('fp-body'),()=>fileSel,id=>openFile(id),
|
|
1523
1557
|
r=>openReserved(r.kind,r.kind==='index'?ixPathOf(r.dir):r.path));
|
|
1524
1558
|
/* Clicking a folder node (tree mode) or an area box (cluster mode) opens that
|
|
1525
|
-
directory's §
|
|
1559
|
+
directory's §8 index entry in the inspector: the authored map when one
|
|
1526
1560
|
exists, the synthesized listing when not. */
|
|
1527
1561
|
function showDir(dir){getIndex().then(dirs=>{const d=dirs.find(x=>x.dir===dir);if(!d)return;
|
|
1528
1562
|
const opened=openPanel();sideBody.scrollTop=0;
|
|
@@ -1541,19 +1575,86 @@ function showDir(dir){getIndex().then(dirs=>{const d=dirs.find(x=>x.dir===dir);i
|
|
|
1541
1575
|
function showLog(path){LOGS=null;getLogs().then(logs=>{const l=logs.find(x=>x.path===path);if(!l)return;
|
|
1542
1576
|
const opened=openPanel();sideBody.scrollTop=0;
|
|
1543
1577
|
shownId=path.replace(/\.md$/,'');
|
|
1544
|
-
sideBody.innerHTML=`<span class="type">update log · §
|
|
1578
|
+
sideBody.innerHTML=`<span class="type">update log · §9</span><h2 class="title">${esc(path)}</h2><div class="body" id="dir-body"></div>`;
|
|
1545
1579
|
renderMarkdown(document.getElementById('dir-body'),l.content||'');
|
|
1546
1580
|
if(opened)requestAnimationFrame(()=>cy.resize());});}
|
|
1581
|
+
/* ── §5 trust: one composition, both modes ──
|
|
1582
|
+
The catalog row carries generated_at/generated_by, the raw `generated`
|
|
1583
|
+
boolean, the derived `trust` tier (hyphenated, the wire spelling), the
|
|
1584
|
+
declared `status` and `stale_after` — so every view can say how much to
|
|
1585
|
+
believe a concept. Three rules, shared by the cards and the trust line:
|
|
1586
|
+
|
|
1587
|
+
· status — only a NON-default one. §5.4 defaults an absent status to
|
|
1588
|
+
`stable`, so badging that would chip every concept everywhere.
|
|
1589
|
+
· trust — suppressed when unverified AND `generated` is undeclared: on a
|
|
1590
|
+
well-curated v0.1 bundle every concept has a lifted timestamp,
|
|
1591
|
+
so keying off generated_at would label every card "unverified".
|
|
1592
|
+
The raw boolean is the one predicate that gets this right.
|
|
1593
|
+
· expired — computed HERE, against the viewer's own today. A baked verdict
|
|
1594
|
+
is wrong from the next midnight and a static render lives for
|
|
1595
|
+
months; the page compares dates the way the CLI does. */
|
|
1596
|
+
const TODAY=()=>{const d=new Date();return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');};
|
|
1597
|
+
const isExpired=sa=>!!sa&&/^\d{4}-\d{2}-\d{2}$/.test(sa)&&TODAY()>=sa;
|
|
1598
|
+
/* One fold for every status keying — chips, facets, suppression — because the
|
|
1599
|
+
row carries the DECLARED value and the CLI folds case when it narrows: a
|
|
1600
|
+
`status: Stable` bundle otherwise grew duplicate facet chips filtering
|
|
1601
|
+
disjoint sets, and "Stable" cards wore a chip the default never earns. The
|
|
1602
|
+
raw spelling still renders as the chip's text; only keys fold. */
|
|
1603
|
+
const foldStatus=v=>{const s=String(v==null?'':v).trim().toLowerCase();return s||'stable';};
|
|
1604
|
+
/* The folded value is a filter key, not a class name: §4.1 lets a producer
|
|
1605
|
+
declare `In Review`, which folded straight into the attribute became
|
|
1606
|
+
`class="status in review"` — two classes a stylesheet may already own. One
|
|
1607
|
+
token for the class, the raw spelling still the chip's text. */
|
|
1608
|
+
const statusSlug=v=>foldStatus(v).replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'')||'stable';
|
|
1609
|
+
const statusChip=c=>foldStatus(c.status)!=='stable'?`<span class="status ${esc(statusSlug(c.status))}">${esc(c.status)}</span>`:'';
|
|
1610
|
+
/* An `unverified` tier is an answer, not an absence — but only once a concept
|
|
1611
|
+
declared `generated:`, since a v0.1 concept declaring neither would wear a
|
|
1612
|
+
chip about a family it never had. The catalog's trust facet gates on this
|
|
1613
|
+
same predicate: a tier shown on every card with no group to narrow on was
|
|
1614
|
+
the shape it took when the two were spelled separately.
|
|
1615
|
+
|
|
1616
|
+
The rule's home is Ruby — Concept.shows_trust?, which the server's
|
|
1617
|
+
/node/meta and okf-tui both ask. This is the client-side twin, unavoidable
|
|
1618
|
+
because it runs over baked rows with no Ruby to call; a change to one is a
|
|
1619
|
+
change to both, and render_test.rb pins them equal so the copy cannot drift
|
|
1620
|
+
silently. */
|
|
1621
|
+
const showsTrust=c=>!!(c.trust&&!(c.trust==='unverified'&&!c.generated));
|
|
1622
|
+
const trustChip=c=>showsTrust(c)?`<span class="tier ${esc(c.trust)}">${esc(c.trust)}</span>`:'';
|
|
1623
|
+
/* No `by` means v0.1 lifted this from a `timestamp`, which recorded no actor.
|
|
1624
|
+
Printing one would be the false provenance claim §5 exists to prevent. */
|
|
1625
|
+
const genChip=c=>c.generated_at?`<span class="mini">⌂ ${esc(c.generated_at)}${c.generated_by?' by '+esc(c.generated_by):''}</span>`:'';
|
|
1547
1626
|
/* Two per-concept reads shared by the inspector and the files preview: the raw
|
|
1548
|
-
markdown body (rendered client-side) and the description
|
|
1549
|
-
hit /node and /node/meta; under EMBED
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
const
|
|
1554
|
-
const
|
|
1627
|
+
markdown body (rendered client-side) and the meta object {description,trust}.
|
|
1628
|
+
Live they hit /node and /node/meta (JSON); under EMBED both resolve from the
|
|
1629
|
+
baked payload — the trust fields read off the same catalog row the cards use,
|
|
1630
|
+
so the two modes compose one line from one ruleset. Never memoized in server
|
|
1631
|
+
mode, so on-disk edits keep reflecting. */
|
|
1632
|
+
const EMBED_ROW=EMBED?Object.fromEntries((EMBED.catalog||[]).map(c=>[c.id,c])):null;
|
|
1633
|
+
const rowTrust=c=>{if(!c)return null;const t={};
|
|
1634
|
+
if(showsTrust(c))t.tier=c.trust;
|
|
1635
|
+
if(c.generated_by)t.generated_by=c.generated_by;
|
|
1636
|
+
if(c.generated_at)t.generated_at=c.generated_at;
|
|
1637
|
+
if(c.status)t.status=c.status;
|
|
1638
|
+
if(c.stale_after)t.stale_after=c.stale_after;
|
|
1639
|
+
return Object.keys(t).length?t:null;};
|
|
1640
|
+
const metaFromRow=c=>{const m={description:(c&&c.description)||''};const t=rowTrust(c);if(t)m.trust=t;return m;};
|
|
1641
|
+
/* The one place meta reaches the DOM. Every producer-supplied string lands via
|
|
1642
|
+
textContent — never innerHTML — so no render path opens outside the
|
|
1643
|
+
sanitizer the bodies already pass through. */
|
|
1644
|
+
function renderMeta(el,meta){el.textContent='';
|
|
1645
|
+
const desc=String((meta&&meta.description)||'').trim();
|
|
1646
|
+
if(desc)el.appendChild(document.createTextNode(desc));
|
|
1647
|
+
else{const e=document.createElement('span');e.className='empty';e.textContent='no description';el.appendChild(e);}
|
|
1648
|
+
const t=meta&&meta.trust;if(!t)return;
|
|
1649
|
+
const line=document.createElement('span');line.className='meta-trust';
|
|
1650
|
+
const chip=(cls,text)=>{const s=document.createElement('span');s.className=cls;s.textContent=text;line.appendChild(s);};
|
|
1651
|
+
if(t.tier)chip('tier '+t.tier,t.tier);
|
|
1652
|
+
if(t.generated_at)chip('gen',t.generated_at+(t.generated_by?' by '+t.generated_by:''));
|
|
1653
|
+
if(foldStatus(t.status)!=='stable')chip('status '+statusSlug(t.status),t.status);
|
|
1654
|
+
if(isExpired(t.stale_after))chip('stale','expired '+t.stale_after);
|
|
1655
|
+
if(line.childNodes.length)el.appendChild(line);}
|
|
1555
1656
|
function getNodeBody(id){return EMBED?Promise.resolve(EMBED.bodies[id]||''):fetch(NODE_ENDPOINT+'?id='+encodeURIComponent(id)).then(r=>r.ok?r.text():'');}
|
|
1556
|
-
function getNodeMeta(id){return EMBED?Promise.resolve(
|
|
1657
|
+
function getNodeMeta(id){return EMBED?Promise.resolve(metaFromRow(EMBED_ROW[id])):fetch(META_ENDPOINT+'?id='+encodeURIComponent(id)).then(r=>r.ok?r.json():{});}
|
|
1557
1658
|
function show(id){const n=byId[id];if(!n)return false;const ty=typeOf[id]||'Untyped';const c=color[ty]||'#64748b';
|
|
1558
1659
|
shownId=id;const opened=openPanel();sideBody.scrollTop=0;
|
|
1559
1660
|
sideBody.innerHTML=`<button type="button" class="type facet" data-focus-type="${esc(ty)}" title="Show only ${esc(ty)} in the graph"><span class="dot" style="background:${c}"></span>${esc(ty)}</button>
|
|
@@ -1571,7 +1672,7 @@ function show(id){const n=byId[id];if(!n)return false;const ty=typeOf[id]||'Unty
|
|
|
1571
1672
|
sideBody.querySelectorAll('[data-focus-tag]').forEach(b=>b.onclick=()=>{
|
|
1572
1673
|
const t=b.getAttribute('data-focus-tag');tagFocused(t)?clearGraphFilter():focusGraphTag(t);});
|
|
1573
1674
|
syncFacets();
|
|
1574
|
-
getNodeMeta(id).then(
|
|
1675
|
+
getNodeMeta(id).then(m=>{const d=document.getElementById('desc');if(d){renderMeta(d,m);d.classList.remove('loading');}}).catch(()=>{});
|
|
1575
1676
|
const bodyEl=sideBody.querySelector('#body');
|
|
1576
1677
|
getNodeBody(id).then(md=>{if(bodyEl.isConnected)renderMarkdown(bodyEl,md);}).catch(()=>{});
|
|
1577
1678
|
return opened;}
|
|
@@ -1928,7 +2029,7 @@ function setTree(on){if(on===treeMode)return;if(on&&clustered)setClustered(false
|
|
|
1928
2029
|
runLayout(layoutSel.value);}}
|
|
1929
2030
|
btnTree.onclick=()=>setTree(!treeMode);
|
|
1930
2031
|
/* ── the authored layer, drawn ──
|
|
1931
|
-
The §
|
|
2032
|
+
The §8 map used to be visible only inside file-tree mode, where a folder node
|
|
1932
2033
|
stood in for a directory's index.md. It is a layer of its own now: switch it
|
|
1933
2034
|
on under any layout and each `index.md` becomes a node, edged to the concepts
|
|
1934
2035
|
it maps and to the child maps beneath it. Cluster and tree mode still work —
|
|
@@ -2209,8 +2310,8 @@ document.querySelectorAll('.rail-item').forEach(b=>b.onclick=()=>{
|
|
|
2209
2310
|
function loadMiniSearch(){return _MiniSearch||(_MiniSearch=loadScript('https://cdn.jsdelivr.net/npm/minisearch@7.2.0/dist/umd/index.js').then(()=>window.MiniSearch));}
|
|
2210
2311
|
function buildFtIndex(){return ftIndex?Promise.resolve(ftIndex):(ftBuilding||(ftBuilding=Promise.all([loadMiniSearch(),getCatalog()]).then(([MiniSearch,list])=>{
|
|
2211
2312
|
const idx=new MiniSearch({idField:'id',fields:FT_FIELDS,storeFields:['id'],
|
|
2212
|
-
extractField:(doc,f)=>f==='tags'?(doc.tags||[]).join(' '):f==='body'?((EMBED&&EMBED.bodies[doc.id])||''):(doc[f]==null?'':String(doc[f])),
|
|
2213
|
-
searchOptions:{prefix:true,fuzzy:0.2,combineWith:'AND',boost:{title:5,id:4,tags:3,type:2,description:2,body:1}}});
|
|
2313
|
+
extractField:(doc,f)=>f==='tags'?(doc.tags||[]).join(' '):f==='body'?((EMBED&&EMBED.bodies[doc.id])||''):f==='sources'?((EMBED&&EMBED.sources[doc.id])||''):(doc[f]==null?'':String(doc[f])),
|
|
2314
|
+
searchOptions:{prefix:true,fuzzy:0.2,combineWith:'AND',boost:{title:5,id:4,tags:3,type:2,description:2,sources:1,body:1}}});
|
|
2214
2315
|
idx.addAll(list);ftIndex=idx;
|
|
2215
2316
|
if((view==='graph'||view==='catalog'||view==='files')&&(q[view]||'').trim())applySearch();
|
|
2216
2317
|
return idx;}).catch(()=>{ftBuilding=null;return null;})));}
|
|
@@ -2242,11 +2343,30 @@ let CATALOG=null;
|
|
|
2242
2343
|
otherwise) and fills descOf on the way, so even the pre-index substring
|
|
2243
2344
|
fallback can match a graph node by its leaf description. */
|
|
2244
2345
|
function getCatalog(){return CATALOG||(CATALOG=(EMBED?Promise.resolve(EMBED.catalog):fetch(CATALOG_ENDPOINT).then(r=>r.json()).then(d=>d.concepts)).then(list=>{list.forEach(c=>{descOf[c.id]=c.description||'';});return list;}));}
|
|
2245
|
-
const catActiveTypes=new Set(), catActiveDirs=new Set(), catActiveTags=new Set()
|
|
2346
|
+
const catActiveTypes=new Set(), catActiveDirs=new Set(), catActiveTags=new Set(),
|
|
2347
|
+
catActiveStatus=new Set(), catActiveTrust=new Set();
|
|
2348
|
+
/* The two §5 facets, counted off the catalog — it is the only payload that
|
|
2349
|
+
knows them, so they cannot be built at boot like types and tags. Status
|
|
2350
|
+
counts the EFFECTIVE value (absent reads stable, the same rule --status
|
|
2351
|
+
narrows by); trust counts the derived tier. A facet with nothing to say
|
|
2352
|
+
stays hidden: the status group needs at least one DECLARED status, and the
|
|
2353
|
+
trust group at least one concept whose tier the page is willing to claim —
|
|
2354
|
+
otherwise every v0.1 bundle would wear a row of chips saying only
|
|
2355
|
+
"unverified". `showsTrust` is that willingness, and all three of the gate,
|
|
2356
|
+
the counts and the narrowing read it, so the facet describes exactly the
|
|
2357
|
+
cards wearing a tier chip. Counting rows the page shows no chip for made it
|
|
2358
|
+
read "unverified 3" over two chipped cards, and narrow to three. */
|
|
2359
|
+
let catStatusCounts={}, catTrustCounts={}, catShowStatus=false, catShowTrust=false;
|
|
2360
|
+
function catFacets(list){catStatusCounts={};catTrustCounts={};catShowTrust=false;
|
|
2361
|
+
list.forEach(c=>{const s=foldStatus(c.status);catStatusCounts[s]=(catStatusCounts[s]||0)+1;
|
|
2362
|
+
if(showsTrust(c)){catTrustCounts[c.trust]=(catTrustCounts[c.trust]||0)+1;catShowTrust=true;}});
|
|
2363
|
+
catShowStatus=list.some(c=>c.status);}
|
|
2364
|
+
const facetByCount=counts=>Object.keys(counts).sort((a,b)=>counts[b]-counts[a]||a.localeCompare(b));
|
|
2365
|
+
const facetItems=counts=>facetByCount(counts).map(v=>({v:v,n:counts[v]}));
|
|
2246
2366
|
const catByCount=types.slice().sort((a,b)=>(TYPES[b]||[]).length-(TYPES[a]||[]).length);
|
|
2247
2367
|
const catChip=t=>`<span class="chip" data-t="${esc(t)}"><span class="dot" style="background:${color[t]}"></span>${esc(t)} <span class="c">${(TYPES[t]||[]).length}</span></span>`;
|
|
2248
2368
|
function syncCatChips(){document.querySelectorAll('#cat-types .chip').forEach(c=>c.classList.toggle('on',catActiveTypes.has(c.getAttribute('data-t'))));
|
|
2249
|
-
const n=catActiveTypes.size+catActiveDirs.size+catActiveTags.size;
|
|
2369
|
+
const n=catActiveTypes.size+catActiveDirs.size+catActiveTags.size+catActiveStatus.size+catActiveTrust.size;
|
|
2250
2370
|
const b=document.getElementById('cat-filters-btn');b.classList.toggle('on-filter',n>0);b.querySelector('.fbadge').textContent=n;}
|
|
2251
2371
|
function catToggle(t){toggleSet(catActiveTypes,t);renderCatFilters();renderCatalog();}
|
|
2252
2372
|
function wireCatChips(el){el.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>catToggle(ch.getAttribute('data-t')));}
|
|
@@ -2258,6 +2378,11 @@ function renderCatFilters(){const qq=(document.getElementById('cat-filter-search
|
|
|
2258
2378
|
chipRow(document.getElementById('cat-ftypes'),typeItems(catByCount.filter(match)),'data-t',t=>catActiveTypes.has(t),t=>{toggleSet(catActiveTypes,t);after();});
|
|
2259
2379
|
chipRow(document.getElementById('cat-fdirs'),dirItems(graphDirs.filter(dirMatch(qq))),'data-dir',d=>catActiveDirs.has(d),d=>{toggleSet(catActiveDirs,d);after();});
|
|
2260
2380
|
chipRow(document.getElementById('cat-ftags'),tagItems(qq?tagsByCount.filter(match):tagsByCount.slice(0,40)),'data-tag',t=>catActiveTags.has(t),t=>{toggleSet(catActiveTags,t);after();});
|
|
2381
|
+
const sItems=facetItems(catStatusCounts).filter(o=>match(o.v)), tItems=facetItems(catTrustCounts).filter(o=>match(o.v));
|
|
2382
|
+
document.getElementById('cat-fstatus-group').hidden=!(catShowStatus&&sItems.length);
|
|
2383
|
+
document.getElementById('cat-ftrust-group').hidden=!(catShowTrust&&tItems.length);
|
|
2384
|
+
chipRow(document.getElementById('cat-fstatus'),sItems,'data-status',s=>catActiveStatus.has(s),s=>{toggleSet(catActiveStatus,s);after();});
|
|
2385
|
+
chipRow(document.getElementById('cat-ftrust'),tItems,'data-trust',t=>catActiveTrust.has(t),t=>{toggleSet(catActiveTrust,t);after();});
|
|
2261
2386
|
syncCatChips();}
|
|
2262
2387
|
function initCatalog(){inited.catalog=true;
|
|
2263
2388
|
// Only the five most common types get inline chips; everything else — types,
|
|
@@ -2268,13 +2393,18 @@ function initCatalog(){inited.catalog=true;
|
|
|
2268
2393
|
const cf=document.getElementById('cat-filters');
|
|
2269
2394
|
document.getElementById('cat-filters-btn').onclick=()=>cf.classList.toggle('open');
|
|
2270
2395
|
document.getElementById('cat-filters-close').onclick=()=>cf.classList.remove('open');
|
|
2271
|
-
document.getElementById('cat-filters-reset').onclick=()=>{catActiveTypes.clear();catActiveDirs.clear();catActiveTags.clear();
|
|
2396
|
+
document.getElementById('cat-filters-reset').onclick=()=>{catActiveTypes.clear();catActiveDirs.clear();catActiveTags.clear();
|
|
2397
|
+
catActiveStatus.clear();catActiveTrust.clear();renderCatFilters();renderCatalog();};
|
|
2272
2398
|
document.getElementById('cat-filter-search').oninput=renderCatFilters;
|
|
2273
|
-
|
|
2399
|
+
// The §5 facets only exist once the catalog is in, so the panel renders
|
|
2400
|
+
// twice: the chips boot knows immediately, then the two that had to wait.
|
|
2401
|
+
getCatalog().then(list=>{catFacets(list);renderCatFilters();renderCatalog();});}
|
|
2274
2402
|
function renderCatalog(){if(!CATALOG)return;getCatalog().then(list=>{const s=(q.catalog||'').toLowerCase();const ids=ftMatch(q.catalog);
|
|
2275
2403
|
const rows=list.filter(c=>{if(catActiveTypes.size&&!catActiveTypes.has(c.type))return false;
|
|
2276
2404
|
if(catActiveDirs.size&&![...catActiveDirs].some(d=>underDir(dirOf(c.id),d)))return false;
|
|
2277
2405
|
if(catActiveTags.size&&!c.tags.some(t=>catActiveTags.has(t)))return false;
|
|
2406
|
+
if(catActiveStatus.size&&!catActiveStatus.has(foldStatus(c.status)))return false;
|
|
2407
|
+
if(catActiveTrust.size&&!(showsTrust(c)&&catActiveTrust.has(c.trust)))return false;
|
|
2278
2408
|
if(ids)return ids.has(c.id);
|
|
2279
2409
|
if(s){const hay=(c.title+' '+c.description+' '+c.type+' '+c.tags.join(' ')+' '+c.id).toLowerCase();if(!hay.includes(s))return false;}return true;});
|
|
2280
2410
|
document.getElementById('cat-cnt').textContent=rows.length+' of '+list.length+' concepts';
|
|
@@ -2284,14 +2414,16 @@ function renderCatalog(){if(!CATALOG)return;getCatalog().then(list=>{const s=(q.
|
|
|
2284
2414
|
const g=document.getElementById('cat-grid');
|
|
2285
2415
|
if(!rows.length){g.innerHTML='<div class="none">No concepts match — try clearing the filters.</div>';return;}
|
|
2286
2416
|
g.innerHTML=rows.map(c=>{const cc=color[c.type]||'#64748b';
|
|
2287
|
-
const st=
|
|
2417
|
+
const st=statusChip(c);
|
|
2418
|
+
const tr=trustChip(c);
|
|
2288
2419
|
const tags=c.tags.slice(0,4).map(t=>`<span class="tag">${esc(t)}</span>`).join('');
|
|
2289
|
-
const ts=
|
|
2420
|
+
const ts=genChip(c);
|
|
2421
|
+
const sa=isExpired(c.stale_after)?`<span class="mini stale">⚠ expired ${esc(c.stale_after)}</span>`:'';
|
|
2290
2422
|
const lk=(c.links_out+c.links_in)?`<span class="mini links">↳ ${c.links_out+c.links_in} links</span>`:'';
|
|
2291
2423
|
return `<article class="card" tabindex="0" data-id="${esc(c.id)}">
|
|
2292
|
-
<div class="r1"><span class="badge"><span class="dot" style="background:${cc}"></span>${esc(c.type)}</span>${st}<span class="area">${esc(c.dir)}/</span></div>
|
|
2424
|
+
<div class="r1"><span class="badge"><span class="dot" style="background:${cc}"></span>${esc(c.type)}</span>${st}${tr}<span class="area">${esc(c.dir)}/</span></div>
|
|
2293
2425
|
<h3>${esc(c.title)}</h3><p class="cd">${esc(c.description)||'<span class="empty">No description.</span>'}</p>
|
|
2294
|
-
<div class="cm">${tags}${ts}${lk}</div></article>`;}).join('');
|
|
2426
|
+
<div class="cm">${tags}${ts}${sa}${lk}</div></article>`;}).join('');
|
|
2295
2427
|
g.querySelectorAll('.card').forEach(el=>{const id=el.dataset.id;el.onclick=()=>goToGraph(id);
|
|
2296
2428
|
el.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();goToGraph(id);}};});});}
|
|
2297
2429
|
|
|
@@ -2502,11 +2634,11 @@ function openReserved(kind,path){fileSel=path;
|
|
|
2502
2634
|
const body=document.getElementById('fp-body');body.innerHTML='<p class="loading">loading…</p>';
|
|
2503
2635
|
const done=txt=>{const wrap=document.createElement('div');wrap.className='body';renderMarkdown(wrap,txt);body.innerHTML='';body.appendChild(wrap);};
|
|
2504
2636
|
if(kind==='index')getIndex().then(dirs=>{const d=dirs.find(x=>x.dir===dirOfPath(path));
|
|
2505
|
-
type.textContent=d&&d.synthesized?'directory index · synthesized':'directory index · §
|
|
2637
|
+
type.textContent=d&&d.synthesized?'directory index · synthesized':'directory index · §8';
|
|
2506
2638
|
if(d&&d.synthesized){body.innerHTML='<div class="body">'+listingHtml(d.listing)+'</div>';
|
|
2507
2639
|
body.querySelectorAll('[data-go]').forEach(x=>x.onclick=()=>openFile(x.getAttribute('data-go')));}
|
|
2508
2640
|
else done((d&&d.body)||'');});
|
|
2509
|
-
else{type.textContent='update log · §
|
|
2641
|
+
else{type.textContent='update log · §9';
|
|
2510
2642
|
LOGS=null;getLogs().then(logs=>{const l=logs.find(x=>x.path===path);done((l&&l.content)||'');});}}
|
|
2511
2643
|
|
|
2512
2644
|
/* ── tags ── */
|
|
@@ -2584,13 +2716,13 @@ const fsOn=()=>document.fullscreenElement===appEl;
|
|
|
2584
2716
|
btnFull.onclick=()=>{if(fsOn())document.exitFullscreen();else if(appEl.requestFullscreen)appEl.requestFullscreen();};
|
|
2585
2717
|
document.addEventListener('fullscreenchange',()=>btnFull.setAttribute('aria-pressed',String(fsOn())));
|
|
2586
2718
|
|
|
2587
|
-
/* ── the §
|
|
2719
|
+
/* ── the §8 map payload — feeds folder clicks, the Indexes tab, and previews ── */
|
|
2588
2720
|
let INDEX=null;
|
|
2589
2721
|
function getIndex(){return INDEX||(INDEX=EMBED?Promise.resolve(EMBED.index):fetch(INDEX_ENDPOINT).then(r=>r.json()).then(d=>d.directories));}
|
|
2590
2722
|
function listingHtml(items){if(!items||!items.length)return '<p class="empty" style="margin:10px 0 0">No concepts directly here.</p>';
|
|
2591
2723
|
return '<ul class="ix-listing">'+items.map(it=>`<li><a data-go="${esc(it.id)}">${esc(it.title||it.id)}</a>${it.description?` <span class="d">— ${esc(it.description)}</span>`:''}</li>`).join('')+'</ul>';}
|
|
2592
2724
|
|
|
2593
|
-
/* ── log payload (the §
|
|
2725
|
+
/* ── log payload (the §9 history) — read by the Files view's log entries ── */
|
|
2594
2726
|
let LOGS=null;
|
|
2595
2727
|
function getLogs(){return LOGS||(LOGS=EMBED?Promise.resolve(EMBED.logs):fetch(LOG_ENDPOINT).then(r=>r.json()).then(d=>d.logs));}
|
|
2596
2728
|
|
|
@@ -3215,8 +3347,8 @@ function bridgeReport(v,shown,total){if(bridge)bridge.report(v,shown,total);}
|
|
|
3215
3347
|
'<p class="pv-meta">'+out+' link'+(out===1?'':'s')+' out · '+inn+' in</p>';
|
|
3216
3348
|
bodyIn.innerHTML=relList('Links to',outL[id]||[])+relList('Linked from',inL[id]||[])+'<div class="body" id="pv-md"></div>';
|
|
3217
3349
|
bodyIn.querySelectorAll('[data-go]').forEach(a=>a.onclick=()=>select(a.getAttribute('data-go')));
|
|
3218
|
-
getNodeMeta(id).then(
|
|
3219
|
-
if(d&&shown===id){d
|
|
3350
|
+
getNodeMeta(id).then(m=>{const d=document.getElementById('pv-desc');
|
|
3351
|
+
if(d&&shown===id){renderMeta(d,m);d.classList.remove('loading');}}).catch(()=>{});
|
|
3220
3352
|
if(snap!=='peek')loadBody();
|
|
3221
3353
|
return true;}
|
|
3222
3354
|
/* lazy: the body is markdown nobody can see at peek, and on a phone peek is the
|
|
@@ -3245,7 +3377,7 @@ function bridgeReport(v,shown,total){if(bridge)bridge.report(v,shown,total);}
|
|
|
3245
3377
|
bodyIn.querySelectorAll('[data-go]').forEach(a=>a.onclick=()=>select(a.getAttribute('data-go')));
|
|
3246
3378
|
raise();}).catch(function(){});}
|
|
3247
3379
|
function fillLog(path){LOGS=null;getLogs().then(function(logs){const l=logs.find(x=>x.path===path);if(!l)return;
|
|
3248
|
-
renderMarkdown(fillHead('update log · §
|
|
3380
|
+
renderMarkdown(fillHead('update log · §9',path,'the §9 history'),l.content||'');
|
|
3249
3381
|
raise();}).catch(function(){});}
|
|
3250
3382
|
|
|
3251
3383
|
/* ---- the camera -------------------------------------------------------- */
|
data/lib/okf/render/graph.rb
CHANGED
|
@@ -55,14 +55,22 @@ module OKF
|
|
|
55
55
|
# answer. Every key here is data a client getter reads from EMBED instead of
|
|
56
56
|
# fetching — and each derives from the *same* folder method the matching
|
|
57
57
|
# OKF::Server::App endpoint uses, so the bake and the live server cannot
|
|
58
|
-
# drift (/node/meta is the exception: the
|
|
59
|
-
# from the catalog
|
|
58
|
+
# drift (/node/meta is the exception: the trust line is composed on the
|
|
59
|
+
# client from the catalog row, so no map is baked for it). `bodies` and
|
|
60
|
+
# `sources` have no bare endpoint at all — they exist so a static file
|
|
61
|
+
# searches offline what the server-mode index deliberately leaves out; the
|
|
62
|
+
# sources text is the same join the Ruby engines index, so `--engine index`
|
|
63
|
+
# and the baked page rank identically. Empty strings are baked too: an
|
|
64
|
+
# undefined getter throws client-side.
|
|
60
65
|
def self.payload(folder)
|
|
61
66
|
{
|
|
62
67
|
catalog: folder.catalog,
|
|
63
68
|
index: folder.directory_index,
|
|
64
69
|
logs: folder.log_entries,
|
|
65
|
-
bodies: folder.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s }
|
|
70
|
+
bodies: folder.concepts.each_with_object({}) { |concept, map| map[concept.id] = concept.body.to_s },
|
|
71
|
+
sources: folder.concepts.each_with_object({}) do |concept, map|
|
|
72
|
+
map[concept.id] = OKF::Bundle::Search.field_texts(concept)["sources"]
|
|
73
|
+
end
|
|
66
74
|
}
|
|
67
75
|
end
|
|
68
76
|
|