Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/v/0.38.0/llms.txt
Full corpus: https://docs.registrystack.org/v/0.38.0/llms-full.txt

# Issue an immunization summary from DHIS2

> Author and verify a signed, multi-concept immunization summary from synthetic DHIS2 Tracker events.

import QuickstartMeta from '../../../components/QuickstartMeta.astro';

Complete [Return a governed value](../return-a-governed-value/) before starting this tutorial.
You will apply that pattern to the public DHIS2 Tracker demo, combine two immunization events into
one governed answer, and verify the signed assertion returned across the Evidence Gateway HTTP
boundary.

<QuickstartMeta
  outcome="A verified assertion containing five governed immunization readings from DHIS2."
  time="About 30 minutes"
  level="Institution source with synthetic data"
  prerequisites={[
    'The completed governed-value tutorial',
    'The Evidence Gateway toolset',
    'Access to the public DHIS2 demo',
    'curl, python3, and an editor',
  ]}
/>

This tutorial uses synthetic data from the public DHIS2 demo.
Do not substitute a real child record.
The demo maintainers can reset its data, credentials, and synthetic subjects.

## Define the answer before the source

The DHIS2 Child Programme records several immunization readings across its Birth and Baby
Postnatal events.
The assertion returns five governed concepts:

| Concept | Form | Meaning |
|---|---|---|
| BCG recorded as administered | Boolean | What the record currently shows for a BCG dose, administered or not. |
| OPV dose count | Bounded integer | The oral polio vaccine dose number the record currently shows, from 0 through 3. |
| Pentavalent dose count | Bounded integer | The pentavalent vaccine dose number the record currently shows, from 0 through 3. |
| Measles recorded as administered | Boolean | What the record currently shows for a measles dose, administered or not. |
| Yellow fever recorded as administered | Boolean | What the record currently shows for a yellow fever dose, administered or not. |

This is a recorded immunization summary, not a conclusion that the child is fully vaccinated or
up to date.
Those conclusions require an approved immunization schedule, the child's age, a jurisdiction,
and rules for exceptional or late doses.

The summary also reports the record's current state, not the state of a closed visit.
The [DHIS2 Tracker API](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-243/tracker.html)
gives an event the `ACTIVE` status by default, and states that only a super user or a user
holding the `F_UNCOMPLETE_EVENT` authority can modify a completed event.
`COMPLETED` therefore marks an event closed to further editing, not an event whose data values
are the only recorded ones.
The distinction decides whether this tutorial returns anything at all.
A sample of 2000 Child Programme tracked entities taken from the public demo on 2026-08-07 held
3885 `ACTIVE` events against 7 `COMPLETED` ones, and no record carried all five immunization
readings on `COMPLETED` events alone.
The demo maintainers can reset that data at any time, so treat the counts as one dated
observation rather than a standing property of the dataset.
The source adapter accepts Birth and Baby Postnatal events in both statuses.
Each assertion describes the record at the time of the request, and a later correction to an
event changes what the next assertion says.

The source adapter also applies two conservative readings:

- An absent source value does not become `false` or `0`.
- Two different values for the same concept make the source record inconsistent.

In either case, Evidence Gateway returns no assertion.

## Choose a synthetic child

Create a working directory and an owner-only curl configuration at `.local/dhis2.curl`:

```sh
mkdir dhis2-immunization-tutorial
cd dhis2-immunization-tutorial
umask 077
mkdir -p .local
touch .local/dhis2.curl
chmod 600 .local/dhis2.curl
```

