Generated from
smart-base/skills/content/authoring-who-smart-guidelines/ig-artifact-ingestion.md— do not edit here.
ig-artifact-ingestion
Skill id:
ig-artifact-ingestion· Package:authoring-who-smart-guidelines· Named by thefhir-artifact-indexgraph typology incat-harness/schemas/cat-harness.ts, which is the declaration that sends a consumer here.
Point at a published FHIR Implementation Guide, read what it actually published, and reconstruct its artefact index as a knowledge graph keyed by the IG’s own canonical URLs — linking each artefact to its JSON, JSON Schema and JSON-LD representations.
Sourcing. The artefact set, the canonical URL scheme and
artifacts.htmlare the HL7 IG Publisher’s; the DAK API layer is WHO SMART Guidelines’. This skill states how an index is reconstructed in this harness and defers to those for what an IG must contain. It is deliberately silent on whether an IG’s content is correct — that isfhir-validation’s lane, and an index that started grading its subject would be doing two jobs badly.
Which IGs this applies to
Not all of them. The test is whether the IG publishes a DAK API, and it is answered by one fact, not by the IG’s name or its publisher:
Are there enumeration schemas at the published root — files matching
<Type>.schema.json, such asValueSets.schema.jsonorLogicalModels.schema.json?
If yes, dakApi is present and the overlay is ingested. If the root was read
and held none, it is absent. If the root was never read, it is unknown —
and unknown is not a kind of absent. “This IG publishes no DAK API” and
“nobody looked” are different facts, and only the first is a reason to stop.
An IG with no DAK API still indexes: the spine is the IG Publisher’s own output, which every IG has. What it loses is the JSON Schema and JSON-LD links, which is the half worth having — so a no-DAK IG is a thin index, not an error.
The index is RECONSTRUCTED, never downloaded
The finding that governs everything here, measured 2026-09-21 against the WHO SMART Trust IG v1.8.0:
No IG publishes an artefact-index instance document.
ValueSets.schema.json and LogicalModels.schema.json sit at the published
root — not under schemas/, contrary to how dak-api.html links them —
and both are JSON Schemas describing the shape of an enumeration response.
Each carries an example block that happens to hold the real list. There is no
ValueSets.json to fetch.
So every field in the resulting graph was assembled, and the index records
in provenance which published file each part came out of. That is required,
not optional: a reader who cannot tell which file a row came from cannot tell a
transcription from an inference.
THREE DOCUMENT FAMILIES IN ONE DIRECTORY — because there are three SOURCES
fhir-artifact-index/ holds three kinds of document, and the reason is not
convenience. Each comes from a different source, so each carries its own
provenance block. An IG does not publish one thing about itself; it publishes
one thing, keeps a second in its source config, and inherits a third from
packages it merely depends on.
| document | schema | source | what its provenance names |
|---|---|---|---|
index.json | folio-fhir-artifact-index/v2 | the IG’s published output (a gh-pages tree) | {kind: "gh-pages", of, readAt} |
menu.json | folio-ig-menu/v1 | the IG’s own sushi-config.yaml | repository + commit + path |
ig-identity.json | folio-ig-identity/v1 | the IG’s own sushi-config.yaml: id, canonical, version, status | readFrom + readAt |
chrome.json | folio-ig-chrome/v2 | the fhir.template chain its ig.ini names — keyed by the chain’s top template, so it lives once, in smart-base’s themes/ beside the template’s theme (plan Q4), and every IG wears it | one block per layer, each with its own commit |
Folding any two together would give one file two answers to “where did this
come from”. That is the whole argument, and it is 0818’s (the menu) and
ajx9’s (the chrome) in one sentence.
The menu is the IG’s own ordering of itself
sushi-config.yaml’s menu: is a nested map, and order is the file’s
order — alphabetising it would silently re-navigate somebody else’s IG. It was
NOT transcribed from a screenshot, and schemas/ig-menu.ts records why: a
picture carries no hrefs, and a closed dropdown is indistinguishable from an
empty one. Measured 2026-09-23: the twelve labels visible in that screenshot
matched 0 of 674 artefact titles — the harvest kept the IG’s artefacts and
dropped both its navigation and the narrative pages it points at.
The chrome comes from a CHAIN, and only part of it is the IG’s own
An IG declares its appearance in no file it owns. Measured 2026-09-23 for the WHO SMART Trust IG:
local-template → who.template.root 0.5.0 → fhir.base.template 1.0.0
So chrome.json carries layers base-first, and every token records which
package won it and what it overrode. That is not bookkeeping: it is the
only way to answer whose value is this? Of the three things a reader
recognises as “the WHO IG”, only two are WHO’s — who.css styles no
#publish-box at all, so the yellow box is HL7’s, inherited unchanged. The
token comes out from: "fhir.base.template" with overrides: [], which is
what proves it rather than asserting it.
An overlay is not a merge. --toc-box-border is 1px solid navy in HL7
and navy in WHO — each right in its own layer, and together they make HL7’s
rule resolve to border: navy, which is invalid. Every individual layer
parses, so a naive merge ships a broken rule silently.
Upstream defects are RECORDED, never corrected
A mirror that silently fixes its subject is not a mirror. Two kinds, both found 2026-09-23 and both carried verbatim into our copy:
shape— one token name carrying two different KINDS of value across layers, as above.malformed— a value that is not valid CSS on its own terms.--breadcrumb-text-color: ##555555atproject.css:82, a doubled#in HL7’s source.
The second is in the skill because of how it presents downstream: it appears in
our generated page as ##555555, looking exactly like a bug in the ingest
that copied it. It cost the author of that ingest about thirty seconds of
believing they had written it, with the source open in the next terminal. A
reader without the source open cannot tell at all. So the mirror carries the
value and the record says whose defect it is.
The detector is deliberately narrow — one pattern. A false positive accuses somebody else’s published stylesheet of a fault it does not have.
CI cannot obtain any of these inputs, and the checks say so
All three ingests read something outside this checkout, and the chrome needs
three upstream checkouts rather than one. worldhealthorganization.github.io
and litlfred.github.io both answer 403 CONNECT from this environment.
So ingest:ig-menu:check and ingest:ig-chrome:check exit 2 with no
source — could not determine, never the 0 a silent skip would give — and both
are registered in gates.ts as report rather than as gates. What IS gated on
every run, without the network: smart-trust:pages:check regenerates all 681
pages from the committed data and compares byte for byte, and
check:kind-validators parses each file against its schema.
Four partial views, and none of them is sufficient
Measured on the WHO SMART Trust IG v1.8.0. The coverage gaps are the reason the pipeline merges rather than picking a winner:
| file | entries | holds | misses |
|---|---|---|---|
canonicals.json | 70 | canonical URL, version, name | the 604 Endpoints and Organizations |
package.tgz → package/.index.json | 674 | every artefact’s type and id | titles, categories, canonicals |
artifacts.html | 676 links | the editorial category and human title | machine-readable anything |
package.manifest.json | 1 | package id, version, FHIR version, build date | — |
artifacts.html is the only source of an artefact’s category. Its per-row
title attribute carries ResourceType/id verbatim, which is the index’s own
key format — so the category join is on an identifier the publisher wrote, not
one the pipeline composed from a filename.
Read the source, not the page about the source. dak-api.html is prose
about what is published, and on the WHO SMART Trust IG it links the enumeration schemas
to a schemas/ directory they are not in. A pipeline written from the
documentation would have found nothing at the paths it named.
Two traps
openapi/openapi.json is not the DAK API. On the WHO SMART Trust IG that path holds
the DDCC Gateway API — a domain API about certificate exchange that merely
lives there. A pipeline that globs for openapi files a piece of subject
matter as an artefact descriptor. The overlay is therefore keyed off each
artefact’s own stem (schemas/<ResourceType>-<id>.openapi.json), never off a
directory scan.
.index.json is lossy. Its Organization entries on the WHO SMART Trust IG carry a
truncated "type": "[". Do not repair such a value and do not propagate it:
canonicals.json is the spine for anything canonical, and for a resource that
appears only in .index.json the field is simply absent. An absent field is a
fact; a repaired one is a guess wearing a fact’s clothes.
What is never invented
canonical is optional on an artefact, and that is load-bearing. Examples
and instance resources have no canonical URL and are the majority of a large IG
— 604 of the WHO SMART Trust IG’s 674. Minting one to fill the slot produces an identifier
that resolves to nothing while looking authoritative, which is the failure the
who-iris catalogue refused for collection/hq-publications and refuses here
for the same reason.
The same holds for category: absent means the IG published no artefact page,
or listed no row for this artefact. It never means “Other” — the IG’s own
artefact page has a literal Other section, and coining a second one would
make the word mean two things in one index.
Materialisation
Default is catalogue by reference, following who-iris: the index records
where each artefact is published and holds no bytes. --materialize-dak adds
the DAK surface only — on the WHO SMART Trust IG, 332K across 71 files, against 7.1M for
the full resource corpus.
Materialised nodes take purpose: "working", never archival: they are
regenerable by re-running the ingest against the same source revision, so they
carry none of archival’s obligations. --check is what proves that claim,
which is what makes the claim admissible.
Each materialised node carries all five gates of
folio-assistant-core/schemas/materialization.ts. Two of them —
sourceLoss and copyright — are unknown for a WHO IG until somebody
establishes otherwise, and leaving them unknown is correct rather than lazy:
no statement has been made about how long a given version’s Pages build stays
reachable, and the IG’s licensing has not been read. A permitted there would
be the “no restrictions known in context” failure that GateVerdict is
three-valued to prevent.
gh-pages and output/ are not the same evidence
Both are accepted (--kind), and the index records which was read. They are
different claims: output/ is a local build whose contents depend on who
ran the publisher and when, while gh-pages is what the world can see. An
index built from the first and labelled the second asserts public availability
for artefacts that may never have been published.
Prefer gh-pages for anything that will be cited. Use output/ when checking
an IG you are mid-way through authoring — and expect the index to change when
it is actually published.
Running it
bun run cat ingest:ig -- \
--source /path/to/gh-pages --kind gh-pages \
--id <ig> \
--base https://worldhealthorganization.github.io/<ig> \
--out <ig> --materialize-dak
--base is the URL the artefacts are published at, and it is not the
canonical base. The WHO SMART Trust IG publishes at worldhealthorganization.github.io
and is canonical at smart.who.int — the index carries both, and composing
either from the other is how a link that resolves for nobody gets written down.
canonicalBase is read from the IG’s own ImplementationGuide canonical, not
derived from --base.
bun run cat ingest:ig:check re-runs the ingest and fails if the committed index
differs. That is what makes this graph regenerable rather than a snapshot
nobody can re-derive; readAt is excluded from the comparison because it moves
every run by design.
Adding another IG
- Clone the IG’s
gh-pages(shallow, single branch — a published IG is large; the WHO SMART Trust IG’s is 342,656 files). - Run the ingest with a new
--idand--out. - Give the new directory a declaration declaring one directory of kind
fhir-artifact-index, as an ingested IG’s own<name>.config.jsondoes. The declaration is<name>.config.jsonsince #695 — resolve it withdeclarationPathIn, never by joining a filename. - Add its
ingest:ig:checkinvocation to the gate set, so the index cannot go stale silently.
Step 4 is the one that gets skipped. An index nothing re-derives is a snapshot, and a stale snapshot of someone else’s corpus is worse than no index: it answers confidently and wrongly about artefacts that have since moved.
Processes that run this skill
| process | step(s) that name it |
|---|---|
| SMART Guideline end to end | 5 · Choose L3 dependencies (WHO / jurisdiction IGs) |