Generated from
smart-base/skills/content/authoring-who-smart-guidelines/dak-postprocessing.md— do not edit here.
dak-postprocessing
Skill id:
dak-postprocessing· Package:authoring-who-smart-guidelines· Layer:smart-base, DAK kind (wassmart-dakuntil stage D, #1767), except the two Library strippers, which arefhir-harness— see §”One step is not WHO’s”.
Everything the WHO build runs between publisher.jar finishing and deployment,
gated by do_dak and by dak.json (upstream’s spelling; ours is
dak.config.json). Each step runs inside the publisher’s
Docker container (docker exec -w /work ig-run python3 …), against output/.
Sourcing.
WorldHealthOrganization/smart-base—.github/workflows/ghbuild.ymlandinput/scripts/— read 2026-09-22 at that repository’smain. Scripts are fetched at build time from smart-basemain, so a downstream IG runs smart-base’s current scripts, not its own checkout’s.
The steps, and what each accomplishes
| # | script | what it produces |
|---|---|---|
| 1 | strip_library_binaries.py | removes base64 content.data from Library resources |
| 2 | strip_library_content.py | replaces inline CQL/ELM with a URL reference |
| 3 | generate_logical_model_schemas.py | a JSON Schema per logical model |
| 4 | generate_valueset_schemas.py | a JSON Schema per ValueSet, plus the enumeration-response schemas |
| 5 | generate_jsonld_vocabularies.py | JSON-LD vocabularies from the ValueSet expansions |
| 6 | generate_dak_api_hub.py | dak-api.html — the hub page, and the .openapi.json / .displays.json sidecars |
| 7 | update_translated_image_refs.py | rewrites image references inside translated HTML pages |
| 8 | generate_smart_liquid.py + inject_smart_liquid.py | IG metadata → Liquid variables, and the include injected into every markdown page |
Steps 1–2 are two workflow steps with near-identical names; 3–4 share one step.
Steps 1–5 belong to fhir-harness, not to this overlay. They only
transform what the Publisher already emitted and add no constraint or profile.
The two strippers were placed there first, and 3–5 followed on the owner’s
ruling of 2026-10-03. They stay in this table because upstream still runs them
in this phase. check:fhir-harness-exclusions reads the table and lets the
base name exactly these five. Steps 6–8 are the WHO surface.
Step 8 is the seam, and it already does what we want
generate_smart_liquid.py declares its own source of truth as “IG Publisher
processed output directory (output/)”. It scans that output for
{ResourceType}-{id}.json, and for each one emits Liquid assignments named
smart__<ResourceType>__<id_normalized>__<category>__<key>
with categories url__canonical, url__page, url__json, text__display,
link__html, and elements__<key> for scalars (name, title, description,
status, version, …). Ids are normalised by replacing every non-alphanumeric
character with _.
This is exactly “use metadata from the IG Publisher to populate the variables
Jekyll processes” — already built, and aimed at the Publisher’s own Jekyll.
It is therefore the first thing to lift, not something to design: the transition
re-points an existing bridge rather than building one. Bean jut3, phase P0 of
kn0t.
A defect worth knowing before you rely on it
The script writes input/pagecontent/smart.liquid.md and says so in its own
output: “will be processed by IG Publisher on the NEXT build”. The
documentation page for the variable surface is therefore one build behind —
and it works around this by writing output/smart.liquid.html directly, bypassing
Jekyll, so the page exists at all.
A metadata surface that needs two builds to converge cannot be the basis of an incremental staging loop. Under the just-the-docs pipeline the variables are computed and consumed in one pass, and this workaround goes away with the problem.
One step is not WHO’s
Steps 1 and 2 strip base64 binaries and inline CQL/ELM out of Library
resources. Nothing in either is DAK-shaped: any FHIR IG that embeds CQL
produces oversized Library resources, and the deploy phase’s *“Delete files
100MB before deployment”* step is the same concern one layer down.
By the smart-stack-layering question — would a
non-WHO FHIR IG need this? — the answer is yes, and it can be named: any IG
using hl7.fhir.uv.cql. They belong in fhir-harness.
This is the falsification test doing its job: two of thirteen steps refused to sit in the layer their step name claims, and the split survived because there was a lower layer to put them in.
Steps 3–6 are the DAK API, and they are the reason ingestion works
The four schema/vocabulary/hub steps are what produce the surface
ig-artifact-ingestion reconstructs an index from:
.schema.json, .displays.json, .openapi.json under schemas/, and
.jsonld at the published root.
Two things already measured about that surface, which constrain any redesign:
- No IG publishes an artefact-index instance.
ValueSets.schema.jsonat the published root is a schema describing an enumeration response, carrying anexamplethat happens to hold the list. The index is reconstructed from four partial views, none sufficient alone (beanqsf5). - Coverage differs per IG, so no view is the winner.
canonicals.jsoncovers 10 % of the WHO SMART Trust IG and 100 % of the WHO SMART Immunizations IG (beanqrnz). Provenance records all views rather than naming one.
Re-derive those numbers rather than quoting them.
On an IG repository’s own site: ig-site:sidecars
Owner, 2026-10-14, on the WHO SMART Trust IG losing its DAK API sidecars when it dropped
its own site.build: “that should already be there in smart-base (dak
api)”. An IG repository whose index.config.json names smart-base as its
site’s landing declares no steps of its own. The site route (cat-harness-tools
stage-local.ts) runs the landing layer’s ig-site:sidecars, which is
ig-site-sidecars.ts: steps 4 to 6’s
generators run over the cached AST by dak-ast-sidecars.ts, written to
$SITE_WORK/ig-api/sidecars/, with $SITE_WORK/ig-api/ig-api.json saying
{"label": "DAK API"} when the IG has a dak.json. fhir-harness binds and
serves them (ig-repo-site.ts, fhir-harness ig-build-pipeline
§”The IG repository’s site route”) and never learns the DAK name. Step 6 runs
too, over the AST and the sidecars with a stand-in hub page: its OpenAPI
documents (<stem>.openapi.json), enumeration schemas and Swagger UI page
openapi/index.html land in $SITE_WORK/ig-api/site/, which fhir-harness
serves at the site root without replacing a path the site already holds
(smart-hiv’s live root served that openapi/, measured 2026-10-14).
The hub page itself is a THIN page (owner, 2026-10-14: “Yes, thin page”):
dak-api-hub-page.ts writes
dak-api.html, a shell made with cat-harness-tools’ thinPageHtml, over a
node dak-api.jsonld that its loader (assets/dak-api-hub.js) draws. The
node links openapi/, every sidecar with its artefact page and (for a JSON
Schema or JSON-LD sidecar) its view page artifact/<file>.html, bound by the
rule fhir-harness binds sidecars by, and the enumeration schemas. It goes
into the same $SITE_WORK/ig-api/site/. An IG whose own dak-api.md holds
only the hub’s marker (<!-- DAK_API_CONTENT -->, as the WHO SMART Trust IG’s does) gets the
thin hub in that page’s place: ig-api.json names the marker as
placeholder, and fhir-harness replaces a page holding it, and no other.
Step 5 depends on expansion, which depends on a terminology server
generate_jsonld_vocabularies.py reads ValueSet expansions, which exist only
if the Publisher expanded them — which depends on tx, the optional terminology
server input. An IG built with a dead or restricted tx produces fewer
vocabularies and fails nothing: the step reports a warning and continues.
So a thin vocabulary output is not evidence of a thin ValueSet. Check the expansion before concluding anything about the content.
Step 7 renders nothing and is not translation
update_translated_image_refs.py rewrites <img> references inside already
generated translated HTML. It exists because translation happens to pages after
they are built. Under a pipeline where the locale layer is part of rendering,
there is no already-built HTML to patch, and this step has no subject.
What the QA reports say, and where they go
Every script here instantiates the same QAReporter as the pre-processing
scripts and emits a phase report: successes, warnings, errors,
files_processed, files_expected, files_missing, and a summary with counts
and a completion timestamp. qa.json from the Publisher run is uploaded as a
workflow artifact.
Both are produced and neither was read downstream. That is the evidence the
qa-report graph typology was registered from on 2026-09-22 —
schemas/qa-report.ts. The shape is what these scripts already emit, not a
design.
Three rules are enforced structurally rather than left to a checker, each one a
discipline this repository has already paid for in prose: a summary may not
disagree with the details it counts; files_missing must be a subset of
files_expected, which is what keeps a determined empty distinguishable from a
not-found; and running is never a pass — a script that died before writing
anything has zero errors, and qaReportVerdict returns unknown for it rather
than ok.
Binary artefacts on deploy and on release
Measured 2026-10-02, owner: “see what happens on smart-base with binary
artefacts on release”. The generic rule and the measuring Tool are
ig-binary-artefacts
and ig-binary-audit. What follows is what the WHO workflows actually do, read
from litlfred/smart-base@e151a4d and WorldHealthOrganization/smart-html@main.
On every push, ghbuild.yml deploys output/ with its binaries. The only
binary step is “Delete files >100MB before deployment”, and no Publisher
binary is that large. A branch build deploys to branches/<branch>/, so each
branch gets another full copy: 966.5 MB of binaries on gh-pages, 907.1 MB of
them in 29 previews, full-ig.zip 644.7 MB in 30 copies.
On a GitHub release, release.yml hands off to smart-html. It runs only
if publication-request.json exists, then calls
WorldHealthOrganization/smart-html/.github/workflows/release.yml@main.
That workflow runs scripts/ig_publisher.py from smart-html’s main, which:
- builds and publishes into the smart-html webroot;
- leaves
ig-build-zips/out of thesitepreviewcopy and untracks it; - moves only files over 100 MB into
release-assets/, plus a copy ofoutput/package.tgz; - uploads
release-assets/to the release whose tag isGITHUB_REF_NAME.
So a release gets package.tgz and, in practice, nothing else. Every other
binary goes to the webroot.
create_package_release.py is not called by any workflow. It is the
script that would cut a release and move the 19 named binaries out of
output/, so they don’t bloat gh-pages. Nothing in .github/ calls it.
The two releases that exist on litlfred/smart-base, v0.2.0 and
v0.2.0.0, are from a March branch (claude/fhir-package-binary-releases-4ngGj)
and carry 5 assets: package.db, package.tgz, package.r4.tgz,
package.r4b.tgz and ai.zip.
downloads.md promises 19 release assets that no release carries. It
also leaves out full-ig.zip, which is the largest of them.
fix_release_links.py rewrites its hard-coded
WorldHealthOrganization/smart-base release links to the building repository,
which is correct. The page’s list is the part that drifted.
None of this is changed here: the workflows live in the WHO repositories. Bean
b8ip holds the proposed changes for the owner.
Where the counterparts are
dak-preprocessing — the six invocations before the
Publisher, and why most of them exist only to populate sushi-config.yaml’s
pages: and menu:. ig-publication — the Publisher run
and the authority to release. ig-artifact-ingestion
— reading the surface steps 3–6 produce, from a published IG.
Processes that run this skill
| process | step(s) that name it |
|---|---|
| SMART Guideline end to end | 4 · Create / adapt L2 DAK assets |