Skip to content

Interface: FeatureDescriptorDto

@kortexya/reasoninglayer


@kortexya/reasoninglayer / FeatureDescriptorDto

Defined in: src/types/sorts.ts:88

Descriptor for a named feature within a sort definition.

Defines the feature name, expected sort (type), whether the feature is required, optional constraints, and a type hint for LLM extraction.

This is used in arrays (not Records). The name field identifies the feature.

optional annotations?: Record<string, string>

Defined in: src/types/sorts.ts:267

Custom OWL annotations on this feature — isIdentifier, unit, enumValues, and whatever else the tenant’s model needs.

The keys are DATA, so they survive the request body’s snake_case transform untouched. Measured: a create carrying annotations: { isIdentifier: 'true', unit: 'none' } reads back "annotations": {"isIdentifier": "true", "unit": "none"} — the camelCase key intact.

key: true is stored as the unique annotation, so this map echoes it.


optional cardinalityOrigin?: CardinalityOriginDto | null

Defined in: src/types/sorts.ts:253

Where the bounds above came from, and therefore whether a write may be refused for breaking them.

A bound sent without this field is declared: measured, a create carrying minCount: 2, maxCount: 3 and no origin reads back "cardinality_origin": "declared", and the write path then enforces it.

inferred records a bound read out of an extracted axiom. It is carried, exported and diffed — and it refuses nothing: a term that breaks an inferred bound is written. The derivation of required is off too: an inferred minCount: 2 reads back "required": false.

Sending it back unchanged is the point of the field. The ordinary way to amend a sort is read → edit → create, and without the origin beside the bound that round trip would promote every bound the extraction pipeline inferred into a declared one, and start refusing the ingestion that wrote it.

An ordinary caller may send inferred only as that unchanged echo: SortsClient.createSort and SortsClient.bulkCreateSorts accept it when the tenant already stores an inferred bound for the feature, on this sort or an ancestor, with the same minCount and maxCount, and the sort’s own record declares no bound for that feature. Anything else labelled inferred — a new bound, a changed one, or a declared bound relabelled — is a declaration, and is refused: 400 from createSort, an unsatisfiable_declaration entry from bulkCreateSorts, with a message that starts sort 'S': feature 'F':. An ancestor’s inferred bound does not let a sort relabel its own declared bound. A new sort therefore cannot carry an inferred bound; to move an extracted sort, use SortsClient.reparentSort rather than a delete and re-create.

A word outside the two is refused with a 422 naming both.


optional constraint?: ConstraintDto | null

Defined in: src/types/sorts.ts:116

Optional constraint on the feature’s value.


optional constraintMessage?: string | null

Defined in: src/types/sorts.ts:318

What a writer whose value constraint refuses is told (Pick small, medium or large).

A refused write reports it as NamedConstraintViolationError.constraintMessage; a SHACL export emits sh:message.


optional constraintName?: string | null

Defined in: src/types/sorts.ts:308

A stable name for constraint (pizza_size_is_known).

A write the constraint refuses reports it as NamedConstraintViolationError.constraintName; a SHACL export emits it as sh:name.


optional description?: string | null

Defined in: src/types/sorts.ts:283

Human-readable prose about the feature, used for semantic embeddings and documentation.

A read answers it whenever the stored declaration carries one, so the ordinary read → edit → create amendment sends it back unchanged.

On an amendment — SortsClient.createSort for a sort that already exists, and SortsClient.bulkCreateSorts — an absent or blank description KEEPS the stored one, and a non-blank value replaces it. No value clears it through this field. A description-only amendment is applied: the declaration counts as changed. To clear it, use SortsClient.patchSortFeature with description: null.


optional expectedSort?: string | null

Defined in: src/types/sorts.ts:92

Expected sort (type) for this feature’s value.


optional expectedSortName?: string | null

Defined in: src/types/sorts.ts:152

The same target, named instead of identified — how an object relation is declared when the target sort’s id does not exist yet.

Resolved only by POST /api/v1/sorts/bulk (SortsClient.bulkCreateSorts), which mints its ids server-side: a relation between two sorts of the SAME batch has no id its caller could write into FeatureDescriptorDto.expectedSort. The name is matched against the batch’s own name → id map and the tenant’s existing sorts.

Measured — one bulk request declaring Invoice.issued_to with expectedSortName: 'Client' alongside a Client definition reads back "expected_sort": "7e764fb0-…", the id of the Client the same batch created. A name that resolves to nothing is reported in the response’s errors and the feature is left unbound; it is never silently dropped.

expectedSort wins when both are given.


