# DC Pipeline Contract The DC pipeline for a single Agentforce session runs as three stages in sequence: ``` fetch_dc.py → 24 dc..json artifacts + dc._session_manifest.json assemble_dc.py → dc._session_tree.json (pure in-memory join) render_dc.py → dc._session_summary.md (pure tree reader) ``` Each stage is independently runnable. `fetch_dc.py` chains the two downstream stages by default; `--no-assemble` / `--no-render` opt out of each. The renderer can also run standalone against a tree produced by a prior invocation. This document owns the shape + invariants of stages 2 and 3. For per-DMO field reference of what stage 1 produces, see [`dc_dmo_fields.md`](./dc_dmo_fields.md). --- ## 1. Pipeline overview | Stage | Script | Input | Output | Scope | |---|---|---|---|---| | Fetch | `scripts/fetch_dc.py` | org + session id | 24 `dc..json` + `dc._session_manifest.json` | DC Query REST API, 5-wave waterfall | | Assemble | `scripts/assemble_dc.py` | fetch outputs | `dc._session_tree.json` | pure in-memory join, no fetches | | Render | `scripts/render_dc.py` | tree + manifest | `dc._session_summary.md` | pure tree reader, no DMO loads | Nothing downstream of fetch issues DC queries. Everything after fetch is pure compute over files on disk, which means the renderer can iterate on markdown format without re-fetching. --- ## 2. Assembly stage `scripts/assemble_dc.py` joins the fetched DC rows into a hierarchical tree. ### 2.1 Purpose + scope **Inputs:** 24 `dc..json` artifacts + `dc._session_manifest.json` produced by `scripts/fetch_dc.py`, under `DATA_ROOT//`. **Output:** `dc._session_tree.json` — session-rooted hierarchical view. Interaction → Step → Generation → GatewayRequest; audit rows (Tag, ObjRecord, Metadata, LLM, Quality, Category, Feedback, FeedbackDetail) nested under the right parent. **Contract:** pure in-memory compute, no new DMO queries, forward-only joins on already-fetched rows. Chain-orphan GW calls fall through to a timestamp-window rule with an explicit `binding_method` flag. ### 2.2 Inputs The assembler does **not** hard-code the 24 template names. It reads `dc._session_manifest.json` first, then iterates `manifest["queries"][*]["name"]` to populate the row map. When `fetch_dc.py` adds a 25th template, the row loader picks it up automatically — the tree logic only references names it knows how to place, so a new DMO is loaded but not rendered until the assembler is taught what to do with it. Per-field schema for every DMO lives in [`dc_dmo_fields.md`](./dc_dmo_fields.md). ### 2.3 Declared binding chain The "normal" path that nests a `GatewayRequest` under its owning `LLM_STEP` in the tree. Every edge is a platform-sanctioned forward FK: ``` Step.ssot__GenerationId__c → Generation.generationId__c Generation.generationResponseId__c → GatewayResponse.generationResponseId__c GatewayResponse.generationRequestId__c → GatewayRequest.gatewayRequestId__c ``` **Which Step types populate `ssot__GenerationId__c`?** Only `LLM_STEP` (verified live). `ACTION_STEP`, `TOPIC_STEP`, `TRUST_GUARDRAILS_STEP`, and `SESSION_END` always have `NOT_SET` here; the chain never resolves for them. **Expected declared-share on typical Agentforce sessions:** minority. Most gateway calls on a real session land via the timestamp-window fallback, not the declared chain. Sessions that rely on Flow-invoked prompt-template actions produce zero declared bindings and many timestamp_window bindings — normal, not a defect. ### 2.4 Timestamp-window fallback Any `GatewayRequest` not reached via the declared chain is placed by timestamp containment. **Window semantics:** - **Closed-closed:** `start_ts <= gw_req.timestamp__c <= end_ts`. - **Null end_ts** (active session / interaction / step) is treated as `+∞`. - **Missing start_ts**: fall through to the next preference tier; never match on start alone. - Boundary ties bind to the earlier parent (Step) in preference to the later one (Interaction). **Tier dominates containment.** Selection order is: 1. Find all Step windows containing the GW timestamp. 2. Restrict to the highest-preference tier present: `ACTION_STEP` → `TOPIC_STEP` → `TRUST_GUARDRAILS_STEP` → any other (catch-all: LLM_STEP without declared binding, SESSION_END, unknown types — never excluded). 3. Among those, pick the innermost (shortest-window) Step. 4. On exact window-size ties, pick the latest `start_ts`. Example: if both a wide `ACTION_STEP` and a narrower nested `TOPIC_STEP` contain the GW timestamp, the `ACTION_STEP` wins because its tier dominates — even though TOPIC is innermost. **Placement fallbacks:** - If no Step window matches, attach to the enclosing `Interaction` as a `timestamp_bound_gateway_calls[]` entry (`bound_to_step_id` is null). - If no Interaction window matches, land in `session.unbound_gateway_calls[]`. **Never declared-bound twice.** A Step that already owns a GW via the declared chain is excluded from timestamp-window candidates. ### 2.5 `binding_method` enum Every GatewayRequest in the tree carries exactly one of these: | Value | When emitted | |---|---| | `declared` | Placed under a Step via the declared chain in §2.3. | | `timestamp_window` | Placed by §2.4 (under a Step or an Interaction). | | `unbound` | Neither chain nor window matched; landed in `session.unbound_gateway_calls[]`. | **Collision handling.** When two Steps' Generation chains resolve to the same GatewayRequest (`Step.ssot__GenerationId__c` collisions at the upstream DMO), first-step-wins: the earlier Step in walk order claims the GW. Subsequent Steps get `"gateway_request": null` with an adjacent `"gateway_request_collision": true` marker on the Step view, so readers can see why the GW is absent. The session-level `counts.gw_binding.declared_collisions` integer tracks the aggregate. On observed data this is 0; non-zero values indicate a real defect in the upstream DMO writers. ### 2.6 Moment placement `session.moments[]` lives at the root of the session, not nested under `Interaction`. Each moment carries `interaction_ids[]`, a sorted list derived from `MomentInteraction` (the junction table). Rationale: the schema supports many-to-many (one moment can span multiple interactions; one interaction can belong to multiple moments), even though observed live data is 1:N. Nesting under Interaction would duplicate rows. Moments are absent on orgs without Agent Optimization enabled; the assembler never requires their presence. ### 2.7 1:1 audit chain invariant For every `GatewayRequest` in the session, exactly one `GatewayResponse` exists (in live data, modulo in-flight calls at fetch time). `counts.audit_chain_1to1_ok` is `true` iff `len(gateway_requests) == len(gateway_responses)`. Drift is flagged in the counts block but does not crash the assembler — a caller re-fetching during an active session can still produce a partial tree. Note: this is Req↔Resp 1:1, not Req↔LLM_STEP. The typical session has more GatewayRequests than LLM_STEPs because prompt-template / plannerservice calls emit GatewayRequest rows without owning Step rows. ### 2.8 `session_shape` enum Lifted verbatim from `dc._session_manifest.json.session_shape` into `dc._session_tree.json counts.session_shape`. Six values, classified by `fetch_dc.py` after the 5-wave waterfall; rules evaluated top-to-bottom, first match wins: | Value | Rule | Added | |---|---|---| | `session_not_found` | `sessions.json` returned 0 rows (bad sid or STDM not yet materialized) | original | | `interactions_not_materialized_yet` | sessions row present, but interactions/steps/messages all 0 (fresh session — STDM hierarchy hasn't materialized yet, but gateway_requests may exist) — assembler takes the gateway-direct path; see §2.9 | | | `gateway_requests_dropped_by_stdm` | DC `gateway_requests == 0` while runtime telemetry outside Data Cloud confirms ≥1 LLM call for the session — i.e. the STDM exporter has dropped writes the platform did emit. Distinct from `planner_ran_no_gateway_logs` (which means logging disabled at the source). This skill cannot disambiguate this state by itself; the operator must check runtime telemetry to reclassify. | | | `abandoned_before_llm` | steps > 0, LLM_STEP count == 0, gw_reqs == 0 (user typed, planner didn't reach an LLM call) | original | | `planner_ran_no_gateway_logs` | LLM_STEP > 0 AND steps_with_generation_id > 0 AND gw_reqs == 0 (Trust Layer gateway logging disabled; extra guard prevents misclassification from broken generations-IN clauses) | original | | `complete` | everything else (the "normal" bucket, including partial chain-orphan sessions) | original | The enum names only the operator-actionable states. For finer shape, read `counts.gw_binding`: if `declared == 0` but `gateway_requests > 0`, the session relied entirely on prompt-template / Flow-invoked LLM calls — normal but worth noticing. `gateway_requests_dropped_by_stdm` is the only shape that this skill cannot definitively classify on its own — disambiguating from `planner_ran_no_gateway_logs` requires a runtime telemetry probe outside Data Cloud. In this skill the session is reported as `planner_ran_no_gateway_logs`; the operator with access to the platform's runtime LLM-gateway telemetry can check for a matching gateway-call event to distinguish a real STDM exporter defect from "logging genuinely disabled at the source." ### 2.9 Output: `dc._session_tree.json` schema Root shape: ```jsonc { "session": { "id": "", "_schema_version": 1, // see §2.9b "identity": { ... }, // see §2.9a "org": { "alias": "...", "instance_url": "..." }, "start_ts": "...", "end_ts": null, // null while active "end_type": "Completed | Abandoned | ...", "channel": "...", "participants": [ { "participant_id": "...", "role": "USER | AGENT", "agent_api_name": "", "agent_version": "...", "agent_type": "..." } ], "moments": [ { "moment_id": "...", "agent_api_name": "...", "agent_version": "...", "request_summary_text": "...", "response_summary_text": "...", "interaction_ids": ["..."], // [] if no bridging rows; never omitted "start_ts": "...", "end_ts": "...", "tag_associations": [ ... ] // moment-scope only } ], "interactions": [ { "id": "...", "type": "TURN | SESSION_END", "topic": "...", "trace_id": "...", // primary column or extracted from AttributeText "start_ts": "...", "end_ts": "...", "messages": [ { "message_id": "...", "type": "Input | Output", "role": "USER | AGENT", // derived via participant join "participant_id": "...", "text": "...", "content_type": "...", "modality": "...", "ts": "..." } ], "telemetry_spans": [ ... ], // usually [] "steps": [ { "id": "...", "type": "LLM_STEP | ACTION_STEP | TOPIC_STEP | TRUST_GUARDRAILS_STEP | SESSION_END", "name": "...", "start_ts": "...", "end_ts": "...", "error_text": null, // NOT_SET → null "generation": { // present iff ssot__GenerationId__c != NOT_SET "generation_id": "...", "response_id": "...", "response_text": "...", "masked_response_text": "...", "feature": "...", "quality": [ ... ], // ContentQuality rows parented on generationId__c "categories": [ ... ], // ContentCategory rows parented on generationId__c "feedback": [ { "feedback_id": "...", "feedback": "UP | DOWN", "action": "...", "details": [ ... ], "records": [ ... ] } ] }, "gateway_request": { // present only when declared chain resolves "binding_method": "declared", "gateway_request_id": "...", "feature": "...", "model": "...", "provider": "...", "prompt_template_dev_name": "...", // "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0, "prompt_text": "...", // raw input prompt "response": { ... }, "tags": [ ... ], "records": [ ... ], "metadata": [ ... ], "llm": [] }, "gateway_request_collision": true // present only when a prior step // claimed this step's declared GW; // gateway_request is null in this case } ], "timestamp_bound_gateway_calls": [ // chain-orphans placed by ts-window { "binding_method": "timestamp_window", "bound_to_step_id": "...", // null if bound to Interaction, not a Step "gateway_request_id": "...", "feature": "...", "model": "...", "response": { ... }, "tags": [...], "records": [...], "metadata": [...], "llm": [] } ], "tag_associations": [ ... ] // interaction-scope only } ], "session_tag_associations": [ ... ], // session-scope only "unbound_gateway_calls": [ // chain-orphans with no window match { "binding_method": "unbound", "gateway_request_id": "...", /* ... */ } ], "counts": { "interactions_total": 0, "interactions_turn": 0, "interactions_session_end": 0, "steps_total": 0, "steps_by_type": { "LLM_STEP": 0, "ACTION_STEP": 0, "TOPIC_STEP": 0, "TRUST_GUARDRAILS_STEP": 0, "SESSION_END": 0 }, "generations": 0, "gateway_requests": 0, "gateway_responses": 0, "gateway_metadata": 0, "gateway_llm": 0, "gateway_records_grounded": 0, "gateway_records_feedback": 0, "feedback": 0, "audit_chain_1to1_ok": true, "gw_binding": { "declared": 0, "timestamp_window": 0, "unbound": 0, "declared_collisions": 0 }, "session_shape": "complete | abandoned_before_llm | planner_ran_no_gateway_logs | gateway_requests_dropped_by_stdm | interactions_not_materialized_yet | session_not_found", "pk_collisions": [ ], // {dmo, key} records; first-write-wins applied "parse_warnings": [ ] // dc..json files that failed to parse } }, "catalog": { // session-filtered, not full org vocabulary "agents_observed": [ "..." ], "tag_definitions": [ ... ], "tag_definition_associations": [ ... ], "tags": [ ... ] }, "_doc": "Assembled from DATA_ROOT//dc.*.json. See dc._session_manifest.json." } ``` **Polymorphic dispatch:** - `GenAIGtwyObjRecord.parent__c` — if in `gw_req_by_id` → attached as `GatewayRequest.records[]` (grounded attachments); if in `feedback_by_id` → attached as `Feedback.records[]`; else silently dropped. - `GenAIContentCategory.parent__c` — if in `generations_by_id` → attached as `Generation.categories[]` (non-TOXICITY detectors); if in `quality_by_id` → nested under the Quality row as `_toxicity_subcategories[]`; else silently dropped. - `GenAIAiAgentTagAssociation` — routed by which of the three scope FKs (`SessionId`, `InteractionId`, `MomentId`) is populated. Rows with none are silently dropped. **Polymorphic dispatch failures are silent by design.** If these id spaces collide in future (they don't today — `gatewayRequestId__c`, `feedbackId__c`, `generationId__c`, and `quality.id__c` are all distinct UUID namespaces), the first-branch-wins behavior records the defect nowhere. Revisit if a real org ever shows a cross-namespace id. #### 2.9a `session.identity` sub-object Harvested from already-fetched rows. Fields that require values absent from every observed DMO row come out as `null`. Every string harvested from `tagValue__c` is piped through `html.unescape()` → quote-strip → `_clean()` to coerce `""`/`NOT_SET`/`UNSET_VALUE` to `None`. | Field | Source DMO | Source column / lookup | |---|---|---| | `org_id` | gateway_requests | `orgId__c`, first non-null after sort | | `platform_user_id` | gateway_requests | `userId__c`, first non-null after sort | | `planner_id` | gateway_requests | `plannerId__c`, first non-null after sort | | `bot_version_id` | gateway_requests | `botVersionId__c`, first non-null after sort | | `app_type` | gateway_requests | `appType__c`, first non-null after sort | | `bot_id` | gateway_request_tags | `tagValue__c` where `tag__c == "bot_id"` | | `bot_name` | gateway_request_tags | `tagValue__c` where `tag__c == "bot_name"` | | `agent_api_name` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_developer_name"` (no fallback; MyAgent has no developer-name tag — look at `bot_name` + `agent_label` instead) | | `agent_label` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_label"` | | `agent_version` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_version_api_name"`, fallback `"version_api_name"` | | `agent_type` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_type"` | | `planner_name` | gateway_request_tags | `tagValue__c` where `tag__c == "planner_name"` | | `planner_type` | gateway_request_tags | `tagValue__c` where `tag__c == "planner_type"` | | `configured_model` | gateway_request_tags | `tagValue__c` where `tag__c == "configured_model_name"` | | `messaging_session_id` | sessions[0] | `ssot__RelatedMessagingSessionId__c` | | `messaging_end_user_id` | participants | `ssot__ParticipantId__c` on the first USER-role row (sorted by `ssot__Id__c`) | | `voice_call_id` | sessions[0] | `ssot__RelatedVoiceCallId__c` | | `individual_id` | sessions[0] | `ssot__IndividualId__c` | | `bootstrap_variables` | sessions[0] | parsed from `ssot__VariableText__c` — channel-specific bootstrap key→value dict (e.g. `__resolved_locale__`, `__supports_result_display__`); empty `{}` when the column is null/empty/unparseable | | `mode` | derived | one of `production_messaging` / `builder_previewer` / `voice` / `unknown` — derived from `(channel, RelatedMessagingSessionId__c, RelatedVoiceCallId__c, bootstrap_variables.keys)`. See `_derive_mode` + `_BUILDER_PREVIEWER_INDICATOR_KEYS` in `assemble_dc.py`. | **Sorting (required for idempotence):** - `gateway_requests` — sort by `(timestamp__c, gatewayRequestId__c)` ascending, then take first non-null. - `gateway_request_tags` — sort by `(parent__c, tag__c, tagValue__c)` ascending, then filter by tag name and take first non-null. - `participants` — sort by `ssot__Id__c` ascending, then take the first USER-role row. - `sessions[0]` — by contract, one row per session; no sort needed. **Trimmed fields:** `user_location`, `user_context` are not promoted (low debugging value, opaque without a decoder ring). **Minimal tree exclusion.** The session-not-found short-circuit path (§2.11) does **not** emit an `identity` sub-object — identity harvesters need non-empty gateway rows, which are guaranteed empty in this branch. Renderer's minimal branch handles absent identity gracefully. #### 2.9b `session._schema_version` Current value: `1`. Emitted on both the full tree and the minimal session-not-found tree. Additive changes (new sub-objects, new fields) do NOT bump this. Breaking changes (rename or remove a top-level key, change the shape of `interactions[]`, etc.) bump it. Renderer refuses to render a tree with an incompatible version; see §3.10. ### 2.10 Idempotence Running the assembler N times on the same `DATA_ROOT//` produces a byte-identical `dc._session_tree.json`. Rules: - Arrays sort by source timestamp ascending; on ties, by primary-key string. - Any set-derived array (`agents_observed`, `interaction_ids[]`, etc.) is emitted via `sorted(...)` before serialization. - `json.dumps(..., sort_keys=True)` for all dict output. - Identity harvest uses the explicit sort keys in §2.9a for first-non-null picks. - Verified on the live test fixture: `diff` of two runs returns empty. ### 2.11 Session-not-found short-circuit If `sessions.json` returned 0 rows, the assembler writes a minimal tree with only `session.id`, `session.org`, `session._schema_version: 1`, and `counts.session_shape = "session_not_found"`. No `identity`, no `interactions[]`, no `catalog{}`. --- ## 3. Render stage `scripts/render_dc.py` turns `dc._session_tree.json` into a human markdown file. ### 3.1 Purpose + scope **Input:** `dc._session_tree.json` + `dc._session_manifest.json` (the latter only for the Empties diagnostics section). **Output:** `dc._session_summary.md` — 11 top-level sections, with `Session bootstrap` and `Planner LLM calls` conditionally suppressed (see §3.3). **Contract:** pure tree reader. No DMO loads. No fetches. Everything the renderer displays either lives directly on `tree.session.*` or is composed from tree fields in the renderer's presentation layer. ### 3.2 Inputs - `dc._session_tree.json` — primary input. Renderer SystemExits with a clear "tree not found" message if absent. - `dc._session_manifest.json` — consulted for the Empties diagnostics section. If absent, that section renders empty. The 24 raw `dc..json` artifacts are NOT loaded by the renderer. ### 3.3 Output: section order The full-tree branch emits these top-level sections in order. The gateway-direct branch (§2.8 `interactions_not_materialized_yet`) and the minimal session-not-found branch use abbreviated section sets; see `_render_gateway_direct` / `_render_minimal` in `render_dc.py`. 1. `## Session identity` — identity fields from `tree.session.identity` plus display-only cells (Agent, Total duration) composed in-renderer. 2. `## Session bootstrap` — channel mode + bootstrap variables (`identity.mode`, `identity.bootstrap_variables`). Surfaces these to make MIAW vs Builder Previewer distinguishable at-a-glance — `ssot__AiAgentChannelType__c` alone collides between the two. Section is suppressed (not rendered) when both `mode` and `bootstrap_variables` are absent — backwards-compat with older trees. 3. `## ID reference` — full UUIDs for everything truncated in the hierarchical trace. 4. `## Transcript` — USER ↔ AGENT narrative per TURN interaction. SESSION_END interactions have no messages and are skipped. 5. `## Complete hierarchical trace` — Interaction → Step → Generation → GatewayRequest, with `+start + duration = +end` math on Interaction and Step lines. Single timestamps on Generation and GatewayRequest (no computed latencies; see §3.6). UUIDs truncated to 8 chars + `…`; full forms in §2 above. 6. `## Per-turn summary` — one row per interaction. 7. `## Planner LLM calls (full prompts + responses)` — **opt-in via `--show-prompts`; suppressed by default**. Walks `interactions[].steps[]`, builds one `#### LLM call N — ` block per step that has a `gateway_request`. Renders the full input prompt (from `gateway_request.prompt_text`) and full response (from `generation.response_text`, HTML-unescaped) inside fenced code blocks. Per-payload display capped at 64 KB; full payloads always remain authoritative on disk in `dc.gateway_requests.json` / `dc.generations.json`. Off by default because multi-turn sessions with 30 KB+ prompts would dominate the summary. 8. `## Visual analysis` — gantt + LLM-call overlay. 9. `## Session counts` — engineer-facing table of manifest counts. 10. `## Empties diagnostics` — one row per DMO with `rows == 0` and a populated `_unavailable_reason` lifted verbatim from the manifest. 11. `## Catalog (session-filtered)` — TagDefinitions / TagDefinitionAssociations / Tags filtered to agents observed in the session. DC alone does not expose per-turn LLM latency in a useful form, so this skill emits no `## Latency rollups` section. Generation and GatewayRequest carry single-write timestamps (see §3.6); they are not start/end pairs. ### 3.4 Ellipsis rule In the `## Complete hierarchical trace`: - All UUIDs (interaction_id, step_id, generation_id, gateway_request_id) render as the first 8 characters followed by `…`. - Full UUIDs live in the `## ID reference` table above, keyed by the same 8-char prefix. This keeps the trace scannable while preserving the ability to cross-reference by ID. ### 3.5 Session-end derivation `session.end_ts` can be null for two reasons: 1. The session is still active. 2. The session ended but STDM hasn't materialized the `end_ts` column yet. Renderer display rule: if `session.end_ts` is non-null, render it directly with `✓ materialized` suffix. If null, look at the last interaction by timestamp: - If a SESSION_END interaction exists, use its `start_ts` as the derived end with `from SESSION_END interaction` suffix. - Otherwise, use the last TURN interaction's `end_ts` (or `start_ts` if its end is null) with `session still open (last TURN)` suffix. The derivation is display-only; the tree's `session.end_ts` is not modified. ### 3.6 Why we don't compute latencies on generation/gateway_request `Generation.timestamp__c` and `GatewayRequest.timestamp__c` are single timestamps representing when the DC row was written. They are NOT `start_ts`/`end_ts` pairs. Treating the delta between two of them as a "latency" is misleading — the gap reflects how DC serialized the audit, not how long the LLM call took. Interaction and Step DMOs DO expose start_ts + end_ts pairs; the renderer computes `+offset + duration = +end` math for those. ### 3.7 Empties diagnostics Walk `manifest["queries"]` for entries with `rows == 0` and a populated `_unavailable_reason`. Render each as a row: `| | | |`. The renderer does not infer reasons. Artifacts that failed to parse go into `tree.counts.parse_warnings` and surface in the `## Session counts` section, not here. ### 3.8 Idempotence Running `render_dc.py --session ` N times on the same `dc._session_tree.json` produces a byte-identical `dc._session_summary.md`. Rules: - All section builders walk tree arrays in the order the tree provides them; the tree itself is already sorted. - No timestamps of "now" embedded in the output. - No set-derived ordering; any set-to-list conversion goes through `sorted(...)`. - Verified on the live test fixture: `diff` of two runs returns empty. ### 3.9 Session-not-found short-path If the tree has `_schema_version: 1` but no `interactions[]` key, the renderer emits a minimal markdown file: ```markdown # Session ## Session identity | Field | Value | |---|---| | Session id | | | Session shape | session_not_found | _No interactions resolved in Data Cloud. Check the session id, or wait for STDM materialization._ ``` No transcript, no trace, no counts, no catalog. ### 3.10 Tree schema version check First check in `main_for_session()`: - `tree.session._schema_version == 1` → render. - `tree.session._schema_version` missing → stderr WARN, render anyway (treat as best-effort for older trees). - Any other value → `SystemExit("render_dc: unsupported tree _schema_version=; expected 1")`. Refuses rather than risking a silent schema mismatch. --- ## 4. Reading order for a new skill author 1. Read this file (contract). 2. Run the pipeline once against a fresh session: `python3 scripts/fetch_dc.py --session --org `. Inspect `DATA_ROOT//` — you get the 24 `dc..json`, the manifest, the tree, and the summary. 3. Open `dc._session_tree.json` and trace the declared chain for one LLM_STEP end-to-end. 4. Read the matching `dc._session_summary.md`. 5. Read `scripts/assemble_dc.py` (module docstring points back here), then `scripts/render_dc.py`. 6. Cross-reference [`dc_dmo_fields.md`](./dc_dmo_fields.md) when you hit a field whose origin is unclear.