Skip to content

@Term() and @Term_*() decorators: type constraints and round-trip correctnessΒ #1243

Description

@mon7792

Discussion πŸ—£

@Term() and @Term_*() decorators in CTO files accept arguments of type String, Number, Boolean, and TypeReference (via the Concerto decorator argument system).

The round-trip CTO β†’ vocabulary β†’ decorator command set β†’ CTO loses non-string argument types: numbers and booleans become strings, and type references become the literal string "undefined". String @Term values round-trip correctly.

Questions:

  1. What types are allowed for @Term and @Term_* (String, Number, Boolean, TypeReference) per the language design?
  2. What is the original intent behind @Term and @Term_*?
  3. Concerto decorators support DecoratorTypeReference β€” should that be supported in @Term and @Term_*, and what should it look like in vocabulary YAML?

Reproduction

Script: reproduce-term-roundtrip.js

npm install @accordproject/concerto-core @accordproject/concerto-cto @accordproject/concerto-vocabulary
node reproduce-term-roundtrip.js

The script:

  1. Parses a decorated CTO model and prints the AST.
  2. Extracts vocabulary YAML via DecoratorManager.extractVocabularies.
  3. Builds a decorator command set via VocabularyManager.generateDecoratorCommands.
  4. Applies commands to a plain model and prints regenerated CTO.
  5. Asserts that decorator command arguments preserve DecoratorString, DecoratorNumber, DecoratorBoolean, and DecoratorTypeReference types.

Result: 2 passed, 5 failed (see Test results below).

Example

CTO model (input)

namespace com.example@1.0.0

concept Dimension {
  o Integer unit
}

@Term("Products")
@Term_sortOrder(1)
@Term_featured(false)
concept Product {
  @Term("Product Name")
  @Term_rank(2)
  @Term_visible(true)
  o String name

  @Term(Dimension)
  @Term_sizeType(Dimension)
  o Integer size
}

AST (CTO β†’ AST)

Parsing preserves correct argument types in the AST:

