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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +195 -0
- data/README.md +25 -6
- 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 +4 -1
- 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 +14 -12
- 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 +7 -6
- 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 +16 -9
- 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 +154 -35
- data/lib/okf/skill/reference/cli.md +155 -42
- 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 +5 -1
|
@@ -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
|
|
data/lib/okf/server/app.rb
CHANGED
|
@@ -18,16 +18,23 @@ module OKF
|
|
|
18
18
|
#
|
|
19
19
|
# GET / the interactive graph page (text/html)
|
|
20
20
|
# GET /node?id=… the concept's raw markdown body (text/markdown)
|
|
21
|
-
# GET /node/meta?id=… its description
|
|
21
|
+
# GET /node/meta?id=… its description and null-stripped §5 trust fields —
|
|
22
|
+
# { description, trust: { tier, generated_by,
|
|
23
|
+
# generated_at, status, stale_after } } (JSON). The
|
|
24
|
+
# page composes the trust line client-side, once, for
|
|
25
|
+
# served and baked pages alike; expiry is the client's
|
|
26
|
+
# to compute, against the viewer's own today.
|
|
22
27
|
# GET /catalog rich per-concept metadata for the catalog/files/stats
|
|
23
28
|
# views: { concepts: [ {id, title, type, description,
|
|
24
|
-
# tags,
|
|
29
|
+
# tags, generated_at, generated_by, generated, trust,
|
|
30
|
+
# status, stale_after, sources, top_dir, dir,
|
|
31
|
+
# links_*} ] } (JSON)
|
|
25
32
|
# GET /tags the tag index { tag => [id, …] } (JSON)
|
|
26
33
|
# GET /types the type index { type => [id, …] } (JSON)
|
|
27
|
-
# GET /index the §
|
|
34
|
+
# GET /index the §8 progressive-disclosure map for the Index panel:
|
|
28
35
|
# { directories: [ …okf-index rows… ] } (JSON, from the
|
|
29
36
|
# boot snapshot — authored maps are structure)
|
|
30
|
-
# GET /log the §
|
|
37
|
+
# GET /log the §9 history for the Log panel: { logs: [ {path,
|
|
31
38
|
# dir, content} ] } (JSON; content read live from disk,
|
|
32
39
|
# like a body — the log is the file that changes most)
|
|
33
40
|
# GET /search?q=… ranked concepts in this bundle, for the ⌘K palette:
|
|
@@ -143,14 +150,14 @@ module OKF
|
|
|
143
150
|
{ concepts: @folder.catalog }
|
|
144
151
|
end
|
|
145
152
|
|
|
146
|
-
# The §
|
|
153
|
+
# The §8 map the Index panel renders — the same rows `okf index` prints,
|
|
147
154
|
# built by the pure OKF::Bundle#directory_index over the boot snapshot
|
|
148
155
|
# (authored index bodies are structure, read at load like the graph).
|
|
149
156
|
def directory_index
|
|
150
157
|
{ directories: @folder.directory_index }
|
|
151
158
|
end
|
|
152
159
|
|
|
153
|
-
# The §
|
|
160
|
+
# The §9 history the Log panel renders: every log.md with its content, root
|
|
154
161
|
# scope first, read live from disk. Built by OKF::Bundle::Folder#log_entries,
|
|
155
162
|
# shared with `okf render`'s bake so the served and baked logs cannot drift.
|
|
156
163
|
def logs
|
|
@@ -181,11 +188,19 @@ module OKF
|
|
|
181
188
|
respond("text/markdown; charset=utf-8", concept.body)
|
|
182
189
|
end
|
|
183
190
|
|
|
191
|
+
# JSON, not a fragment: a server-composed HTML line would be a second
|
|
192
|
+
# render path reaching innerHTML outside DOMPurify — the exact hole the
|
|
193
|
+
# self-contained-page rule names — and would need a byte-identical JS twin
|
|
194
|
+
# for the baked page anyway. The client owns the one composition, and
|
|
195
|
+
# every string here lands via textContent there.
|
|
184
196
|
def node_meta(id)
|
|
185
197
|
concept = concept_for(id)
|
|
186
198
|
return not_found if concept.nil?
|
|
187
199
|
|
|
188
|
-
|
|
200
|
+
payload = { "description" => concept.description.to_s }
|
|
201
|
+
trust = trust_fields(concept)
|
|
202
|
+
payload["trust"] = trust unless trust.empty?
|
|
203
|
+
respond_json(payload)
|
|
189
204
|
end
|
|
190
205
|
|
|
191
206
|
# Resolve an id to its concept, read live from disk. The id is only ever a key
|
|
@@ -200,11 +215,32 @@ module OKF
|
|
|
200
215
|
nil
|
|
201
216
|
end
|
|
202
217
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
# Null-stripped, and absent as a whole when a concept says nothing about
|
|
219
|
+
# its own provenance — a v0.1 concept that adopted no §5 family gets the
|
|
220
|
+
# panel it always had. The tier is skipped for unverified-and-undeclared
|
|
221
|
+
# through Concept#shows_trust?, the one predicate the card chip and the
|
|
222
|
+
# page's facets read too: an untouched v0.1 bundle must not read
|
|
223
|
+
# "unverified" on every panel. No expiry verdict is baked (the client
|
|
224
|
+
# compares dates with the viewer's today), and no actor is invented for a
|
|
225
|
+
# lifted timestamp.
|
|
226
|
+
def trust_fields(concept)
|
|
227
|
+
fields = {}
|
|
228
|
+
fields["tier"] = concept.trust if concept.shows_trust?
|
|
229
|
+
fields["generated_by"] = concept.generated_by
|
|
230
|
+
fields["generated_at"] = iso(concept.generated_at)
|
|
231
|
+
# The row's serialization, not the raw Psych value: `status: no` reads
|
|
232
|
+
# as false, blank?(false) is true, and stripping it here while the
|
|
233
|
+
# baked page reads the row's "false" split the served and baked
|
|
234
|
+
# inspectors over one concept.
|
|
235
|
+
fields["status"] = concept.declared_status&.to_s
|
|
236
|
+
fields["stale_after"] = iso(concept.stale_after)
|
|
237
|
+
fields.reject { |_, value| OKF.blank?(value) }
|
|
238
|
+
end
|
|
206
239
|
|
|
207
|
-
|
|
240
|
+
# The one temporal-serialization rule the catalog row keeps — shared, not
|
|
241
|
+
# mirrored, so /node/meta and the row cannot drift (see OKF.iso8601).
|
|
242
|
+
def iso(value)
|
|
243
|
+
OKF.iso8601(value)
|
|
208
244
|
end
|
|
209
245
|
|
|
210
246
|
def respond(content_type, body)
|
|
@@ -225,10 +261,6 @@ module OKF
|
|
|
225
261
|
def not_found
|
|
226
262
|
self.class.not_found
|
|
227
263
|
end
|
|
228
|
-
|
|
229
|
-
def html_escape(str)
|
|
230
|
-
Rack::Utils.escape_html(str.to_s)
|
|
231
|
-
end
|
|
232
264
|
end
|
|
233
265
|
end
|
|
234
266
|
end
|
data/lib/okf/server/hub.rb
CHANGED
|
@@ -599,7 +599,7 @@ module OKF
|
|
|
599
599
|
end
|
|
600
600
|
|
|
601
601
|
# ok / warn / error, with the word that carries the same message for a
|
|
602
|
-
# reader who cannot see the colour. validate and lint stay separate (§
|
|
602
|
+
# reader who cannot see the colour. validate and lint stay separate (§11):
|
|
603
603
|
# a curation finding is a warning and never a conformance error, so a thin
|
|
604
604
|
# bundle keeps its link and only a non-conformant one reads as broken.
|
|
605
605
|
# Memoised like #counts — every stray 404 renders a bundle list too, and
|
data/lib/okf/skill/SKILL.md
CHANGED
|
@@ -35,13 +35,13 @@ Two ideas govern everything:
|
|
|
35
35
|
declare a graph; it arises from how you link concepts. Good linking *is* good
|
|
36
36
|
knowledge modelling.
|
|
37
37
|
|
|
38
|
-
## The hard rules (§
|
|
38
|
+
## The hard rules (§11 conformance)
|
|
39
39
|
|
|
40
40
|
Three conditions, all hard — `validate` fails a bundle on any of them:
|
|
41
41
|
|
|
42
|
-
1. **§
|
|
43
|
-
2. **§
|
|
44
|
-
3. **§
|
|
42
|
+
1. **§11 cond. 1** every non-reserved `.md` file has a parseable YAML frontmatter block;
|
|
43
|
+
2. **§11 cond. 2** every such block has a **non-empty `type`**;
|
|
44
|
+
3. **§11 cond. 3** every reserved file present is well-formed — a nested `index.md` has
|
|
45
45
|
no frontmatter, the bundle-root `index.md` carries *only* `okf_version`, and
|
|
46
46
|
`log.md` date headings are ISO `YYYY-MM-DD`.
|
|
47
47
|
|
|
@@ -55,11 +55,11 @@ most common mistake:
|
|
|
55
55
|
|
|
56
56
|
| Lens | Question | Tool | Nature |
|
|
57
57
|
|-----------|-----------------------------------|-------------------------|---------------------------|
|
|
58
|
-
| **Legal** | Is it conformant OKF? (§
|
|
58
|
+
| **Legal** | Is it conformant OKF? (§11) | `validate` | Binary, tolerant |
|
|
59
59
|
| **Good** | Is it navigable, complete, fresh? | `lint` | Advisory, structural |
|
|
60
60
|
| **True** | Is it consistent and *current*? | *you*, over `lint --json` | Semantic — needs meaning |
|
|
61
61
|
|
|
62
|
-
`validate` is *forbidden* by §
|
|
62
|
+
`validate` is *forbidden* by §11 from failing a bundle for broken links or missing
|
|
63
63
|
optional fields — that is `lint`'s job. And neither tool can judge contradictions
|
|
64
64
|
or *semantic* staleness (a concept that parses fine but no longer matches
|
|
65
65
|
reality); only an agent reasoning over meaning can. That last lens is where you
|
|
@@ -81,7 +81,7 @@ read and act on, never a missing toolchain to send to doctor.
|
|
|
81
81
|
Don't memorize the surface — `okf --help` maps every verb, `okf <verb> --help` its
|
|
82
82
|
flags. The division of labour is the whole game:
|
|
83
83
|
|
|
84
|
-
- **Shell out — never eyeball —** anything a verb computes: conformance (§
|
|
84
|
+
- **Shell out — never eyeball —** anything a verb computes: conformance (§11), what
|
|
85
85
|
exists, what links where, where a term lives, what's stale, the map. Every read
|
|
86
86
|
verb takes `--json` and the list views filter by type/dir/tag, so ask the narrow
|
|
87
87
|
question instead of paging the bundle.
|
|
@@ -92,9 +92,11 @@ flags. The division of labour is the whole game:
|
|
|
92
92
|
(parses fine, no longer true), whether a loose file is terminal-by-design, whether
|
|
93
93
|
a singleton tag is a deliberate marker. Tool output is evidence, never a verdict.
|
|
94
94
|
|
|
95
|
-
The one trap worth carrying in your head: **
|
|
96
|
-
`okf lint`
|
|
97
|
-
|
|
95
|
+
The one trap worth carrying in your head: **the age cutoff is off by default** —
|
|
96
|
+
a plain `okf lint` reports concepts past their own declared `stale_after` (the
|
|
97
|
+
`expired` check reads the clock the CLI supplies), but never judges *age*; pass
|
|
98
|
+
`--stale-after <90d|12w|ISO-date>` when you want anything not touched since then
|
|
99
|
+
flagged too. <!-- check:stale -->
|
|
98
100
|
|
|
99
101
|
Read [cli.md](reference/cli.md) before *interpreting* a verb's output in depth:
|
|
100
102
|
what `validate` may and may not reject, lint's categories and check ids, the JSON
|
|
@@ -105,8 +107,8 @@ shapes, the tag-curation views, the server's trust boundary.
|
|
|
105
107
|
Picking up a bundle you don't already know — to consume or maintain — start with
|
|
106
108
|
`okf dirs <dir|@slug>`: one row per *directory*, so it stays small on a bundle of
|
|
107
109
|
any size and it names the branches every other view narrows to. Then open the one
|
|
108
|
-
you want with `okf index <dir|@slug> --dir <branch>` (the §
|
|
109
|
-
index body, rollups, and listing), and read `log.md` (the §
|
|
110
|
+
you want with `okf index <dir|@slug> --dir <branch>` (the §8 map: that directory's
|
|
111
|
+
index body, rollups, and listing), and read `log.md` (the §9 baseline of what
|
|
110
112
|
changed last) — all of it **before** greping or opening leaves. Reach for `index`
|
|
111
113
|
rather than grep for the one reason that outranks convenience: **grep cannot find
|
|
112
114
|
an index entry that is missing**, so enumeration drift is invisible to it — you
|
|
@@ -20,11 +20,16 @@ mis-homed hubs, an uncurated tag layer — that is [refine](refine.md).
|
|
|
20
20
|
`okf loose <root> --json`.
|
|
21
21
|
3. Interpret through the three lenses the okf skill teaches, and keep them
|
|
22
22
|
separate:
|
|
23
|
-
- conformance errors (§
|
|
23
|
+
- conformance errors (§11) are the only hard failures; fix them first,
|
|
24
24
|
always;
|
|
25
25
|
- lint findings are curation debt across reachability, backlog,
|
|
26
|
-
completeness, freshness, provenance, and hygiene.
|
|
27
|
-
rank them by how much each hurts a reader navigating
|
|
26
|
+
completeness, freshness, provenance, attestation, migration, and hygiene.
|
|
27
|
+
They are advisory; rank them by how much each hurts a reader navigating
|
|
28
|
+
the graph. Read the stats' `trust` and `status` distributions for the
|
|
29
|
+
bundle's posture (how much is unverified, what is draft or deprecated),
|
|
30
|
+
and treat the Migration findings as an offer, not debt — §13 keeps a v0.1
|
|
31
|
+
bundle consumable forever, and the [migrate playbook](migrate.md) carries
|
|
32
|
+
the rewrite when the user wants it;
|
|
28
33
|
- loose files can be legitimate terminal leaves, so judge each one before
|
|
29
34
|
linking it anywhere.
|
|
30
35
|
4. Propose, then apply: list the fixes worth making (must-fix errors first,
|