Open the file and add the provider-published shared credential for the
[public DHIS2 demo](https://play.im.dhis2.org/stable-2-43-1/):

```text
user = "admin:district"
```

The same account signs in to the demo's web interface, where Tracker Capture shows the
`Child Programme` records this tutorial reads.

Set the source values in your terminal:

```sh
export DHIS2_BASE_URL='https://play.im.dhis2.org/stable-2-43-1'
export DHIS2_PROGRAM_ID='IpHINAT79UW'
```

Not every synthetic child carries a value in all five immunization fields, so list a page of
`Child Programme` records with the field selection the source will use:

```sh
curl --silent --show-error --fail \
  --config .local/dhis2.curl \
  --get \
  --url "$DHIS2_BASE_URL/api/tracker/trackedEntities" \
  --data-urlencode "program=$DHIS2_PROGRAM_ID" \
  --data-urlencode 'orgUnitMode=ACCESSIBLE' \
  --data-urlencode 'fields=trackedEntity,enrollments[program,events[programStage,status,dataValues[dataElement,value]]]' \
  --data-urlencode 'pageSize=200' \
  --output .local/dhis2-candidates.json
```

The [DHIS2 Tracker API](https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-243/tracker.html)
applies `orgUnitMode=ACCESSIBLE` when a request sends no `orgUnits`, so this page spans every
organisation unit the demo account can read.
The response wraps the records in a `trackedEntities` array beside a `pager` object.

Create `choose-dhis2-subject.py`. The script picks the first record that carries all five
readings without contradicting itself, applying the same programme, stage, status, and
data-element rule the source extractor applies later. It writes only that record's identifier to
an owner-only local file and prints no identifier:

```python
import json
import os
import pathlib

STAGES = {"A03MvHHogjR", "ZzYYXq4fJie"}
STATUSES = {"ACTIVE", "COMPLETED"}
ELEMENTS = {
    "bx6fsa0t90x": "bcg",
    "ebaJjqltK5N": "opv",
    "vTUhAUZFoys": "penta",
    "FqlgKAG8HOu": "measles",
    "rxBfISxXS2U": "yellow_fever",
}

page = json.loads(pathlib.Path(".local/dhis2-candidates.json").read_text())
records = page.get("trackedEntities", [])
chosen = None
for record in records:
    readings = {}
    consistent = True
    for enrollment in record.get("enrollments", []):
        if enrollment.get("program") != "IpHINAT79UW":
            continue
        for event in enrollment.get("events", []):
            if event.get("programStage") not in STAGES:
                continue
            if event.get("status") not in STATUSES:
                continue
            for reading in event.get("dataValues", []):
                name = ELEMENTS.get(reading.get("dataElement"))
                if name is None:
                    continue
                value = reading.get("value")
                if readings.setdefault(name, value) != value:
                    consistent = False
    if consistent and len(readings) == len(ELEMENTS):
        chosen = record["trackedEntity"]
        break

if chosen is None:
    raise SystemExit(
        f"none of the {len(records)} records on this page carries all five readings"
    )

subject = pathlib.Path(".local/dhis2-subject.txt")
descriptor = os.open(subject, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(descriptor, "w", encoding="utf-8") as destination:
    destination.write(chosen)
print(f"chose 1 subject from {len(records)} records")
```

Run it:

```sh
python3 choose-dhis2-subject.py
```

It prints only a count of the records it examined, so no synthetic identifier reaches your
screen, your shell history, or a tracked file.
When it reports that no record on the page qualifies, add `--data-urlencode 'page=2'` to the list
request and run both commands again.

Read the chosen subject into your terminal:

```sh
export DHIS2_TRACKED_ENTITY_ID="$(cat .local/dhis2-subject.txt)"
```

The identifier selects the source record.
It is used for the DHIS2 read and the Evidence Gateway request, and it does not appear in the
signed assertion.

## See the DHIS2 boundary

Read the same bounded fields that Evidence Gateway will use:

```sh
curl --silent --show-error --fail \
  --config .local/dhis2.curl \
  --get \
  --url "$DHIS2_BASE_URL/api/tracker/trackedEntities/$DHIS2_TRACKED_ENTITY_ID" \
  --data-urlencode "program=$DHIS2_PROGRAM_ID" \
  --data-urlencode 'fields=trackedEntity,enrollments[program,events[programStage,status,dataValues[dataElement,value]]]' \
  --output .local/dhis2-response.json
```

The response contains the tracked entity, its programme enrollment, the two events, and their
data values.
Some event data values are unrelated to the five concepts.
Evidence Gateway will receive the bounded response, discard unrelated values during extraction,
and release only the five declared concepts.

This distinction is deliberate.
The source uses the `record-transformed` posture because DHIS2 cannot filter the `dataValues`
array to individual data-element identifiers in this response shape.

## Create the authoring project

Download the full DHIS2 OpenAPI document and create an editable local project:

```sh
curl --silent --show-error --fail \
  --config .local/dhis2.curl \
  --header 'Accept: application/x-yaml' \
  --url "$DHIS2_BASE_URL/api/openapi.yaml" \
  --output .local/dhis2.openapi.yaml

evidencectl init dhis2-immunization \
  --openapi .local/dhis2.openapi.yaml \
  --profile local

cd dhis2-immunization
```

`evidencectl init` retains the OpenAPI document and creates empty `selectors/`, `sources/`,
`adapters/`, `schemas/`, `questions/`, `derivations/`, and `fixtures/` directories.
It generates only disposable local Evidence Gateway signing, audit, and subject-binding material.
It does not invent the source policy or the answer.

## Draft the DHIS2 source

Ask `evidencectl` to draft the selected operation and response fields from the retained OpenAPI
document:

```sh
evidencectl source suggest . \
  --source-id child-tracker \
  --operation 'GET /api/tracker/trackedEntities/{uid}' \
  --select /trackedEntity \
  --select '/enrollments/*/program' \
  --select '/enrollments/*/events/*/programStage' \
  --select '/enrollments/*/events/*/status' \
  --select '/enrollments/*/events/*/dataValues/*/dataElement' \
  --select '/enrollments/*/events/*/dataValues/*/value'
```

The full DHIS2 response schema contains unions and `uid` formats outside the closed authoring
subset that `evidencectl` accepts.
The command reports those skipped fields and the bounds that still need review.
Continue only when the command exits successfully and writes the six files listed next.

The command creates one editable source, two scripts, and three schemas:

```text
sources/child-tracker.yaml
adapters/child-tracker-prepare.rhai
adapters/child-tracker-extract.rhai
schemas/child-tracker-parameters.schema.yaml
schemas/child-tracker-response.schema.yaml
schemas/child-tracker-facts.schema.yaml
```

## Define the tracked-entity selector

Create `selectors/child-tracked-entity-v1.yaml`:

```yaml
maximumAggregateBytes: 64
fields:
  tracked_entity_id:
    type: string
    minimumBytes: 11
    maximumBytes: 11
```

A DHIS2 identifier is exactly eleven characters.
This bounded field is the only value a caller may send for the child role, and it selects the
source record without reaching the assertion.

## Configure the bounded source

Replace `sources/child-tracker.yaml` with:

```yaml
transport: http-json
baseUrl: https://play.im.dhis2.org
posture: record-transformed
authentication:
  kind: basic
  usernameRef: secret:file/dhis2-username
  passwordRef: secret:file/dhis2-password
request:
  method: GET
  pathTemplate: /stable-2-43-1/api/tracker/trackedEntities/{uid}
  pathBindings:
    uid: {from: selector, role: child, profile: child-tracked-entity-v1, field: tracked_entity_id}
  fixedHeaders:
    - name: Accept
      value: application/json
  selectorInputs:
    - role: child
      alternatives:
        - profile: child-tracked-entity-v1
          fields: [tracked_entity_id]
  prepareScript: adapters/child-tracker-prepare.rhai
  adapterParameters:
    programId: IpHINAT79UW
    responseFields: trackedEntity,enrollments[program,events[programStage,status,dataValues[dataElement,value]]]
  adapterParametersSchema: schemas/child-tracker-parameters.schema.yaml
  preparationLimits:
    query: required
    jsonBody: forbidden
    maximumQueryPairs: 2
    maximumQueryNameBytes: 16
    maximumQueryValueBytes: 256
    maximumStringBytes: 256
    maximumNormalizedBytes: 4096
  projection:
    - /trackedEntity
    - /enrollments/*/program
    - /enrollments/*/events/*/programStage
    - /enrollments/*/events/*/status
    - /enrollments/*/events/*/dataValues/*/dataElement
    - /enrollments/*/events/*/dataValues/*/value
  redirects: deny
  timeoutMilliseconds: 10000
  maximumResponseBytes: 262144
  concurrencyLimit: 8
responseSchema: schemas/child-tracker-response.schema.yaml
extractScript: adapters/child-tracker-extract.rhai
factSchema: schemas/child-tracker-facts.schema.yaml
```

`baseUrl` carries an origin only, so the demo's `/stable-2-43-1` prefix belongs to the path
template.
The path binding accepts only the authorized `child.tracked_entity_id` selector.
The caller cannot replace the DHIS2 origin, programme, field selection, response size, or source
credential.

Add the source credential as owner-only files:

```sh
touch secrets/dhis2-username secrets/dhis2-password
chmod 600 secrets/dhis2-username secrets/dhis2-password
```

Open `secrets/dhis2-username` and enter `admin`.
Open `secrets/dhis2-password` and enter `district`.
Do not add quotes or a trailing newline.
The values stay under the project's ignored `secrets/` directory and are never copied into the
source definition.

## Prepare the bounded read

The tracked entity identifier reaches DHIS2 through the path binding.
The programme and field selection reach it as the only two query parameters the source allows.
Replace `adapters/child-tracker-prepare.rhai` with:

```rhai
fn prepare(selectors, context) {
    let parameters = context["parameters"];
    #{
        query: [
            #{name: "program", value: parameters["programId"]},
            #{name: "fields", value: parameters["responseFields"]}
        ],
        body: ()
    }
}
```

Replace `schemas/child-tracker-parameters.schema.yaml` with:

```yaml
type: object
additionalProperties: false
required: [programId, responseFields]
properties:
  programId: {const: IpHINAT79UW}
  responseFields:
    const: trackedEntity,enrollments[program,events[programStage,status,dataValues[dataElement,value]]]
```

The schema closes both parameters around their reviewed values, so a later edit cannot widen the
programme or the field selection.
The script reads only those parameters and the authorized selector.
It cannot read the source credential, the caller identity, the purpose, or the signing keys.

## Close the response bounds

Evidence Gateway validates the projected response before extraction runs, and every array in a
schema must state its bound.
Replace `schemas/child-tracker-response.schema.yaml` with:

```yaml
type: object
additionalProperties: false
required: [trackedEntity]
properties:
  trackedEntity: {type: string, minLength: 11, maxLength: 11}
  enrollments:
    type: array
    maxItems: 4
    items:
      type: object
      additionalProperties: false
      required: [program]
      properties:
        program: {type: string, minLength: 11, maxLength: 11}
        events:
          type: array
          maxItems: 32
          items:
            type: object
            additionalProperties: false
            required: [programStage, status, dataValues]
            properties:
              programStage: {type: string, minLength: 11, maxLength: 11}
              status: {type: string, minLength: 1, maxLength: 16}
              dataValues:
                type: array
                maxItems: 64
                items:
                  type: object
                  additionalProperties: false
                  required: [dataElement, value]
                  properties:
                    dataElement: {type: string, minLength: 11, maxLength: 11}
                    value: {type: string, minLength: 1, maxLength: 256}
```

A child with no enrollment and an enrollment with no event both stay valid, and the extraction
script decides what they mean.
A response that exceeds any bound is refused before a script sees it.

## Normalize the two events

The OpenAPI document describes the nested response shape, but it cannot decide how repeated event
values become one answer.
Open `adapters/child-tracker-extract.rhai` and replace its draft body with this reviewed rule:

```rhai
fn source_boolean(value) {
    if value == "true" { return true; }
    if value == "false" { return false; }
    throw("source_protocol_error");
}

fn dose_count(value) {
    let count = parse_integer(value);
    if count < 0 || count > 3 { throw("source_protocol_error"); }
    count
}

fn merge_reading(current, next) {
    if is_missing(current) { return next; }
    if current != next { throw("source_protocol_error"); }
    current
}

fn extract(source_response, context) {
    let parameters = context["parameters"];
    let bcg = ();
    let opv = ();
    let penta = ();
    let measles = ();
    let yellow_fever = ();

    let enrollments = get_path(source_response, "/enrollments");
    if is_missing(enrollments) { return #{outcome: "no_match"}; }

    for enrollment in enrollments {
        if enrollment["program"] == "IpHINAT79UW" {
            let events = get_path(enrollment, "/events");
            if !is_missing(events) {
                for event in events {
                    let stage = event["programStage"];
                    let accepted_stage =
                        stage == "A03MvHHogjR" || stage == "ZzYYXq4fJie";
                    // A DHIS2 event is ACTIVE by default, and COMPLETED marks
                    // it closed to further editing rather than marking its
                    // readings final. Both statuses carry recorded data
                    // values, so the extractor reads both.
                    let status = event["status"];
                    let accepted_status =
                        status == "COMPLETED" || status == "ACTIVE";
                    if accepted_stage && accepted_status {
                        for reading in event["dataValues"] {
                            let element = reading["dataElement"];
                            let value = reading["value"];
                            if element == "bx6fsa0t90x" {
                                bcg = merge_reading(bcg, source_boolean(value));
                            } else if element == "ebaJjqltK5N" {
                                opv = merge_reading(opv, dose_count(value));
                            } else if element == "vTUhAUZFoys" {
                                penta = merge_reading(penta, dose_count(value));
                            } else if element == "FqlgKAG8HOu" {
                                measles = merge_reading(measles, source_boolean(value));
                            } else if element == "rxBfISxXS2U" {
                                yellow_fever = merge_reading(
                                    yellow_fever,
                                    source_boolean(value)
                                );
                            }
                        }
                    }
                }
            }
        }
    }

    let facts = #{record_reference: source_response["trackedEntity"]};
    if !is_missing(bcg) { facts["bcg_recorded"] = bcg; }
    if !is_missing(opv) { facts["opv_dose_count"] = opv; }
    if !is_missing(penta) { facts["penta_dose_count"] = penta; }
    if !is_missing(measles) { facts["measles_recorded"] = measles; }
    if !is_missing(yellow_fever) {
        facts["yellow_fever_recorded"] = yellow_fever;
    }
    #{outcome: "match", facts: facts}
}
```

The extractor reads every bounded event and data value.
It accepts `ACTIVE` and `COMPLETED` Birth and Baby Postnatal events, converts DHIS2 strings into
typed facts, and refuses conflicting readings.
Reading both statuses widens what `merge_reading` compares: a concept recorded once on an
`ACTIVE` event and again on a `COMPLETED` event must carry the same value, or the source record
is inconsistent and Evidence Gateway returns no assertion.
Unrelated data elements are never added to `facts`.

Replace `schemas/child-tracker-facts.schema.yaml` with the closed extraction result:

```yaml
type: object
additionalProperties: false
required:
  - record_reference
  - bcg_recorded
  - opv_dose_count
  - penta_dose_count
  - measles_recorded
  - yellow_fever_recorded
properties:
  record_reference: {type: string, minLength: 11, maxLength: 64}
  bcg_recorded: {type: boolean}
  opv_dose_count: {type: integer, minimum: 0, maximum: 3}
  penta_dose_count: {type: integer, minimum: 0, maximum: 3}
  measles_recorded: {type: boolean}
  yellow_fever_recorded: {type: boolean}
```

A missing immunization reading now fails the fact contract.
Evidence Gateway does not convert absence into a negative answer.

## Author the multi-concept question

Create `questions/immunization-summary.yaml`:

```yaml
id: immunization-summary
question: Which immunizations are recorded for this child?
purpose: care-continuity
subject:
  role: child
  selector: tracked_entity_id
  profile: child-tracked-entity-v1
  derivation: true
source:
  ref: child-tracker
answers:
  - concept: bcg_recorded_as_administered
    type: boolean
  - concept: opv_dose_count_recorded
    type: bounded-integer
    minimum: 0
    maximum: 3
  - concept: penta_dose_count_recorded
    type: bounded-integer
    minimum: 0
    maximum: 3
  - concept: measles_recorded_as_administered
    type: boolean
  - concept: yellow_fever_recorded_as_administered
    type: boolean
derivation: derivations/immunization-summary.rhai
disclosure:
  allow:
    - bcg_recorded_as_administered
    - opv_dose_count_recorded
    - penta_dose_count_recorded
    - measles_recorded_as_administered
    - yellow_fever_recorded_as_administered
```

`source.ref` names the reviewed source, whose closed fact schema decides what the derivation
receives.
The `answers` list declares the exact concepts and their forms.
The `disclosure.allow` list must match those concepts in order.
Adding another answer or changing an integer bound is a reviewed project change.

Create `derivations/immunization-summary.rhai`:

```rhai
fn answer(facts, selectors, context) {
    if facts["record_reference"] !=
       selectors["child"]["values"]["tracked_entity_id"] {
        throw("derivation_input_error");
    }
    #{
        bcg_recorded_as_administered: facts["bcg_recorded"],
        opv_dose_count_recorded: facts["opv_dose_count"],
        penta_dose_count_recorded: facts["penta_dose_count"],
        measles_recorded_as_administered: facts["measles_recorded"],
        yellow_fever_recorded_as_administered: facts["yellow_fever_recorded"]
    }
}
```

The first comparison prevents a response for another tracked entity from being evaluated.
The returned map must contain exactly the aliases declared under `answers`.
Evidence Gateway validates every value against its declared form before signing.

## Start the project

Compile the editable source and question into one immutable local generation, then start Evidence
Gateway and its local issuer:

```sh
evidencectl dev start .
```

```text
Evidence ready at http://127.0.0.1:8080
Issuer ready at http://127.0.0.1:8081
```

This command validates the source schema, collection bounds, extractor, fact schema, question,
answer forms, derivation, disclosure list, and secret bindings before either service becomes
ready.
Local assurance keeps the project editable and does not require production fixtures.

## Request the real assertion

Write the subject into an owner-only request file. The identifier is read from the file the
discovery script already wrote, so it is never typed and never expanded onto a command line:

```sh
python3 - <<'PY'
import json
import os
import pathlib

value = pathlib.Path("../.local/dhis2-subject.txt").read_text().strip()
selection = {"subjects": [{"role": "child", "field": "tracked_entity_id", "value": value}]}
descriptor = os.open(
    "../.local/dhis2-subjects.json", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600
)
with os.fdopen(descriptor, "w", encoding="utf-8") as destination:
    json.dump(selection, destination, separators=(",", ":"))
    destination.write("\n")

print("Subject file: ready")
PY
```

Prepare a closed request and its independent verification expectations for that subject:

```sh
evidencectl request prepare immunization-summary \
  --purpose care-continuity \
  --subjects-file ../.local/dhis2-subjects.json \
  --name immunization-summary
```

Use this only with the public synthetic record.
`evidencectl` takes either `--subject` or `--subjects-file` and refuses both, so this request
carries no identifier on its command line. It reads the file only when the file is a regular file
you own with exactly one link and mode 0600.

Send that request across the Evidence Gateway HTTP boundary:

```sh
curl --silent --show-error --fail-with-body \
  --config .evidence/requests/immunization-summary/authorization.curl \
  --request POST \
  --url http://127.0.0.1:8080/v1/evidence \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/jose+json' \
  --data-binary @.evidence/requests/immunization-summary/request.json \
  --output immunization-summary.jws.json
```

The caller contacts Evidence Gateway.
Evidence Gateway authenticates and authorizes the caller, performs one bounded authenticated
DHIS2 read, derives the five concepts, audits the disclosure, and returns a signed flattened JSON
Web Signature (JWS).

## Verify before reading

Verify the response against the retained request and trust expectations:

```sh
evidencectl verify immunization-summary.jws.json \
  --context .evidence/requests/immunization-summary/verification.json \
  --output immunization-summary.verified.json
```

```text
VERIFIED
```

Inspect the verified payload:

```sh
python3 -m json.tool immunization-summary.verified.json
```

The selected demo record determines the boolean and dose-count values.
The supported values have this shape:

```json
[
  {
    "providesValueFor": "urn:registrystack:evidence:local:concept:immunization-summary:bcg_recorded_as_administered",
    "value": false
  },
  {
    "providesValueFor": "urn:registrystack:evidence:local:concept:immunization-summary:opv_dose_count_recorded",
    "value": 1
  },
  {
    "providesValueFor": "urn:registrystack:evidence:local:concept:immunization-summary:penta_dose_count_recorded",
    "value": 1
  },
  {
    "providesValueFor": "urn:registrystack:evidence:local:concept:immunization-summary:measles_recorded_as_administered",
    "value": true
  },
  {
    "providesValueFor": "urn:registrystack:evidence:local:concept:immunization-summary:yellow_fever_recorded_as_administered",
    "value": true
  }
]
```

The verified assertion contains no tracked entity identifier, event identifier, event date,
programme enrollment, unrelated health reading, or source credential.

## Inspect the audit and clean up

Stop the local services:

```sh
evidencectl dev stop
```

Inspect the last operation:

```sh
evidencectl audit show --last-operation
```

```text
ACCESS AUTHORIZED immunization-summary care-continuity requester=<pseudonym>
DISCLOSURE RELEASED bcg_recorded_as_administered, opv_dose_count_recorded, penta_dose_count_recorded, measles_recorded_as_administered, yellow_fever_recorded_as_administered
```

The requester value changes on each fresh project.

Read those two lines for what they leave out.
They name the requester pseudonym, the question, the purpose it was authorized under, and the five
concepts released.
Neither line carries a `true`, a `false`, or a dose count, so the audit trail cannot answer the
question it recorded.
Neither line carries the tracked entity identifier, an event identifier, a data element, or any
part of the DHIS2 response.
An operator reviewing this trail can establish who asked, why, and which readings were released,
and cannot learn the child's immunization record from it.

Remove the stopped local generation and the local DHIS2 artifacts:

```sh
evidencectl dev clean
cd ..
rm -f choose-dhis2-subject.py \
  .local/dhis2-candidates.json \
  .local/dhis2-subject.txt \
  .local/dhis2-subjects.json \
  .local/dhis2-response.json \
  .local/dhis2.curl \
  .local/dhis2.openapi.yaml
```

Keep the project editable while evaluating the source.
Before deployment, add project-specific fixtures, replace the demo account with a least-privilege
service account, review the source acquisition posture, and build a reviewed production candidate.

## Troubleshooting

| Symptom | Cause | Resolution |
| --- | --- | --- |
| `curl` fails with 404 against `$DHIS2_BASE_URL` | The demo publishes each DHIS2 release under its own version path, and this tutorial pins `stable-2-43-1`. | Take the current version path from the demo landing page, then update `DHIS2_BASE_URL` and the `pathTemplate` prefix in `sources/child-tracker.yaml`. Re-read the Tracker API reference for that version before trusting the field names. |
| `curl` fails with 401 | The shared demo account was rotated or disabled. | Take the credential the demo publishes now and update `.local/dhis2.curl`, `secrets/dhis2-username`, and `secrets/dhis2-password`. Do not put a real DHIS2 account in these files. |
| `choose-dhis2-subject.py` reports that no record on the page carries all five readings | The demo data was reset, or this page of records holds no complete Child Programme case. | Request a later page with `page=2`, or raise `pageSize`. Do not drop a concept from the question so a partial record qualifies. |
| Writing the subject file fails with `FileExistsError` | `os.open` refuses `O_EXCL` when the file is already there, so a second run cannot overwrite it. | Remove `../.local/dhis2-subjects.json` and run the step again. |
| Evidence Gateway returns no assertion for a subject that worked earlier | The record changed: a reading was removed, or an `ACTIVE` and a `COMPLETED` event now disagree. | Run the discovery step again and pick a current subject. Leave `merge_reading` refusing conflicts. A disagreement between two events is a fact about the record. |
| Evidence Gateway reports a source dependency failure | The shared demo was slow or unreachable, or the response exceeded `timeoutMilliseconds` or `maximumResponseBytes`. | Retry later. Treat the bounds as reviewed source policy, and raise one only after deciding it is right for the deployment. |
| `curl` fails with a certificate error | An intercepting proxy or an out-of-date trust store on your machine. | Repair the trust store, or exempt the demo host from interception. Do not reach for `--insecure`: the source denies redirects and reaches the demo over HTTPS only. |

## Next

- [See Evidence Gateway refuse unsafe requests](../refuse-unsafe-evidence-requests/)
- [Build and deploy an Evidence Gateway project](../build-and-deploy-evidence-project/)
- [Verify an assertion as a consumer](../verify-an-assertion-as-a-consumer/)