Generated from smart-base/skills/content/authoring-who-smart-guidelines/dak-postprocessing.md — do not edit here.

✎ Edit this page’s source

dak-postprocessing

Skill id: dak-postprocessing · Package: authoring-who-smart-guidelines · Layer: smart-base, DAK kind (was smart-dak until stage D, #1767), except the two Library strippers, which are fhir-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.yml and input/scripts/ — read 2026-09-22 at that repository’s main. Scripts are fetched at build time from smart-base main, so a downstream IG runs smart-base’s current scripts, not its own checkout’s.

The steps, and what each accomplishes

#scriptwhat it produces
1strip_library_binaries.pyremoves base64 content.data from Library resources
2strip_library_content.pyreplaces inline CQL/ELM with a URL reference
3generate_logical_model_schemas.pya JSON Schema per logical model
4generate_valueset_schemas.pya JSON Schema per ValueSet, plus the enumeration-response schemas
5generate_jsonld_vocabularies.pyJSON-LD vocabularies from the ValueSet expansions
6generate_dak_api_hub.pydak-api.html — the hub page, and the .openapi.json / .displays.json sidecars
7update_translated_image_refs.pyrewrites image references inside translated HTML pages
8generate_smart_liquid.py + inject_smart_liquid.pyIG 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.json at the published root is a schema describing an enumeration response, carrying an example that happens to hold the list. The index is reconstructed from four partial views, none sufficient alone (bean qsf5).
  • Coverage differs per IG, so no view is the winner. canonicals.json covers 10 % of the WHO SMART Trust IG and 100 % of the WHO SMART Immunizations IG (bean qrnz). 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:

  1. builds and publishes into the smart-html webroot;
  2. leaves ig-build-zips/ out of the sitepreview copy and untracks it;
  3. moves only files over 100 MB into release-assets/, plus a copy of output/package.tgz;
  4. uploads release-assets/ to the release whose tag is GITHUB_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

processstep(s) that name it
SMART Guideline end to end4 · Create / adapt L2 DAK assets

folio-assistant — code under the Apache License 2.0, documentation under CC BY 3.0. Generated schema and API references are rebuilt from source on every push to main.

deployed b08c961 · 2026-10-23T23:11:09Z