optional expectedTypeHint?: string | null

Defined in: src/types/sorts.ts:94

Primitive type hint for LLM extraction (e.g., “DateTime”, “Integer”, “String”, “Real”).


optional index?: IndexDirectiveDto | null

Defined in: src/types/sorts.ts:299

How the feature takes part in the retrieval indexes.

Absent means the declaration is silent about indexing, which is NOT the same as not_indexed. A read answers the directive whenever the stored declaration states one.

On an amendment — SortsClient.createSort for a sort that already exists, and SortsClient.bulkCreateSorts — an absent index KEEPS the stored directive, and a value replaces it. not_indexed is a directive, not a clear: no value returns a stated directive to silence through this field. To return it to silence, use SortsClient.patchSortFeature with index: null.


optional key?: boolean

Defined in: src/types/sorts.ts:131

Declare this feature @key / @unique — an extensional functional dependency over the sort’s subsort-closed extent.

A write carrying a key value an existing term already holds is that term. An identical duplicate coreferences into it (createTerm answers 200 with the existing term); a conflicting one is refused with @key clash … (⊥).

This is what gives an application’s model identity. Without it a second createTerm for the same customer makes a second customer, and every later join is ambiguous.


optional maxCount?: number | null

Defined in: src/types/sorts.ts:216

The greatest number of values this feature may hold.

maxCount binds under BOTH world modes, unlike FeatureDescriptorDto.minCount. Measured on a sort declaring lines with minCount: 2, maxCount: 3, writing four values is refused identically on an open and on a closed sort: 422 Constraint violation: Feature 'lines' holds 4 values, above its declared maximum of 3. Over-supply is a contradiction whatever the world assumption; under-supply is only missing information.

maxCount: 0 is accepted and reads back { min_count: 0, max_count: 0 } — a feature declared to hold no value.

Omitting it is the unbounded maximum; the OSFQL spelling of the pair is @card(min) / @card(min, max), with * for unbounded.

⚠️ Enforcement is gated on FeatureDescriptorDto.cardinalityOrigin — an inferred bound refuses nothing. See that field.


optional minCount?: number | null

Defined in: src/types/sorts.ts:193

The least number of values this feature must hold — the bound required cannot express.

minCount binds only under a closed world. Measured against the engine on a sort declaring lines with minCount: 2, maxCount: 3:

values written worldMode: 'open' worldMode: 'closed'
0 201 created 422 required feature 'lines' is missing
1 201 created 422 feature 'lines' holds 1 values, below its declared minimum of 2
2 201 created 201 created

On an open sort a short or absent feature RESIDUATES — it is unconstrained, not wrong — so a schema that means its minimum must also say CreateSortRequest.worldMode closed. FeatureDescriptorDto.maxCount needs no such help.

A non-zero minCount IS FeatureDescriptorDto.required. The engine keeps the two consistent in one direction only: measured, { minCount: 2, required: false } reads back "required": true, and { minCount: 2 } with no required also reads back "required": true. The reverse does not hold — { required: true } alone reads back with NO minCount at all. And { required: true, minCount: 0 } is refused with a 400: “a feature that may hold no value at all is NOT required”. OSFQL @card(0, n) required is the same contradiction, and is a parse error on execute and preview.

A minCount above FeatureDescriptorDto.maxCount is refused with a 400 — “declares min_count 5 above max_count 2, so no term could ever satisfy it” — rather than stored unsatisfiable.

A read answers 0 here whenever only maxCount was declared: measured, { maxCount: 3 } reads back { min_count: 0, max_count: 3 }.

⚠️ Enforcement is gated on FeatureDescriptorDto.cardinalityOrigin — an inferred bound refuses nothing. See that field.


name: string

Defined in: src/types/sorts.ts:90

Feature name.


required: boolean

Defined in: src/types/sorts.ts:114

Whether this feature must be present on instances of the sort.

Enforced only when the sort’s CreateSortRequest.worldMode is closed. On an open sort a missing feature residuates — it is unconstrained, not absent — so required: true there constrains nothing.

This is the same statement as minCount >= 1 at a coarser resolution, and the engine derives it: measured, a descriptor sent with { minCount: 2, required: false } reads back "required": true. The implication runs one way only — required: true alone stores no minCount — and the contradictory pair { required: true, minCount: 0 } is refused with a 400. The OSFQL spelling of that pair, @card(0, n) required or required @card(0), is refused too: OsfqlClient.execute and OsfqlClient.preview throw an OsfqlError with errorType: 'parse' and its line and column, rather than storing a minimum of 1. See FeatureDescriptorDto.minCount.