DMN ↔ HoloDeck YAML Mapping¶
ARCHIVED (2026-08-29). Part of the archived 036 spec — see the banner in
spec.mdand the successordocs/product-specs/040-holodeck-temporal/spec.md.Companion artifact to
spec.md/refinements.md/tasks/plan.md. Status: design input for T2 (workflow models) and T3 (table model) — illustrative, not normative until those tasks land. Produced 2026-06-10 from a design discussion on how the native YAML DSL relates to the OMG DMN standard.Note (2026-07-25): the worked examples below still use the original loan-hardship scenario. They are retained as DMN-mapping illustrations only — the anchor sample has since moved to the Targeted Compliance Framework (
spec.mdUS6). The element-by-element mappings, the DRD semantics, and thesource:/input_datarows are all current; only the domain in the examples is stale. Thedate(..)expression on line ~51 has been corrected in place.
The honest claim¶
The v1 artifacts carry DMN semantics, not the DMN interchange format:
- Standard-faithful: FEEL expression syntax (refinements §5: FEEL syntax
is the contract), hit-policy semantics (
UNIQUE/FIRST/PRIORITY), decision-table structure (input expressions, unary-test cells, output values), DRD composition (decisions requiring decisions and input data, acyclic by validation). - Not the standard artifact: no DMN 1.x XML import/export, no DMNDI (diagram geometry), no boxed expressions. Spec Out of Scope, by design.
- Pitch wording: "DMN-semantics policy-as-code" — not "DMN-compatible" and not "bring your existing DMN files" (those need the deferred XML importer; see Migration path).
Division of labor¶
| File | DMN concept it carries |
|---|---|
workflow.yaml |
The DRD (logical form): decisions, input data, information requirements |
tables/*.dmn.yaml |
One decision table each (<decision> + <decisionTable>) |
schemas/*.json (gates) |
<itemDefinition> — the type of each input-data fact |
Decision table mapping (tables/*.dmn.yaml)¶
Example — the Level-2 affordability table:
# DMN: <decision> + <decisionTable>
id: affordability
name: Hardship affordability assessment
version: "2026-06-01.1" # FR-013: required label, snapshotted into the run record
hit_policy: UNIQUE # DMN: hitPolicy — exactly one rule may match
# DMN: <input>/<inputExpression> — full FEEL expressions live HERE only.
# `income` / `residency` are workflow node ids; dot-paths reach into the
# gated object (edge node) or verdict (policy node).
inputs:
- name: surplus_ratio
expression: (income.net_monthly_income - income.monthly_expenses) / income.net_monthly_income
type: number
# CORRECTED 2026-07-25 — was `date(application_date) - date(income.statement_date)`,
# which research.md caveat 6 proved unevaluable: bkflow-feel's `date_func`
# production accepts only a quoted literal, not a variable. Date-typed gate
# fields cross the boundary as Python `datetime.date` and subtract bare;
# caveat 1 requires the wrapper to convert the resulting timedelta to days.
- name: statement_age
expression: application_date - income.statement_date
type: days
- name: residency_status
expression: residency.status
type: string
# DMN: <output>; `values` = outputValues (and the priority order under PRIORITY)
outputs:
- name: affordability
type: string
values: [affordable, marginal, unaffordable]
# DMN: <rule> — each `when` cell is a FEEL unary test, keyed by input name.
# An input omitted from `when` is the irrelevant cell (always matches), which
# is DMN's "-"; an explicit "-" is accepted and means the same thing.
# CORRECTED 2026-07-25 — this example was positional (`when: [">= 0.25", ...]`)
# until T3 landed. Open design point 1 below resolved to keyed cells, so the
# positional form no longer parses. See `models/decision_table.py::Rule`.
rules:
- when: { surplus_ratio: ">= 0.25", statement_age: "<= 90", residency_status: '"verified"' }
then: { affordability: affordable }
annotation: Comfortable surplus, fresh statements, verified residency
- when: { surplus_ratio: "[0.10..0.25)", statement_age: "<= 90", residency_status: '"verified"' }
then: { affordability: marginal }
annotation: Thin surplus — refer for officer review
- when: { surplus_ratio: "< 0.10" }
then: { affordability: unaffordable }
annotation: No realistic repayment capacity
- when: { statement_age: "> 90" }
then: { affordability: unaffordable }
annotation: Statements too stale to assess
# DMN: defaultOutputEntry — without it, no-match fails loudly (FR-012)
# default: { affordability: unaffordable }
Element-by-element:
*.dmn.yaml |
DMN element | Notes |
|---|---|---|
id, name |
<decision id name> |
|
version |
(no direct DMN equivalent) | HoloDeck addition; FR-013 snapshot key |
hit_policy |
hitPolicy |
UNIQUE multi-match errors per FR-012; PRIORITY resolves by values order (DMN's output-values-as-priority rule) |
inputs[].expression |
<inputExpression> (FEEL) |
Full FEEL allowed here only |
inputs[].type |
typeRef |
|
rules[].when[<input name>] |
<inputEntry> (FEEL unary test) |
>= 0.25, [0.10..0.25), "verified"; an omitted key (or -) is the irrelevant cell |
rules[].then |
<outputEntry> |
|
rules[].annotation |
rule annotation | |
outputs[].values |
outputValues |
Doubles as PRIORITY ordering, highest first |
default |
defaultOutputEntry |
Optional; absence ⇒ loud no-match failure |
Open design points for T3¶
- ~~Positional vs keyed
whencells.~~ RESOLVED in T3 — keyed only. Positional mirrors DMN's column layout, but keyed cells are diff-friendlier and immune to column-reorder bugs, and a reordered column silently changing every rule's meaning is exactly the class of error this spec exists to prevent.Rule.whenis adict[str, str]keyed by input name; a cell naming an undeclared input is rejected at load. Positional entries do not parse. A future transpiler (spec 039) emits keyed cells. - Expression placement rule. Full FEEL (arithmetic, date math) is valid
only in
inputs[].expression; rule cells are restricted to unary tests. Faithful to DMN and matches what the FEEL-library research verified best (bkflow-feel). Encode as a T3 validation rule.
DRD mapping (workflow.yaml)¶
The workflow file is the DRD in logical form:
name: loan-hardship-underwriting
version: "2026-06-01.1"
nodes:
# DMN <inputData> — produced by an agent; the gate schema is the
# <itemDefinition> (the type of the fact).
- id: income
edge: { agent: agents/income-extractor/agent.yaml }
gate: { schema: schemas/income.json }
- id: residency
edge: { agent: agents/residency-verifier/agent.yaml }
gate: { schema: schemas/residency.json }
- id: doc_fraud_flag
edge: { agent: agents/doc-fraud-detector/agent.yaml }
gate: { schema: schemas/fraud-flag.json }
# DMN <decision> — `inputs:` is the <informationRequirement> list.
# CORRECTED 2026-07-25 — `hit_policy:` was shown on each node until the T3
# refactor moved it onto the referenced table, where DMN puts it (one table,
# one hit policy, one source of truth). `PolicyNode` has no such field and
# forbids extras, so the earlier form no longer parses.
- id: affordability
decision: tables/affordability.dmn.yaml # the table declares UNIQUE
inputs: [income, residency] # requiredInput ×2
- id: risk_tier
decision: tables/risk.dmn.yaml # the table declares FIRST
inputs: [doc_fraud_flag, income]
- id: final_determination
decision: tables/determination.dmn.yaml # the table declares PRIORITY
inputs: [affordability, risk_tier] # requiredDecision ×2
requires_human: true
decided_by: "Hardship Officer"
draft: { agent: agents/reasons-drafter/agent.yaml }
ai_may_draft: [reasons]
| DMN DRD element | HoloDeck equivalent |
|---|---|
<decision> |
policy or human node (one table each) |
<inputData> |
edge node's gated output |
<itemDefinition> |
the gate's JSON Schema |
<informationRequirement> (requiredInput / requiredDecision) |
the inputs: list (kind implicit — resolved by what the id names) |
| Acyclic requirements graph (DMN mandate) | FR-003 load-time cycle rejection — same constraint |
<inputData> not agent-produced |
the workflow-level input_data: block (facts of record — prior state, case facts). Distinct from an edge node's gated output, which is agent-produced. |
<knowledgeSource> |
not modeled — see proposal below |
<businessKnowledgeModel> (reusable invocable logic) |
not modeled — every table is bound to exactly one node |
| DMNDI (diagram layout) | not modeled — logical DRD only; visual modeler is North Star |
Behavioral deltas from standard DRD¶
- DMN allows any decision as an evaluation entry point; v1 always evaluates the full DAG to the top. Evaluating one table in isolation is the policy test executor's job (T12).
- One
workflow.yamlholds exactly one DRD; a DMN definitions file may hold several sharing elements.
Proposal: source: annotation (knowledgeSource-lite)¶
<knowledgeSource> is DMN's record of the authority for a decision —
"this table implements §72 of the Hardship Policy." For the regulated
audience that traceability link is substance, not decoration, and it costs
one optional field:
# on a node or table
source: "Hardship Policy v4.2 §72"
- Non-executable annotation; flows into the run record and OTel span attributes.
- Recommended for T2/T3 model fields (one-line schema addition now vs. a migration later). A future "draft table from policy document" feature would populate it automatically.
Authoring routes (where tables come from)¶
- Hand-authored YAML — the only route in v1; guarded by the published JSON schema (T2), load-time FEEL validation with table/rule/cell locators (T3), and policy tests (T12).
- Imported from DMN XML — deferred but architecturally preserved: a
transpiler walks
<decisionTable>and emits exactly the shape above (decision tables only; no DRD-XML, BKM, or boxed expressions). Likely small once T3's model exists, if "bring your existing DMN" becomes a sales requirement. - AI-drafted from a policy document — now specced:
docs/product-specs/039-policy-generator(was "not in any spec yet"). Emits tables and theworkflow.yamlDRD, with TODO markers where edge nodes go; edge agents stay hand-authored. On-pattern (AI drafts, human reviews/versions/commits — same move asai_may_draft: reasons; the committed table stays deterministic), and US5 policy tests are exactly how a drafted table is verified.
036 ships only what makes a drafted table safe to run: the provenance
block (T3a) and the review gate — holodeck workflow run refuses a table with
generated_by and no reviewed_by (FR-030, SC-009). This extends "the LLM is
never the spine" to authoring time: without it, a model could be the spine by
writing the rules rather than by producing a verdict.
039 is built only after 036's MVP ships. Its golden corpus is pinned at
docs/product-specs/039-policy-generator/corpus-manifest.md.