{
  "$class": "concerto.metamodel@1.0.0.Models",
  "models": [
    {
      "$class": "concerto.metamodel@1.0.0.Model",
      "decorators": [],
      "namespace": "com.example@1.0.0",
      "imports": [],
      "declarations": [
        {
          "$class": "concerto.metamodel@1.0.0.ConceptDeclaration",
          "name": "Dimension",
          "isAbstract": false,
          "properties": [
            {
              "$class": "concerto.metamodel@1.0.0.IntegerProperty",
              "name": "unit",
              "isArray": false,
              "isOptional": false
            }
          ]
        },
        {
          "$class": "concerto.metamodel@1.0.0.ConceptDeclaration",
          "name": "Product",
          "isAbstract": false,
          "properties": [
            {
              "$class": "concerto.metamodel@1.0.0.StringProperty",
              "name": "name",
              "isArray": false,
              "isOptional": false,
              "decorators": [
                {
                  "$class": "concerto.metamodel@1.0.0.Decorator",
                  "name": "Term",
                  "arguments": [
                    {
                      "$class": "concerto.metamodel@1.0.0.DecoratorString",
                      "value": "Product Name"
                    }
                  ]
                },
                {
                  "$class": "concerto.metamodel@1.0.0.Decorator",
                  "name": "Term_rank",
                  "arguments": [
                    {
                      "$class": "concerto.metamodel@1.0.0.DecoratorNumber",
                      "value": 2
                    }
                  ]
                },
                {
                  "$class": "concerto.metamodel@1.0.0.Decorator",
                  "name": "Term_visible",
                  "arguments": [
                    {
                      "$class": "concerto.metamodel@1.0.0.DecoratorBoolean",
                      "value": true
                    }
                  ]
                }
              ]
            },
            {
              "$class": "concerto.metamodel@1.0.0.IntegerProperty",
              "name": "size",
              "isArray": false,
              "isOptional": false,
              "decorators": [
                {
                  "$class": "concerto.metamodel@1.0.0.Decorator",
                  "name": "Term",
                  "arguments": [
                    {
                      "$class": "concerto.metamodel@1.0.0.DecoratorTypeReference",
                      "type": {
                        "$class": "concerto.metamodel@1.0.0.TypeIdentifier",
                        "name": "Dimension"
                      },
                      "isArray": false
                    }
                  ]
                },
                {
                  "$class": "concerto.metamodel@1.0.0.Decorator",
                  "name": "Term_sizeType",
                  "arguments": [
                    {
                      "$class": "concerto.metamodel@1.0.0.DecoratorTypeReference",
                      "type": {
                        "$class": "concerto.metamodel@1.0.0.TypeIdentifier",
                        "name": "Dimension"
                      },
                      "isArray": false
                    }
                  ]
                }
              ]
            }
          ],
          "decorators": [
            {
              "$class": "concerto.metamodel@1.0.0.Decorator",
              "name": "Term",
              "arguments": [
                {
                  "$class": "concerto.metamodel@1.0.0.DecoratorString",
                  "value": "Products"
                }
              ]
            },
            {
              "$class": "concerto.metamodel@1.0.0.Decorator",
              "name": "Term_sortOrder",
              "arguments": [
                {
                  "$class": "concerto.metamodel@1.0.0.DecoratorNumber",
                  "value": 1
                }
              ]
            },
            {
              "$class": "concerto.metamodel@1.0.0.Decorator",
              "name": "Term_featured",
              "arguments": [
                {
                  "$class": "concerto.metamodel@1.0.0.DecoratorBoolean",
                  "value": false
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Vocabulary YAML (AST β†’ vocabulary)

Extracted vocabulary loses type information for non-string @Term / @Term_* arguments (undefined placeholders):

locale: en
namespace: com.example@1.0.0
declarations:
  - Product: Products
    sortOrder: 1
    featured: false
    properties:
      - name: Product Name
        rank: 2
        visible: true
      - size: undefined
        sizeType: undefined

Decorator command set (vocabulary β†’ DCS)

generateDecoratorCommands coerces all arguments to DecoratorString (numbers/booleans as JSON-ish values, type refs as "undefined"):

{
  "$class": "org.accordproject.decoratorcommands@0.4.0.DecoratorCommandSet",
  "name": "terms-en",
  "version": "1.0.0",
  "commands": [
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": "Products"
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term_sortOrder",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": 1
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term_featured",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": false
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product",
        "property": "name"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": "Product Name"
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product",
        "property": "name"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term_rank",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": 2
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product",
        "property": "name"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term_visible",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": true
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product",
        "property": "size"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": "undefined"
          }
        ]
      }
    },
    {
      "$class": "org.accordproject.decoratorcommands@0.4.0.Command",
      "type": "UPSERT",
      "target": {
        "$class": "org.accordproject.decoratorcommands@0.4.0.CommandTarget",
        "namespace": "com.example@1.0.0",
        "declaration": "Product",
        "property": "size"
      },
      "decorator": {
        "$class": "concerto.metamodel@1.0.0.Decorator",
        "name": "Term_sizeType",
        "arguments": [
          {
            "$class": "concerto.metamodel@1.0.0.DecoratorString",
            "value": "undefined"
          }
        ]
      }
    }
  ]
}

Regenerated CTO (DCS β†’ CTO)

Applying the command set to a plain model produces quoted string arguments for everything that was not a string label:

namespace com.example@1.0.0

concept Dimension {
  o Integer unit
}

@Term("Products")
@Term_sortOrder("1")
@Term_featured("false")
concept Product {
  @Term("Product Name")
  @Term_rank("2")
  @Term_visible("true")
  o String name
  @Term("undefined")
  @Term_sizeType("undefined")
  o Integer size
}

Test results

Assertions from reproduce-term-roundtrip.js on the generated decorator command set:

  PASS  @Term("Products") on Product             β†’ DecoratorString
  PASS  @Term("Product Name") on Product.name    β†’ DecoratorString
  FAIL  @Term(Dimension) on Product.size         β†’ DecoratorTypeReference
          $class            expected: concerto.metamodel@1.0.0.DecoratorTypeReference
                              actual: concerto.metamodel@1.0.0.DecoratorString
          type.name         expected: Dimension
                              actual: undefined
          type.namespace    expected: com.example@1.0.0
                              actual: undefined
  FAIL  @Term_sortOrder(1) on Product            β†’ DecoratorNumber
          $class  expected: concerto.metamodel@1.0.0.DecoratorNumber
                    actual: concerto.metamodel@1.0.0.DecoratorString
  FAIL  @Term_rank(2) on Product.name            β†’ DecoratorNumber
          $class  expected: concerto.metamodel@1.0.0.DecoratorNumber
                    actual: concerto.metamodel@1.0.0.DecoratorString
  FAIL  @Term_featured(false) on Product         β†’ DecoratorBoolean
          $class  expected: concerto.metamodel@1.0.0.DecoratorBoolean
                    actual: concerto.metamodel@1.0.0.DecoratorString
  FAIL  @Term_visible(true) on Product.name      β†’ DecoratorBoolean
          $class  expected: concerto.metamodel@1.0.0.DecoratorBoolean
                    actual: concerto.metamodel@1.0.0.DecoratorString

  2 passed, 5 failed

SCRIPT

/* eslint-disable require-jsdoc */
/**
 *
 * Setup:
 *   npm install @accordproject/concerto-core @accordproject/concerto-cto @accordproject/concerto-vocabulary
 *
 * Run:
 *   node reproduce-term-roundtrip.js
 */

const { ModelManager, DecoratorManager } = require('@accordproject/concerto-core');
const { Printer } = require('@accordproject/concerto-cto');
const { VocabularyManager } = require('@accordproject/concerto-vocabulary');

// // model with non-string @Term_* decorator arguments
const DECORATED_MODEL_CTO = `
namespace com.example@1.0.0

concept Dimension {
  o Integer unit
}

@Term("Products")
@Term_sortOrder(1)
@Term_featured(false)
concept Product {
  @Term("Product Name")
  @Term_rank(2)
  @Term_visible(true)
  o String name

  @Term(Dimension)
  @Term_sizeType(Dimension)
  o Integer size
}
`;

const PLAIN_MODEL_CTO = `
namespace com.example@1.0.0

concept Dimension {
  o Integer unit
}

concept Product {
  o String name
  o Integer size
}
`;

// CONSTANTS
const META = 'concerto.metamodel@1.0.0';

let passed = 0;
let failed = 0;

// HELPER FUNCTIONS

/**
 * Assert decorator scalar argument type/value.
 * @param {string} label test label
 * @param {object|undefined} command decorator command
 * @param {string} expectedClass expected argument class
 * @param {string|number|boolean} expectedValue expected argument value
 * @returns {void}
 */
function check(label, command, expectedClass, expectedValue) {
    if (!command) {
        console.log(`  SKIP  ${label} β€” command not found`);
        return;
    }
    const arg = command.decorator.arguments[0];
    const classOk = arg.$class === expectedClass;
    const valueOk = arg.value === expectedValue;

    if (classOk && valueOk) {
        console.log(`  PASS  ${label}`);
        passed++;
    } else {
        console.log(`  FAIL  ${label}`);
        if (!classOk) {
            console.log(`          $class  expected: ${expectedClass}`);
            console.log(`                    actual: ${arg.$class}`);
        }
        if (!valueOk) {
            console.log(`          value   expected: ${JSON.stringify(expectedValue)} (${typeof expectedValue})`);
            console.log(`                    actual: ${JSON.stringify(arg.value)} (${typeof arg.value})`);
        }
        failed++;
    }
}

/**
 * Assert decorator type-reference argument shape.
 * @param {string} label test label
 * @param {object|undefined} command decorator command
 * @param {string} expectedName expected type name
 * @param {string} expectedNamespace expected type namespace
 * @returns {void}
 */
function checkTypeReference(label, command, expectedName, expectedNamespace) {
    if (!command) {
        console.log(`  SKIP  ${label} β€” command not found`);
        return;
    }
    const arg = command.decorator.arguments[0];
    const classOk = arg.$class === `${META}.DecoratorTypeReference`;
    const typeName = arg.type && arg.type.name;
    const typeNamespace = arg.type && arg.type.namespace;
    const nameOk = typeName === expectedName;
    const namespaceOk = typeNamespace === expectedNamespace;

    if (classOk && nameOk && namespaceOk) {
        console.log(`  PASS  ${label}`);
        passed++;
    } else {
        console.log(`  FAIL  ${label}`);
        if (!classOk) {
            console.log(`          $class            expected: ${META}.DecoratorTypeReference`);
            console.log(`                              actual: ${arg.$class}`);
        }
        if (!nameOk) {
            console.log(`          type.name         expected: ${expectedName}`);
            console.log(`                              actual: ${typeName}`);
        }
        if (!namespaceOk) {
            console.log(`          type.namespace    expected: ${expectedNamespace}`);
            console.log(`                              actual: ${typeNamespace}`);
        }
        failed++;
    }
}

const find = (cmds, name, declaration, property) =>
    cmds.find(c =>
        c.decorator.name === name &&
        (!declaration || c.target.declaration === declaration) &&
        (!property || c.target.property === property)
    );

function main() {
    // 1. LOAD THE MODEL
    const mm = new ModelManager();
    mm.addCTOModel(DECORATED_MODEL_CTO);

    // 2. DISPLAY THE AST
    const ast = mm.getAst();
    console.log('=== AST (CTO β†’ AST) ===');
    console.log(JSON.stringify(ast, null, 2));
    console.log('=== ===');

    // 3. EXTRACT THE VOCABULARIES
    const { vocabularies: vocab } = DecoratorManager.extractVocabularies(mm, { locale: 'en' });

    // 4. DISPLAY THE VOCABULARIES
    console.log('\n=== Vocabulary YAML ===');
    console.log(JSON.stringify(vocab, null, 2));
    console.log('\n=== Vocabulary YAML ===');

    // 5. EXTRACT THE DECORATOR COMMANDS SET
    const vocabManager = new VocabularyManager();
    vocabManager.addVocabulary(vocab[0]);
    const decoratorCommandSet = vocabManager.generateDecoratorCommands(mm, 'en');

    // 6. DISPLAY THE DECORATOR COMMANDS SET
    console.log('\n=== Decorator Command Set ===');
    console.log(JSON.stringify(decoratorCommandSet, null, 2));
    console.log('\n=== Decorator Command Set ===');


    // 7. APPLY THE DECORATOR COMMANDS SET TO THE PLAIN MODEL and display the CTO
    const pmm = new ModelManager();
    pmm.addCTOModel(PLAIN_MODEL_CTO);
    const pmmWithDecorators = DecoratorManager.decorateModels(pmm, decoratorCommandSet);
    const pmmAst = pmmWithDecorators.getModelFile('com.example@1.0.0').getAst();
    console.log('\n=== Plain Model with Decorators ===');
    console.log(Printer.toCTO(pmmAst));
    console.log('\n=== Plain Model with Decorators ===');

    const cmds = decoratorCommandSet.commands;

    console.log('\n=== TEST RESULTS ===');

    // 8. CHECKS
    // Primary @Term label β€” should always be DecoratorString
    check(
        '@Term("Products") on Product             β†’ DecoratorString',
        find(cmds, 'Term', 'Product'),
        `${META}.DecoratorString`, 'Products'
    );
    check(
        '@Term("Product Name") on Product.name    β†’ DecoratorString',
        find(cmds, 'Term', 'Product', 'name'),
        `${META}.DecoratorString`, 'Product Name'
    );
    checkTypeReference(
        '@Term(Dimension) on Product.size         β†’ DecoratorTypeReference',
        find(cmds, 'Term', 'Product', 'size'),
        'Dimension',
        'com.example@1.0.0'
    );

    // @Term_* with integer value β€” should be DecoratorNumber
    check(
        '@Term_sortOrder(1) on Product            β†’ DecoratorNumber',
        find(cmds, 'Term_sortOrder', 'Product'),
        `${META}.DecoratorNumber`, 1
    );
    check(
        '@Term_rank(2) on Product.name            β†’ DecoratorNumber',
        find(cmds, 'Term_rank', 'Product', 'name'),
        `${META}.DecoratorNumber`, 2
    );

    // @Term_* with boolean value β€” should be DecoratorBoolean
    check(
        '@Term_featured(false) on Product         β†’ DecoratorBoolean',
        find(cmds, 'Term_featured', 'Product'),
        `${META}.DecoratorBoolean`, false
    );
    check(
        '@Term_visible(true) on Product.name      β†’ DecoratorBoolean',
        find(cmds, 'Term_visible', 'Product', 'name'),
        `${META}.DecoratorBoolean`, true
    );

    console.log('\n=== TEST RESULTS ===');
    console.log('\n=== TEST SUMMARY ===');

    console.log(`\n  ${passed} passed, ${failed} failed`);

    console.log('\n=== TEST SUMMARY ===');
}

main();

Summary

Stage String @Term Number @Term_* Boolean @Term_* Type ref @Term / @Term_*
CTO β†’ AST OK OK OK OK
AST β†’ vocabulary OK OK (numeric YAML) OK (boolean YAML) Lost (undefined)
vocabulary β†’ DCS OK Coerced to DecoratorString Coerced to DecoratorString Coerced to "undefined" string
DCS β†’ CTO OK Quoted strings Quoted strings @Term("undefined")

Parsing (CTO β†’ AST) is correct; the breakage is in vocabulary extraction and decorator command generation (and therefore round-trip back to CTO).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions