agentsclimarketplace

A12 dmtool

Skill mbackschat/a12-dmtool-releases/codex/skills/a12-dmtool

Public release distribution for dmtool — the A12 Kernel document model-authoring CLI + Claude Code/Codex plugins with first-class rules support. Not an official A12/mgm artifact.

Install
npx -y skills add mbackschat/a12-dmtool-releases --skill a12-dmtool

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Author and validate A12 Kernel document models with the dmtool CLI — both a model's structure (fields, groups, type definitions, includes, config) and its validation rules. Use when a user (often a document modeller or business analyst) wants to create, edit, check, or understand an A12 document model — add or change fields and groups, factor out reusable includes, refactor structure, or author validation rules on it. Covers model creation, the structure edits and refactors (extract/move/rename), the rule envelope and error-scenario polarity, field-path references, per-row iteration, and the explore→compose→check loop.

SKILL.md

23.1 KB, as published. Nobody here has run it

Authoring A12 document models with dmtool

You help author and validate A12 document models — both their structure (fields, groups, type definitions, includes, config) and their validation rules — with the dmtool CLI. The CLI describes itself — you explore it rather than memorizing it — so this skill carries only the judgment the tool can't give you (polarity, the traps, the kernel's laws). You don't need A12 background docs: the CLI's self-description + its kernel-checked feedback are enough if you follow the rules below.

⛔ First — is this even a rule? Prefer a field/group property

A rule is the most expensive way to state a constraint: a condition + error code + per-locale messages, plus (inside a repeatable group) a GroupFilled guard. Many constraints are really properties of the field — declared once, enforced natively, no rule. Before composing any condition, check this:

The constraint is really…Declare it as a field propertyNot this rule
"X must be provided"the field's required property (a key in its spec: field add / field modify). In a repeatable group required means required per present row — it replaces a whole GroupFilled(G) And FieldNotFilled(X) ruleFieldNotFilled(X)
a number within a rangethe field's min/max[X] > max
a string in a fixed formatthe field's regexa pattern rule
a value from a fixed setan enum fielda value-list rule

dmtool patterns marks these field-level alternatives explicitly. Write a rule only for what a field property can't express: cross-field logic, conditional requiredness ("required when Channel is EXPRESS"), date ordering, cross-row aggregates. Rule of thumb — if the constraint names one field and a fixed presence/limit/format, it's a field property, not a rule.

Your tools

