Generate the IG source¶
This is the step that reads your instance. One command pulls the data sets, the event and tracker programs, the tracked entity types, the option sets, the category combinations, and the organisation-unit hierarchy, and writes them out as the source of a publishable package - one form per data set, one per event program, one per tracker stage, a code list per option set, and two files per organisation unit. Nothing is written to DHIS2 and nothing is published yet; this writes files into your project directory.
Who this is for: the operator regenerating after a metadata change, and the implementer running generation for the first time.
Before you start: a scaffolded project pointing at your instance (Set up an IG project), ideally after a clean validate - generate refuses the codes and names validate marks as build-aborting.
You will be able to:
- run the whole pipeline, or one target of it, and read the summary
- say which directory each target owns and what a re-run does to it
- narrow the selection so a national instance stays reviewable
- find the notes a run raised and know which ones repeat validate
Run the whole pipeline¶
$ d2w fhir generate
running 8 step(s)
[1/8] instance metadata: 14 questionnaire target(s), 13 option set(s), 5 categories,
1,332 organisation unit(s)
[2/8] foundation: 23 files written, 0 files unchanged
[3/8] option sets: 13 option sets, 39 files written, 0 files unchanged
[4/8] categories: 5 categories, 15 files written, 0 files unchanged
[5/8] questionnaires: 14 questionnaires, 28 files written, 0 files unchanged, 1 note raised here
[6/8] examples: 14 examples, 14 files written, 0 files unchanged, 2 notes raised here
[7/8] organisation units: 1,332 organisation units, 2,667 files written, 0 files unchanged
[8/8] pages: 6 pages, 20 files written, 0 files unchanged, 1 note raised here
full pipeline: 2,806 file(s) written across 7 target(s)
info: local_basic (fhir.toml) -> /home/you/demo-ig
fhir generate (7)
┏━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃Target ┃ Subject ┃ Directory ┃ Files written ┃ Files deleted ┃ Distinct notes┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│foundation │ - │ ig/input/fsh/foundation │ 23 │ 0 │ 0 │
│option-sets │ 13 option sets │ ig/input/resources/termi… │ 39 │ 0 │ 0 │
│ │ │ resources/concept-maps │ │ │ │
│categories │ 5 categories │ ig/input/resources/categ… │ 15 │ 0 │ 0 │
│ │ │ resources/concept-maps │ │ │ │
│questionnaires │ 14 questionnaires │ ig/input/fsh/data-sets, │ 28 │ 0 │ 1 │
│ │ │ fsh/event-programs, │ │ │ │
│ │ │ fsh/tracker-programs, │ │ │ │
│ │ │ fsh/tracked-entity-types, │ │ │ │
│ │ │ fsh/data-dictionary, │ │ │ │
│ │ │ resources/assignments, │ │ │ │
│ │ │ resources/attribute-opti… │ │ │ │
│ │ │ resources/concept-maps │ │ │ │
│examples │ 14 examples │ ig/input/fsh/examples │ 14 │ 0 │ 1 │
│org-units │ 1,332 organisation units │ ig/input/fsh/organizatio… │ 2,667 │ 0 │ 0 │
│ │ │ resources/registry │ │ │ │
│pages │ 6 pages │ ig/input/pagecontent │ 20 │ 0 │ 0 │
└───────────────┴──────────────────────────┴───────────────────────────┴───────────────┴───────────────┴───────────────┘
note: the run's step lines count 4 note(s) and the table counts 2: a step line
counts what that target raised, and the Distinct notes column counts a note once
for the whole run, on the first target that raised it
note: 2 distinct note(s) across 2 target(s); full list in
/home/you/demo-ig/reports/fhir-generate-notes.md (--details to print)
Every line carries two different numbers. The subject is what the target
covers on the instance - 1,332 organisation units, 14 questionnaires - and the
file counts say how many files publishing that took, which is larger wherever
one covered object ships as several resources: an organisation unit becomes
both an Organization and a Location, so 1,332 units are 2,667 files.
Foundation covers no instance object at all, so its line and its Subject
cell are files alone.
The step lines and the table disagree about the examples on purpose: the step line counts what that target raised on its own, and the table counts each note once, on the first target that raised it - the greys-out note the examples and the pages both repeat is filed under the questionnaires. A run whose two totals differ says which counting each is, in a line under the table, so nobody has to reconstruct it from a target that announced 329 and shows 0.
The bare run is the one to reach for: it reads the instance once and every
target builds off that single result, where seven separate commands each
open a client of their own. It is also the cheap half of the chain - against
a local instance a run like the one above is quick, and the real wall clock
is all in the compile and publish that follows.
uv run d2w fhir generate is the same command through the project's pinned
toolchain, and the closing info: line names the profile it read and the
project it wrote.
Re-running against an unchanged instance converges to zero:
$ d2w fhir generate
running 8 step(s)
[1/8] instance metadata: 14 questionnaire target(s), 13 option set(s), 5 categories,
1,332 organisation unit(s)
[2/8] foundation: 0 files written, 23 files unchanged
[3/8] option sets: 13 option sets, 0 files written, 39 files unchanged
[4/8] categories: 5 categories, 0 files written, 15 files unchanged
[5/8] questionnaires: 14 questionnaires, 0 files written, 28 files unchanged, 1 note raised here
[6/8] examples: 14 examples, 0 files written, 14 files unchanged, 2 notes raised here
[7/8] organisation units: 1,332 organisation units, 0 files written, 2,667 files unchanged
[8/8] pages: 6 pages, 0 files written, 20 files unchanged, 1 note raised here
full pipeline: 0 file(s) written across 7 target(s)
Know the eight targets¶
d2w fhir generate All seven, in that order, off one pass over the instance
d2w fhir generate foundation Identifier aliases + the extensions + the capture contract
d2w fhir generate option-sets Option sets -> CodeSystem/ValueSet pairs
d2w fhir generate categories Categories -> CodeSystem/ValueSet pairs
d2w fhir generate questionnaires Data sets + event programs + tracker programs -> Questionnaires
d2w fhir generate examples Example QuestionnaireResponses answering those Questionnaires
d2w fhir generate org-units Organisation units -> Organization/Location instances
d2w fhir generate pages Narrative site pages + per-artifact intros
d2w fhir generate load-set Synthetic QuestionnaireResponse corpus into load/ (not IG source)
Name a target when you want that target alone - a tight edit loop on one directory. A solo run prints its own detail table and all of its notes:
$ d2w fhir generate pages
running 2 step(s)
[1/2] instance metadata: 14 questionnaire target(s), 1,332 organisation unit(s)
[2/2] pages: 0 written, 20 unchanged, 1 note
fhir generate pages
┌──────────────┬──────────────────────────┐
│profile │ local_basic (fhir.toml) │
│project │ /home/you/demo-ig │
│target │ ig/input/pagecontent │
│files written │ 0 │
│unchanged │ 20 │
│files deleted │ 0 │
│pages │ 6 │
│intros │ 14 │
└──────────────┴──────────────────────────┘
note: data set 'Child Health' (BfMAe6Itzgt) greys out 8 disaggregated cells, which
are not published; a response answering one would not be of the form:
DUSpd8Jq3M7.hEFKSsPV5et, DUSpd8Jq3M7.psbwp3CQEhs, ca8lfO062zg.Prlt0C1RF0s,
ca8lfO062zg.V6L425pT3A0, d5xTg3WR3DP.Prlt0C1RF0s and 3 more
What each target writes, and where:
| Target | Writes |
|---|---|
foundation |
ig/input/fsh/foundation/ - the identifier aliases and NamingSystems, the D2Period / D2FormType / D2AttributeValue and tracker extensions, the response profiles, $generate, and the capture CapabilityStatement. Depends on fhir.toml alone; never touches DHIS2. Prerequisite for a compiling IG. |
option-sets |
Pre-built CodeSystem/ValueSet JSON into ig/input/resources/terminology/, ConceptMaps into resources/concept-maps/. |
categories |
Its own CodeSystem/ValueSet JSON into ig/input/resources/categories/, ConceptMaps into resources/concept-maps/. |
questionnaires |
One Questionnaire per data set (fsh/data-sets/), per event program (fsh/event-programs/), per tracker stage plus the program's registration form (fsh/tracker-programs/<program stem>/), per tracked entity type's person-only registration form (fsh/tracked-entity-types/), the shared data dictionary (fsh/data-dictionary/), assignment Lists (resources/assignments/), attribute-option-combo terminology (resources/attribute-option-combos/), and the two ConceptMaps that map an instance's own statements outward - D2Sex_CM and D2Section_CM - into resources/concept-maps/. |
examples |
One Usage: #example response per target into fsh/examples/<target stem>-<n>.fsh. |
org-units |
Profiles and level terminology into fsh/organization/; the registry as pre-built JSON - Organization-<stem>.json + Location-<stem>.json per unit - into resources/registry/. |
pages |
Six site pages plus per-artifact intros into ig/input/pagecontent/ - markdown, not FSH. |
load-set |
Test data into load/ beside ig/, for posting at a running facade (Serve the IG). Gitignored; the bare run never writes it. |
What the artifacts contain - the extensions, the identifier slices, the value-type mapping, the stems in those file names - is the reference material in Identifiers and the D2 extensions and Terminology and ConceptMaps.
Each target owns its subdirectories and syncs each one: writes what
changed, leaves what did not, deletes generated files that no longer belong.
A JSON sync owns its directory outright - it deletes every *.json the run
did not produce - which is why each terminology pair gets a directory of its
own. concept-maps/ is the one shared directory; ownership there is stated
by file-name prefix. The tracker-programs/ sweep prunes a per-program
subdirectory it emptied. Hand-authored files are safe by the same rule the
whole tree follows: the sweep only deletes files carrying the generated
header, so ig/input/pagecontent/index.md and any markdown you drop beside
it survive every regenerate.
A run that rewrites FSH removes the compile. ig/fsh-generated/ is SUSHI's
output and nobody else's, and the moment a target writes a FSH source it holds
resources compiled from sources that no longer exist. So the run removes it and
says which tree went and why:
note: removed ig/fsh-generated: it held SUSHI's compile of FSH sources this run
rewrote, and check-artifacts, serve, forward and `make build` all read that
tree. Run `make sushi` in the project to compile the sources this run wrote.
A compile that stops on an error removes it too. SUSHI writes
ig/fsh-generated/ as it goes and keeps whatever it had written when it
stopped, and that half reads as a finished guide to everything downstream - the
FHIR endpoint would publish those Questionnaires as though the compile had
succeeded. So make sushi removes the directory when SUSHI exits non-zero, and
says which tree went:
SUSHI stopped on an error. Removed ig/fsh-generated, which held a partial
compile: the FHIR endpoint has nothing to read until a compile finishes.
SUSHI's own exit status is what make reports, so a failing compile still fails
the recipe. The presence of ig/fsh-generated/ therefore means one thing: a
compile that finished.
make sushi writes it again from the sources on disk, and make build runs the
publisher's own SUSHI before it publishes, so a build needs nothing extra.
Serving and forwarding read the compiled tree too, and each already refuses a
project holding none by naming those same two steps - generate, then make
sushi - so a serve right after a generate is the flow those pages already
teach. d2w fhir serve --live reads the instance instead and needs no compile
at all.
A regenerate that writes nothing leaves the compile where it is: the sources it
was compiled from are still the sources on disk. Nothing else is removed -
ig/temp/ is the IG publisher's own scratch, which no command reads between
builds, and ig/output/ is a published site, which is a thing to replace
deliberately rather than to discard on a source edit.
The pre-built resources are not committed. The scaffolded .gitignore
covers ig/input/resources/ - thousands of regenerable files - while
ig/input/fsh/ is committed, so the FSH diff after a metadata change is
still there to review.
Narrow the selection¶
Absent or empty selection tables mean all of that kind on the instance. A national instance carries hundreds of forms, so an IG meant for review names the handful of UIDs it is about:
[generate.data_sets]
include_ids = ["TuL8IOPzpHh"] # EPI Stock
[generate.event_programs]
include_ids = ["EVTsupVis01"] # Supervision visit
[generate.tracker_programs]
include_ids = ["IpHINAT79UW"] # Child Programme - registration + one per stage
[generate.tracked_entity_forms]
include_ids = ["nEenWmSyUEp"] # Person - register a person, no program
[generate.option_sets]
include_ids = ["OsVaccType1"] # Vaccine type
[generate.categories]
include_ids = ["yY2bQYqNt0o", "Qzh0MSUx4RM"]
[generate.organisation_units]
max_level = 4 # or root = "<uid>" for one sub-hierarchy
Name the terminology tables too, and not only the form tables. A table left
absent selects everything of its kind, and one DHIS2 name carrying a raw <
anywhere in that everything refuses the run - which is why every guide in
examples/igs/ names its option sets and its categories explicitly.
Or seed the same lists while scaffolding: d2w fhir init --data-set ...
--event-program ... --tracker-program ... --org-unit-max-level 4 (offline; written
as given). Points worth knowing:
- The two program tables select opposite
programTypes, and a UID listed under the wrong one fails the run by name rather than being quietly reshaped. With empty lists the sweep routes each program by its live type. [generate.tracked_entity_forms]is the person-only shelf. Left empty it publishes a registration form for each tracked entity type the selected tracker programs register - a form that registers somebody in DHIS2 without enrolling them in any program. Name UIDs to publish a different set.- The option-set closure: a selected form binding an option set outside
[generate.option_sets] include_idspulls the set in anyway, with a note. Validate resolves scope the same way, so the two never disagree. - Registry scale: every organisation unit emits two instances, and the
IG publisher validates and renders every resource, so the registry sets the
wall clock of the publisher run.
org-unitswarns once a registry passes 2,000 instances, naming both dials (max_level,root). See Build and publish the guide. - Examples:
[generate.examples]per_targetsets responses per form (0 disables the target);source = "synthetic"(default, deterministic, no data endpoint called) or"instance"(answers from the values the server holds - deliberate on production, since real captured values land in a document you are about to publish). - Locales:
[generate] locales = []means every locale found on the instance; list BCP-47 or DHIS2-style tags (pt_BRandpt-BRboth work) to narrow.NAMEtranslations become concept designations and title/name translation extensions; a question DHIS2 gives a form name to takes its label translation fromFORM_NAME. Nothing else is emitted.
Know which spelling a surface speaks¶
A DHIS2 data element carries two spellings, and they are written for two
audiences. name is written for analytics and reference - it has to be unique
and it reads like a report column. formName is written for input: not always
to shorten, but to clarify what a person is being asked. So each surface speaks
the one it is for.
Input surfaces speak formName, reference surfaces speak name. A
Questionnaire.item.text - a data set question, an event programme question, a
tracker stage question, a registration form's tracked entity attribute, and the
group heading of a disaggregated element - is the form name wherever DHIS2
states one and the name wherever it does not. The data dictionary is the
reference surface, so a D2DE_CS or D2TEA_CS concept keeps displaying the
name and states the form name beside it as a form-name property; a consumer
holding one concept reaches both spellings. The cells of a disaggregated
question are labelled by their category option combo, which has no form name of
its own.
Both spellings are person-written, so both pass the same gate: a < in a form
name refuses a run and is graded a template-hostile-name error by
d2w fhir validate exactly as a < in a name is.
Know what a run refuses to write¶
A DHIS2 name is kept byte-true on the resource it becomes, and a DHIS2 form name
is kept byte-true on the question it labels; the IG publisher writes both into
pages it strict-parses after writing. A < opens a tag, so the parse of the page
it just wrote fails - hours in, once every resource has already been rendered. A
run that would write such a name is refused whole before a single file is
written, naming the object and which of its two spellings carries the character,
and exiting 1:
$ d2w fhir generate
error: dataElements 'Vitamin A given to < 5y' (tU7GixyHhsv), asked by dataSets
'Child Health' (BfMAe6Itzgt), has a name carrying '<'. ...
$ echo $?
1
The whole run is refused rather than the one object skipped, because a quietly skipped object leaves a guide that disagrees with its own selection - a Questionnaire referencing terminology nobody wrote.
This is the same statement d2w fhir validate makes, in both directions.
Validate reads the same hostile_names posture this run does, so under
"refuse" - and unset, which refuses - every name graded a selection-scoped
template-hostile-name error there refuses a run here, and every name refused
here is graded that error there. Under "substitute" neither command stops, and
validate grades those names informational naming the wording published in their
place. It
holds over every kind of object a selection publishes: option sets and their
options, categories and their category options, organisation units, data sets,
event programs, tracker programs and their stages, tracked entity types, and the
data elements and tracked entity attributes those forms ask as questions. Codes
are gated too, on the six collections whose codes become identifier values -
optionSets, categories, organisationUnits, dataSets, programs,
programStages.
Run d2w fhir validate first anyway: generate stops at the first object it
cannot write, so on an instance with several offenders only validate lists them
all. The fix is in DHIS2 - rename the object - or leave it out of the selection.
examples/igs/refused-names/ is a working exhibit of both commands on one
poisoned selection.
A build reads neither of them. make build publishes whatever
ig/fsh-generated/ and ig/input/ hold, so artifacts written before this gate
existed, artifacts from a generate an older lock pinned, and hand-authored FSH
all reach the publisher without ever passing it. d2w fhir check-artifacts is
the same refusal applied to those files, through the same two predicates, and
it is what make build runs first - see The build refuses before it
begins.
Answer the hostile-name question¶
Refusing is one answer, and for many instances it is the wrong one. DHIS2 names
carry < legitimately: an age band is called 5 to < 15 years, Female and a
disaggregation cell is called Male, <15y. Renaming several hundred of those in
a production instance to publish a guide is not a fix - it is the guide's problem
pushed onto the people who run the instance.
So a run has three outcomes, and you choose which:
| Outcome | How you get it | What happens |
|---|---|---|
| Refuse | --refuse-hostile-names, or hostile_names = "refuse" |
The run writes nothing and names the object, exactly as above. Every code is published byte-true. |
| Ask | neither flag, no dial, and a terminal | The names and codes are shown with their rewrites and you answer yes or no. |
| Substitute | --substitute-hostile-names, or hostile_names = "substitute" |
The guide publishes the rewritten wording and the hyphenated codes, and every rewrite is noted. |
A flag beats the fhir.toml dial, and the dial beats the question. With no
terminal to ask on - a script, a CI job - the run never hangs on a prompt: it
prints the same block, names the two flags, and leaves every name and code as
DHIS2 states it.
The name rewrite is wording, not escaping. A comparison becomes the words it
stands for - < reads "under", > reads "over", <= and </= read "at most",
>= and >/= read "at least" - so a reader of the guide reads a sentence:
| DHIS2 name | Published name |
|---|---|
5 to < 15 years, Female |
5 to under 15 years, Female |
Male, <15y |
Male, under 15y |
Age <= 5 |
Age at most 5 |
>/=2500g |
at least 2500g |
>10 sec |
over 10 sec |
Codes carrying a comparison are reworded the way names are, and only under
substitute. A < in a code opens a tag in the table cell an identifier value
lands in, so under refuse the run is refused. Under substitute both < and
> are reworded, so a code and the name beside it read alike, and then each space
is hyphenated:
| DHIS2 code | Published code |
|---|---|
<1500g |
under-1500g |
>10 sec |
over-10-sec |
>/=2500g |
at-least-2500g |
Codes carrying a space are rewritten too, and only under substitute. A
space is legal in an R4 code, so nothing refuses Pre eclampsia - and the IG
publisher's anchor slug strips whitespace, so it and a literal Preeclampsia
render one anchor id (IG_PUBLISHER_ISSUES.md 107), while every URL, CQL quotation, and
terminology server below the guide escapes or quotes the space at its own
discretion. Each space becomes a hyphen:
| DHIS2 code | Published code |
|---|---|
Pre eclampsia |
Pre-eclampsia |
IPT 1 |
IPT-1 |
A published code depends on its own DHIS2 code and nothing else, so adding a form
to the selection or cleaning another code in DHIS2 never changes it. DHIS2 holds
no duplicate codes, but a rewrite can land on one: Pre eclampsia beside a literal
Pre-eclampsia, or <5 beside under 5, which both read under-5. Publishing
either under a suffix would let a published code move from one object to the
other between runs, so the run is refused instead, naming both codes; change one
of them in DHIS2. d2w fhir validate reports the same pair as a
code-substitution-collision error.
DHIS2 is never written to, and nothing is lost. No UID is touched. Every
rewritten concept states the DHIS2 code byte-true as a dhis2-code property - in
the option-set, category, and category-option-combo vocabularies and in the
.../id/*-code identifier CodeSystems alike - and the ConceptMaps keep taking a
published concept back to its DHIS2 UID. The capture path reads that property, so
a QuestionnaireResponse answering with a published code still writes the DHIS2
code to DHIS2.
A substitute build says so. Every generated site page - Forms, Registry,
Terminology, Identifiers, Periods and Capturing data - opens with a note saying
that some names and codes are published in a different spelling from the one the
DHIS2 instance holds, how they were rewritten, and that each rewritten concept
states the instance's spelling as its dhis2-name or dhis2-code property. A run
that substitutes always carries the note; a run that does not never does. The
hand-written index.md is left as it is.
Every rewrite lands in the notes - a name-substitution note per distinct DHIS2
name and a code-substitution note per distinct DHIS2 code, however many
resources carry them - so the notes report is the record of where the guide and
the instance say different things:
note: the DHIS2 name '5 to < 15 years, Female' carries a comparison the IG
publisher's pages cannot carry as it stands; the guide publishes '5 to under
15 years, Female', states '5 to < 15 years, Female' as a `dhis2-name`
property, and DHIS2 keeps the name it holds
note: the DHIS2 code 'IPT 1' carries a space, which the IG publisher's anchors
and every URL downstream of them handle at their own discretion; the guide
publishes 'IPT-1' and states the DHIS2 code beside it as a `dhis2-code` concept
property
Substitution reaches further than the refusal does. The refusal reads the names
and form names of the objects a selection publishes and the questions they ask;
the rewrite reads every name the emission publishes, which also covers the cell
names of a disaggregation. Those reach a Questionnaire's cell labels and the
data dictionary's category option combo displays without passing any refusal -
one national selection generated cleanly and then handed the publisher 738 of
them. Under refuse that class is caught by d2w fhir check-artifacts, before
you spend a build on it; under substitute it never reaches the disk.
Read the notes¶
Each target raises aggregate notes - a selection entry that matched nothing,
an option set the closure pulled in, a form skipped for a linkId
collision. A note carries a kind, and three kinds (code-fallback,
code-collision, stem-fallback) are restatements of what validate already
reports. A bare run counts them - the echoes separately - and writes them
all to reports/fhir-generate-notes.md, each note once, under the first
target that raised it:
note: 3 distinct note(s) across 2 target(s) (+8 validate echoes); full list in
reports/fhir-generate-notes.md (--details to print)
Read that as: generation found three things worth your attention, and eight
more it would only be repeating d2w fhir validate to tell you about.
Nothing is hidden - the file carries every note, echoes under a trailing
### Restatements of validate findings heading. --details prints every
note inline instead; a solo target always prints all of its notes. From the
demo run above:
$ cat reports/fhir-generate-notes.md
# fhir generate notes
- Profile: local_basic (http://localhost:8080)
- Generated: 2026-08-15T18:02:36+00:00
## questionnaires
- data set 'Child Health' (BfMAe6Itzgt) greys out 8 disaggregated cells, which are not published; a response answering one would not be of the form: DUSpd8Jq3M7.hEFKSsPV5et, DUSpd8Jq3M7.psbwp3CQEhs, ca8lfO062zg.Prlt0C1RF0s, ca8lfO062zg.V6L425pT3A0, d5xTg3WR3DP.Prlt0C1RF0s and 3 more
## examples
- 1 question takes an attachment, a geometry document, or a reference to a DHIS2 object the IG does not publish; left unanswered in the synthetic examples: Birth certificate (uf3svrmp8Oj)
The greys-out note is a fact about the form, so the questionnaires, the examples, and the pages targets all raise it - and the file states it once, on the questionnaires, because they are the first target that did.
That first note is the one worth reading twice, because it is a DHIS2 fact
about your form rather than a defect in the run. A data set section can
grey out individual cells of a disaggregated group - DHIS2's own entry
screen renders the cell and refuses to let anyone type in it. Those cells are
not published, so no consumer of this package can answer one either; a
response that did would not be a response to this form. The note names each
one as <data element>.<category option combo> and counts the rest, and the
same note is raised by every target that reads the form.
Four outcomes the notes do not carry¶
A national instance raises several hundred terminology notes per run, and a form nobody can submit would be lost among them. So when an organisation-unit assignment names no organisation unit the run publishes, the run closes with a line of its own:
warning: 29 published form(s) carry an empty organisation-unit assignment under
[generate.organisation_units] max_level 2: no organisation unit may report them,
and the facade refuses to draft a response for one. The assignment hangs on:
Child Programme (IpHINAT79UW), EPI Stock (TuL8IOPzpHh). Widen the
organisation-unit selection - raise `[generate.organisation_units] max_level`, or
set its `root` higher up the hierarchy - or narrow the form selection in
fhir.toml to the forms those organisation units report, then run
`d2w fhir generate` again.
The count is of published Questionnaires, which is what a capture client is
refused at - a tracker program's stages each publish a form and share their
program's one List - so this line, the facade's 422 and
d2w fhir check-artifacts all state one number, and all three prescribe the
same two selections. The names beside it are the data sets and programs DHIS2
hangs the assignment on, because that is the object a reader reassigns.
Choosing a max-level carries the
trade-off that usually produces it.
The second is a form every attribute option combo it declares is restricted
away from. DHIS2 scopes a category option to organisation units on top of the
assignment, and refuses a capture keyed to a combo not usable at the
organisation unit it was filed from with E8025 - so a form whose vocabulary no
organisation unit that may report it admits any concept of is a form nobody can
submit, exactly as an empty assignment is, one axis over. The run already wrote
the restriction Lists that prove it, so it closes with the fact rather than
leaving it to a $generate 422:
warning: 3 published form(s) declare attribute option combos DHIS2 restricts
away from every organisation unit that may report them under
[generate.organisation_units] max_level 3: no capture for one of them can be
keyed to a combo this DHIS2 instance accepts, and the facade refuses to draft a
response for one. They are: EPI Stock (TuL8IOPzpHh), Life-Saving Commodities
(ULowA8V3ucd), Project Management (QX4ZTUbOt3a). Widen the organisation-unit
selection until one of the restricted organisation units is published - raise
`[generate.organisation_units] max_level`, or set its `root` to an organisation
unit above them - or narrow the form selection in fhir.toml to the forms those
organisation units report, then run `d2w fhir generate` again.
d2w fhir check-artifacts files the same fact as a warning-level finding,
reading it back off the published Lists, so a build machine with no DHIS2
connection asks the same question. The examples target publishes no example for
such a form: there is no capture DHIS2 would take, so drafting one would be
publishing the very response the facade then refuses.
The third is that same form one axis further over: one DHIS2 has closed every
combo of. A category option is scoped to a calendar window as well, and DHIS2
refuses a capture the window does not cover entirely with E8032 - so a form
whose every declared combo closed before the period it reports now, which is the
newest completed period of its own period type and therefore before every later
period too, is a form nobody can submit. The run already wrote the windows that
prove it, so it closes with the fact:
warning: 2 published form(s) declare attribute option combos DHIS2 has closed:
no attribute option combo of the form is valid for any period it reports, so no
capture for one of them can be keyed to a combo this DHIS2 instance accepts, and
the facade refuses to draft a response for one. They are: EPI Stock
(TuL8IOPzpHh), Life-Saving Commodities (ULowA8V3ucd). A category option's window
is DHIS2 metadata - `startDate` and `endDate` on the category option - and
fhir.toml has no setting that widens it. Reopen the category options in DHIS2, or
narrow the form selection in fhir.toml to the forms whose attribute option combos
are still open, then run `d2w fhir generate` again.
Unlike the other two, no fhir.toml dial reaches this one, and the line says so
rather than implying one: the window lives on the DHIS2 category option.
d2w fhir check-artifacts files the same fact as a warning-level finding, read
off the windows the vocabulary publishes and the period type the Questionnaire
declares, so a build machine with no DHIS2 connection asks the same question. The
examples target keys an example to a combo still open for the period it reports,
and publishes no example at all for a form none of whose combos is.
The fourth is a selection entry that matched nothing. A UID no longer on the instance - an object renamed, deleted, or never there - costs the guide a whole form, its examples and its page, so the run closes with that line of its own too, naming every UID the instance answered nothing for:
A table none of whose entries matched says so outright - no
[generate.event_programs] include_ids entry matched an event program on this
instance, so this run publishes no event program at all - because a guide with
nothing of a kind in it is rarely what the file meant to ask for.
d2w fhir check-artifacts reports the same entry as a warning-level finding
against fhir.toml, so a tree on disk answers for itself with no connection.
The fifth is not a loss at all - it is a gap the run made on purpose, and says
so because nothing else would. A DHIS2 program rule whose action is ASSIGN
computes a question's answer on import, and DHIS2 refuses a payload whose answer
is neither empty nor byte-equal to the value it calculated (E1307), so every
example this run writes leaves those questions unanswered:
note: 4 published form(s) ask 5 question(s) a DHIS2 program rule computes the
answer to on import, so every example this run wrote leaves them unanswered -
DHIS2 refuses a capture answering one anything but the value it calculated, with
E1307. They are: Antenatal care visit (WZbXY0S00lP), Child Programme - Birth
(A03MvHHogjR), ...
Without the line, a form whose corpus answers nine of thirteen questions reads
as an emitter that lost four. d2w fhir check-artifacts files the other half of
the fact: an example that answers one of those questions anyway, in the compiled
JSON or in a hand-authored FSH source.
Every command with an instance behind it narrates its steps on stderr - a
spinner on a terminal, one plain [k/N] label: summary line per step when
redirected. --no-progress turns it off; d2w --json fhir generate implies
it and puts the per-target file lists and counts on stdout as JSON.
Check the site pages landed¶
pages is the last target and the only one writing markdown: forms.md,
registry.md, terminology.md, identifiers.md, periods.md, and
capture.md - the scaffolded site menu - plus per-artifact intros the
publisher injects into matching artifact pages (Questionnaire-<stem>-intro.md
always; CodeSystem and Organization intros only when the DHIS2 object
carries a description). index.md is scaffolded once and is yours. Every
DHIS2 name and description on these pages is escaped on the way in, so a
data set called "Mortality < 5 years by gender" renders as text rather than
aborting the publisher's HTML parse.