The CLI is self-describing — explore it:

  • dmtool --help — the verb list; every command is dmtool -m <model.json> <target> <op> (the model is set once with -m, before or after the verb); dmtool <target> <op> --help — that op's parameters and what they mean; dmtool manifest — the same, machine-readable (each verb's target/op + params).
  • dmtool operators — the DSL operator catalog (each operator's meaning, operands, examples, and gotchas — read the gotchas, they often name the exact fix for a rejection); dmtool operators <id> — one in full. The list's header serves the per-kind emptyOperandDefaults (how an empty field behaves in a comparison), and an entry's semantics block states its kernel-verified facets — empty-operand behavior, strict-vs-inclusive boundaries, what an all-empty aggregate folds to. Check there instead of assuming.
  • dmtool schema <target> <op> — the JSON an op consumes/returns (and dmtool schema result — the universal output envelope every verb emits).

The three you lean on:

  • dmtool -m <model.json> model describe — orient: fields, kinds, enum values, groups, repeatability.
  • dmtool operators — pick operators by meaning.
  • dmtool -m <model.json> rule check --field <ABSOLUTE field path> --condition "<DSL>" --code <ID> — submit a candidate and get the real kernel's verdict (the result envelope's valid + diagnostics). This is your ground truth. (dmtool -m <model.json> model check statically re-checks a model's existing rules + computations.)

⛔ Inspect and edit a model ONLY through dmtool's structured verbs — never read or hand-edit the raw .dm.json, and don't fall back to export to inspect, either. To see a model: model describe (structure + kinds + enum values) and field read / rule read (one element in full — field read echoes the field's metadata and its per-kind config: number min/max/unit/scale, string pattern/length/patternMessage, enum values, date/time formats — so you can confirm a constraint actually persisted). To change it: the edit verbs. export is NOT an inspection tool — it dumps the model's raw DM-JSON for emitting it (saving, handing to A12 Tools); eyeballing an export dump to understand a model is the same anti-pattern as cat-ing the file, because you're reading the kernel's internal format that describe/read exist to abstract. The structured reads already show everything you'd look for — if one seems to omit something you need, that's a bug report, not a cat or an export.

Batch the edits you already know into one apply/batch call. Binary startup is sub-100 ms (no warmup to amortize — so don't batch for that), but every call is a round-trip you pay for: reading the output and reasoning before the next one. So once you know several edits — fields, rules, an include mounted twice — land them in a single apply/batch (also atomic: all-or-nothing, rolled back on any failure) rather than one verb at a time. Keep single calls for exploring and checking (describe/operators/rule check), where you genuinely need each result before deciding the next.

Going bilingual is the classic case to batch. Adding a second locale (config modify --add-locale de_DE) makes the kernel require, in the new locale: every enum value's labels (else MVK_INTERNAL_VALUES_AND_DISPLAY_VALUES"display value and XML value not specified together"), every label you set on a new field, a message on every existing rule/computation — not just the ones you're editing (a rule left with only its old-locale message → MVK_ERROR_MESSAGE_FOR_LANGUAGE_MISSING), and every patterned string field's pattern message (a pattern field's patternMessage left in only the old locale hits the same MVK_ERROR_MESSAGE_FOR_LANGUAGE_MISSING — the per-locale form is the patternMessages map). The --add-locale itself is rejected until all of these are present (so it's all-or-nothing). Scope it precisely: run config modify --add-locale <loc> --dry-run first — it lists the exact per-element gaps this model actually has (which rules/enums/patterned fields are missing the new locale), so you top up only those instead of guessing from the list above (the enum-label and pattern-message items apply only if the model has them). Pre-existing single-locale field labels are the one exception (tolerated — labels, unlike the error texts above, aren't required per-locale), so the requirement is asymmetric and surprising. Do the --add-locale and the per-enum-value label top-ups, any new fields' bilingual labels, the new-locale message on every existing rule (rule read with no arg lists them), and a field modify re-supplying patternMessages for the new locale on every patterned string field in one apply (you can't set a new-locale message before --add-locale declares it, so they must ride the same atomic apply), so the single terminal gate sees a consistent model instead of rejecting a half-bilingual one.

Naming model files — match the basename to the model id. A12 Tools requires a model's file basename to equal its model name (the header id), and it does not strip the suffix — so the suffix is part of the id, not just a file decoration. When you create a model, give --id and -o the same stem: model new --id Order_DM -o Order_DM.json. The A12 Tools convention is <Name>_DM for a document model, <Name>_TDM for a type-definition model (some repos use a lowercase .dm.json with a bare id — match whatever the workspace already uses). The trap the importer flags: --id order written to Order_DM.json (id ≠ basename). dmtool itself resolves references by id, never by filename — but the A12 modeler gates basename == id, so keep them equal. When you group extract, the new sub-model is written as <reference>.dm.json in the include-dir — take its exact path from the result's subModel field; a guessed <reference>.json won't load.

If dmtool is command not found: the plugin bundles an installer next to this skill — run it to download the binary on demand: bash "${CLAUDE_SKILL_DIR}/ensure-dmtool.sh" (that variable is this skill's own directory on Claude Code; on Codex run the ensure-dmtool.sh that sits beside this SKILL.md). It fetches the per-OS native build, checksum-verifies it, and prints a line like dmtool ready: <absolute path>. Use that printed path to invoke dmtool for the rest of the session — a script you run mid-session can't reliably add it to PATH. Don't build it from source or fetch it any other way. (Where dmtool already runs, none of this applies — just use it.)

The loop

  1. Orientdmtool -m <model> model describe to learn the fields, their kinds, enum values, and which groups repeat (the structured view; not export, which only dumps the raw model).
  2. Pick operators — from dmtool operators, by meaning.
  3. Compose the condition — minding polarity, paths, and iteration below.
  4. Checkdmtool -m <model> rule check …. If valid:true, done. If not, read each diagnostic.
  5. Iterate — the diagnostic code+summary name the problem; look the operator up in the catalog for the fix; adjust and re-check.

⚠️ Polarity — the single most important thing

A rule's condition is TRUE when the document is INVALID. It describes the error scenario (the violation), not the requirement. There is no Not operator — instead, pick the negative-form predicate.

So to enforce a requirement, write its violation:

Requirement✅ condition (the violation)❌ common mistake (the opposite rule)
"X must be provided"FieldNotFilled(X)FieldFilled(X)
"amount must be ≤ 1000"[X] > 1000[X] <= 1000
"at least one of A/B set"NoFieldFilled(A, B)AtLeastOneFieldFilled(A, B)

The kernel accepts both polarities (both are valid conditions), so check returning valid:true does not mean your polarity is right — only that the syntax/types are. Always re-read your condition as "this is true exactly when the document is wrong."

⚠️ The first row is about polarity (a rule's condition is true on a violation) — it does not mean plain requiredness should be a rule. Unconditional "X is required" is the field's required property (see the gate above); reach for FieldNotFilled only for conditional requiredness or as a row-existence guard.

Field-path references

A condition is evaluated relative to the rule's group (its iteration scope — defaults to the error field's parent group).

  • Bare name = a field in the rule's own scope, else — when the name is unique across the whole model — that one field, wherever it sits (a model-config fallback, on in dmtool-created models). There is no upward search: from a rule scoped to /Subscription/Addons, [Tier] resolves to /Subscription/Tier because Tier is unique model-wide, not because it's an ancestor's field. A bare name that exists in several groups is rejected (MVK_FIELDNAME_NOT_UNIQUE) — write the parent navigation (../Tier) or the absolute path instead.
  • Absolute path for a field in a different branch (or just to be explicit): [/Customer/Status].
  • Brackets […] mark a field used as a value — a comparison operand: [Quantity] > 0, [/Customer/Status] == "ACTIVE". Anything inside a function/predicate/aggregate's parentheses is a BARE ref, never bracketed: FieldNotFilled(Quantity), Sum(Items*/Amount), DateRange(OrderDate, DeliveryDate), StartOfDateRange(CoverageWindow). Bracketing a call's argument is a parse error, not extra safety.
  • You can compare field-to-field, not just field-to-literal — bracket both: [EffectiveFee] < [BaseFee].
  • Strict vs inclusive: map the wording carefully. "lower than / below / more than / exceeds" → strict (< / >); "at least / no less than / at most / no more than" → inclusive (<= / >=). And remember the violation is the opposite of the requirement: requirement "must be at least base" (>= base, valid) → violation < base.
  • Enums compare by stored value, not the display label: == "ACTIVE", not == "Active". (model describe lists the stored values.)
  • Booleans/confirms compare to the capitalized True/False[Active] == True, not the JSON true/false (a lowercase true is a parse error, MVK_UNEXPECTED_TOKEN). A confirm field compares only to True (== True / != True); [Sig] == False is rejected (MVK_INVALID_COMPARE_TO_YES) — a confirm is checked-or-not, so test the unchecked side with != True (or FieldNotFilled).

Empty values in a comparison

How an empty (unspecified) field behaves in a comparison depends on its type — the trap: an empty number participates as 0, so [Amount] < 100 fires on an empty Amount (0 < 100). The per-type table is served by the tool (the dmtool operators header emptyOperandDefaults; per-operator deviations in each entry's semantics.emptyOperand), and rule check warns (RK_UNGUARDED_NUMBER_COMPARISON) exactly where an empty value would fire your comparison — its fix names both remedies; decide by intent, guarding with FieldFilled(…) And … when absence shouldn't trip the rule.

One corner draws no warning: there are no empty-string values (an empty string field is just unfilled), so [F] == "" is never true — test absence with FieldNotFilled(F).

Per-row iteration & the negative guard

  • Putting the error field inside a repeatable group makes the rule fire once per row ("each …"). That happens automatically — you don't ask for it; you choose the error field.
  • "Each X must …" is a per-row rule — don't recast it as one document-level count/aggregate. The error field's location is the decision: a field inside the repeatable group → the rule fires per offending row and points at that row. Rewriting (e.g.) "each line item needs a ShippedDate" as a single whole-document check over Lines*/ShippedDate (a count, or NotAllFieldsFilled(Lines*/ShippedDate) on a top-level field) is a different rule — it fires once for the whole document and flags the wrong locus. When the requirement says "each", keep the error field in the row.
  • A negative condition (FieldNotFilled, NoFieldFilled, …) inside an iterating rule is rejected (MVK_NEG_CONDITION_IN_ITERATION) unless guarded by a positive existence check on the row: GroupFilled(<the repeatable group>) And <your negative condition>.
  • Guard row existence with GroupFilled(<the repeatable group>), not FieldFilled(<some sibling field>). A sibling field can be empty while the row exists, so an arbitrary-field guard quietly changes which rows the rule covers — GroupFilled is the row-presence check.

Aggregates over a repeatable group

When a rule reasons about all the rows at once (not one row), it folds the repetitions with an aggregate, and the * wildcard is what flattens them. Such a rule is model-level (it spans rows), so its error field is normally a non-repeatable field — the rule then fires once, not per row.

  • The * goes on whatever flattens the repetitions — the field for a value aggregate (Sum(Lines*/Amount), NumberOfFilledFields(Lines*/Sku), MaxValue / MinValuenot the operand-list Min/Max, which take value expressions, never a starred path), or the group itself to count rows (NumberOfFilledGroups(Lines*)). A single repeatable group reference needs that *: NumberOfFilledGroups(Lines) without it is rejected MVK_NO_WILDCARD, and a * where the group must stay whole (GroupFilled(Lines*)) is rejected MVK_NO_WILDCARDS_ALLOWED. (Plain GroupFilled(Group) takes no * — it's the per-row existence guard from the section above, valid only from inside the iterating group, never as a model-level reference.)
  • Pick the operator by the question: total of the amounts → Sum(Lines*/Amount); how many rowsNumberOfFilledGroups(Lines*); how many filled instances of a field → NumberOfFilledFields(Lines*/Sku). Confirm names/operands with dmtool operators.
  • "No two rows share a key" → FieldValuesNotUnique(/Group*/Key) (absolute starred path; error field the key itself inside the repeated group, e.g. --field /Order/Items/Sku). It validates and persists under the default grouping and still iterates per row (points at the offending row). The sibling RepetitionNotUnique instead needs the rule at the repeated group's PARENT (--group <parent>; the default grouping rejects it) — its operators entry walks through that authoring. Reach for FieldValuesNotUnique first.
  • Resolve from the rule's scope, or go absolute. A wildcard path resolves relative to the error field's group; from a different branch a relative Lines*/Amount is MVK_INVALID_ENTITY — write the absolute /Invoice/Lines*/Amount. When unsure, go absolute.
  • An aggregate is a number, so compare it: Sum(Lines*/Amount) > 500, or against a field by bracketing it: [FeeCap] < Sum(Lines*/Amount).
  • Having filters which rows are folded: Sum(Lines*/Amount Having [Lines/Type] == "FEE") sums only the fee lines.
  • The error field must appear in the condition (any rule — kernel MVK_ERROR_FIELD_NOT_REFERENCED). A model-level aggregate's error field is not referenced by the aggregate's own path, so reference it explicitly: put the error field on the cap/limit you compare the aggregate against ([FeeCap] < Sum(...) references FeeCap), or guard with FieldFilled(<errorField>). "Put it on a non-repeatable field" is necessary but not sufficient — the field still has to be named in the condition.
  • The error field may instead sit inside the aggregated group — the message then lands on a row's field, but only the FIRST row's. It's valid (the starred path references the in-row field, satisfying the bullet above), but the default scope is then the repeatable group itself, which can't resolve a relative starred path (the MVK_INVALID_ENTITY above) — write it absolute: error field /Invoice/Lines/Amount, condition Sum(/Invoice/Lines*/Amount) > 500 (or lift the scope with --group to the repeatable group's parent /Invoice, where the path may stay relative). The scope choice does not change the runtime: an aggregate-only condition fires once either way, pinned to the first row's Amount — it never marks every row (marking each offending row is a per-row rule — see Per-row iteration above — not an aggregate). When a natural non-repeatable field exists, prefer it — one error at the locus that explains it.

Example — "the FEE-line total must not exceed the invoice's FeeCap" (repeatable /Invoice/Lines with Amount/Type; non-repeatable /Invoice/FeeCap):

dmtool -m invoice.json rule check --field /Invoice/FeeCap \
  --condition "FieldFilled(FeeCap) And [FeeCap] < Sum(Lines*/Amount Having [Lines/Type] == \"FEE\")" \
  --code FEE_OVER_CAP
# → "valid": true — FeeCap is referenced (via the comparison), so the error field appears in the condition

Dates

  • A date/time constant is German-format and quoted — date "31.12.2024" (dd.MM.yyyy), time "17:00:00". An ISO-style literal ("2024-12-31") is read as a string, so an ordering comparison is rejected as MVK_INVALID_TYPE_FOR_COMPARISON — the code name is unhelpful here, but its fix hint points the right way: write the German format, not switch to ==. (Often cleaner to skip the literal: compare to another date field or Today, or pull a part — YearFromDate(D) < 2020.)
  • An empty date operand does not suppress a date function — e.g. DifferenceInDays reads an empty operand as a 0-difference, so the comparison can fire on absence (each operator's semantics.emptyOperand facet states its behavior; the rule check guard warning covers these too). Lead with AllFieldsFilled(DateA, DateB) And … when absence shouldn't trip the rule.
  • Argument order matters — the DifferenceIn* family is directional. Take the direction from the operator's meaning and example (dmtool operators DifferenceInDays) rather than assuming it.

Custom conditions (host-delegated)

CustomCondition <Name> is an escape hatch: the named check runs in the host application's code, not in the rule language — its logic is not visible in the model. Two rules:

  • Don't guess what it decides. Reading a rule that uses one, name it as a host-delegated check ("delegates to the app-defined CreditApproved check") and stop — inventing its meaning from the name is wrong.
  • Polarity is unchanged (see above): like any condition it is part of the violation, so the rule fires (document invalid) when the whole errorCondition is true — not when it's false. CustomCondition references no field, so pair it with one to cover the error field: FieldFilled(Applicant) And CustomCondition CreditApproved.

Worked example (a different model, to show the pattern)

Requirement: "When an order's Channel is EXPRESS, each line item's DeliveryDate must be provided." Model has enum /Order/Channel (values STANDARD, EXPRESS) and a repeatable group /Order/LineItems with field DeliveryDate.

  • Error field (drives per-row iteration): /Order/LineItems/DeliveryDate.
  • Violation = the row exists and channel is EXPRESS and the date is missing — guarded because it iterates and uses a negative:
    GroupFilled(/Order/LineItems) And [/Order/Channel] == "EXPRESS" And FieldNotFilled(DeliveryDate)
    
  • Confirm:
    dmtool -m order.json rule check \
      --field /Order/LineItems/DeliveryDate \
      --condition "GroupFilled(/Order/LineItems) And [/Order/Channel] == \"EXPRESS\" And FieldNotFilled(DeliveryDate)" \
      --code EXPRESS_ITEM_NEEDS_DELIVERY_DATE
    # → the envelope reports "valid": true, "diagnostics": []
    

Apply the same shape to your own model: find the enum + the repeatable group with describe, choose the error field for the per-row scope, write the violation, guard it if it iterates with a negative, then check.

Reading a rejection

check returns diagnostics with a code and summary, and the common structural/syntax codes carry an enriched fix/explain naming the exact correction (dmtool diagnostics <code> serves the same guidance on its own). Trust the fix first; for anything operator-specific, look the operator up with dmtool operators <id> — its gotchas and examples usually say exactly what to do.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.