Ting IDE

A short offline guide · optional desktop editor and compiler

Getting started

Every tab edits a section of the same .ting source. Save writes that one file. Compile creates a separate .tingir program beside it.

  1. Click New. The starter already contains a fact type, an output type, a rule and a passing test.
  2. In General, give the module a name. Keep Program for a normal reasoning program.
  3. Visit Data, Rules and Tests to edit the example.
  4. Click Save and choose a location. Click Compile.
  5. Read Build feedback. Double-click a located error to visit its source.

Knowledge and Decisions are optional. Leave them empty until you need them. Each editing box omits its outer section heading and removes the outer two-space indentation. The IDE restores both when it saves.

General — identify your program

ControlWhat to enter
Module nameA stable name such as my_program or equipment.inspection. Use ASCII letters, digits and underscores; identifiers cannot start with a digit.
VersionThree numbers, such as 1.0.0. This is your module’s version.
Build asProgram for a complete reasoning plan; Library for reusable exported declarations; Tests only for a module that can be checked and tested but cannot be compiled for deployment.
LibrariesMap imported module names to source files and edit imports or exports. Most single-file programs do not need this.

The summary counts the document’s declarations. It is not a validation result. Use Full source to add documentation: paragraphs, comments or explicit language features.

Data — define the shapes of facts and results

Choose Fact types, Output types or Classifications in the drop-down. Define each type once. Every field is required and its value must have the declared type.

Example for the Fact types box:

schema Item:
  name: text required
  ready: boolean required

Example for the Output types box:

conclusion ReadyItem:
  name: text required

Supported scalar names are text, integer, float, boolean, bytes and null. A union can be written text | null required. Array syntax requires requires feature arrays in Full source; consult the bundled language manual.

Classifications are optional. For an existing Item fact type, class Ready applies_to Item declares a classification. Membership must be established explicitly; an Item is not automatically Ready. The IDE enables the classifications feature when this box gains a declaration.

Entities and strong references

Use an entity when several facts need to identify the same object while its other properties change. An EntitySchema declares an ordered key. EntityRef contains the schema’s qualified name and its key values. A ReferenceField requires an existing entity of its declared target schema when facts are published.

Declare, resolve and update a linked object

This complete example creates an asset and its sensor, rejects a broken link, then changes the asset identity and redirects the sensor atomically. The retained snapshot continues to resolve the original asset.

from ting_reasoner import (
    Engine, EntitySchema, FactSchema, Field, KnowledgeBase,
    MemoryPolicy, ReferenceField, SchemaError,
)

Asset = EntitySchema("equipment.Asset", {"serial": str, "location": str},
                     key=("serial",))
Sensor = FactSchema("equipment.Sensor", (
    Field("name", str), ReferenceField("asset", "equipment.Asset"),
))
engine = Engine(KnowledgeBase([Asset, Sensor]),
                memory=MemoryPolicy(max_entity_index_entries=8))

# The sensor may come first: the complete declaration is validated together.
sensor, asset = engine.declare(
    Sensor(name="temperature", asset=Asset.ref("pump-a")),
    Asset(serial="pump-a", location="north"),
)
saved = engine.snapshot()
assert engine.resolve(Asset.ref("pump-a"))["location"] == "north"
assert engine.referrers(Asset.ref("pump-a"))[0].handle == sensor

try:
    engine.retract(asset)
except SchemaError:
    pass  # The sensor still requires this target; neither fact changed.
else:
    raise AssertionError("a referenced entity must not be deleted")

# Rename explicitly, redirecting the incoming edge in the same transaction.
with engine.transaction() as transaction:
    transaction.retract(asset)
    transaction.declare(Asset(serial="pump-b", location="south"))
    transaction.replace(sensor, Sensor(name="temperature", asset=Asset.ref("pump-b")))

print(engine.resolve(Asset.ref("pump-b"))["location"])
print(saved.resolve(Asset.ref("pump-a"))["location"])
# Output: south, then north, on separate lines.
report = engine.entity_index_report
assert (report.entities, report.references) == (1, 1)
assert report.peak_reserved_entries == 4

Choose keys and reference fields

Keys contain one or more required fields, each with one exact int, str or bytes type. Key order matters. Booleans, floats, nulls, unions, arrays and other references cannot be key fields. For a composite key, write EntitySchema("Part", {"serial": str, "revision": int}, key=("serial", "revision")) and construct its reference with Part.ref("pump-a", 2). FactSchema retains ordinary scalar fields, including fields named key or entity_key.

Constructing a reference checks its portable value shape; EntitySchema.ref() also checks key arity and types. Publication checks the reference against the compiled schema and complete resulting fact set. Different live facts cannot share an entity key. Declaring the identical fact again reuses its existing handle. References contain no handles or physical row addresses, so compaction and process transfer preserve their identity.

Transactions, cycles and snapshots

A multi-fact declare(), transaction commit, reset or single rule firing validates its complete resulting graph before publication. Forward references and cycles are allowed when every target exists. Deleting a target requires removing or redirecting every surviving incoming reference in the same atomic operation. Replacing an entity within its schema preserves its key; rename with explicit retraction, declaration and redirection. A failed operation preserves committed facts, outputs and explanations. Earlier successful rule firings remain committed if a later firing fails.

engine.resolve(ref) returns the current immutable target fact. A well-typed key without a target raises KeyError; an unknown target schema or wrong key shape raises SchemaError. engine.referrers(ref) returns EntityLink records in source-fact publication order, then field order. Each link provides its source fact, field and explanation handle. saved.resolve(ref) and saved.referrers(ref) query only that saved snapshot. Reintroducing a key denotes the same logical identity in the new snapshot; no hidden generation is added.

Author, test and compile reference-aware rules

Save the following source as entity_equipment.ting. The explicit requires feature entity-references declaration enables key(...), reference Target required and ref Target [keys]. Names in source are local or explicitly imported; compiled reference targets use their qualified schema names. For a bytes key, use a lowercase hexadecimal object such as ref Part [{"bytes":"00ff"},2], in the declared key order.

The two fixed assets form a valid cycle. The rule joins a request to all matching alarms by exact reference identity. Both tests explicitly include the fixed knowledge using given seed.

ting source 1
requires semantics 1
module entity_equipment version "1.0.0" role program
requires feature entity-references
requires feature seed-facts

schemas:
  schema Asset key(serial):
    serial: text required
    backup: reference Asset required
    location: text required
  schema Request:
    asset: reference Asset required
  schema Alarm:
    asset: reference Asset required
    code: text required

conclusions:
  conclusion SelectedAlarm:
    asset: reference Asset required
    code: text required

knowledge:
  seed pump_a: Asset:
    serial: "pump-a"
    backup: ref Asset ["pump-b"]
    location: "north"
  seed pump_b: Asset:
    serial: "pump-b"
    backup: ref Asset ["pump-a"]
    location: "south"

rules:
  rule select_alarm:
    when:
      match Request:
        asset: bind selected
      match Alarm:
        asset: eq use selected
        code: bind code
    then:
      conclude SelectedAlarm:
        asset: use selected
        code: use code

tests:
  test selected_reference:
    given seed
    given:
      fact Request:
        asset: ref Asset ["pump-a"]
      fact Alarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
      fact Alarm:
        asset: ref Asset ["pump-b"]
        code: "pressure"
    expect:
      conclusion SelectedAlarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
    firings: 1
  test all_alternative_matches:
    given seed
    given:
      fact Request:
        asset: ref Asset ["pump-a"]
      fact Alarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
      fact Alarm:
        asset: ref Asset ["pump-a"]
        code: "pressure"
    expect:
      conclusion SelectedAlarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
      conclusion SelectedAlarm:
        asset: ref Asset ["pump-a"]
        code: "pressure"
    firings: 2

In the IDE, paste the complete example into Full source, save, then use Check, Tests and Compile. Data, Knowledge, Rules and Tests retain their usual categories. Editing entity declarations in a section enables the feature declaration automatically. The editor highlights key, reference and ref; Tests feedback reports entity and edge counts. From a terminal, run ting-build compile entity_equipment.ting -o entity_equipment.tingir. Use this script beside the compiled file; execution does not require the source.

from ting_reasoner import Engine, EntityRef, load_tingir

engine = Engine(load_tingir("entity_equipment.tingir"))
Request = engine.schema("entity_equipment.Request")
Alarm = engine.schema("entity_equipment.Alarm")
selected = EntityRef("entity_equipment.Asset", ("pump-a",))
engine.declare(Request(asset=selected), Alarm(asset=selected, code="temperature"))
engine.run()
result = engine.conclusions[0]
print(result["code"], engine.resolve(result["asset"])["location"])
# Output: temperature north
assert len(engine.explain(result)[0].fact_ids) == 2
assert engine.resolve(selected)["backup"] == EntityRef("entity_equipment.Asset", ("pump-b",))

Understand matching and evidence

Reference fields support binding, equality and inequality; the Python constraint API also supports membership with one_of(). Source reference literals use bind, eq or ne constraints. References have no less-than ordering. Binding references or using literal references in derived facts preserves their type; all targets must exist after that firing. Rule explanations record the facts explicitly matched by the rule. Reading a target with Python resolve() does not add a rule premise or an implicit target-property dependency. To reason from target properties, use an explicit follow condition or match the target and its key in the rule. Reference literals accept complete constant keys. For rule bindings, use the explicit action construction described below. Implicit dereference expressions are unsupported.

Follow a reference in a rule

Use EntitySchema.follow(use("binding"), ...) when a rule needs properties of an entity identified by an earlier fact. The reference binding must already be established by an earlier positive match and must target that exact schema. follow matches the complete ordered key, then applies the supplied non-key filters and bindings. Key fields are fixed by the reference; attempts to override or bind them in the same call are rejected. A Python EntityRef constant may also be passed directly.

This complete Python example explains an asset’s location using both the request and the asset itself. Changing the asset invalidates its previous conclusion; a retained snapshot continues to contain the earlier location.

from ting_reasoner import (
    Engine, EntitySchema, FactSchema, KnowledgeBase, ReferenceField,
    Rule, bind, emit, use,
)

Asset = EntitySchema("Asset", {"serial": str, "location": str}, key=("serial",))
Request = FactSchema("Request", [ReferenceField("asset", "Asset")])
locate = Rule("locate", (
    Request.pattern(asset=bind("selected")),
    Asset.follow(use("selected"), location=bind("place")),
), (emit("location", place=use("place")),))
engine = Engine(KnowledgeBase((Asset, Request), (locate,)))
asset, request = engine.declare(
    Asset(serial="pump-a", location="north"),
    Request(asset=Asset.ref("pump-a")),
)
engine.run()
result = engine.conclusions[0]
print(result["place"])
assert engine.explain(result)[0].fact_ids == (request.id, asset.id)
saved = engine.snapshot()

# The target is a real premise: changing it withdraws the old answer.
engine.replace(asset, Asset(serial="pump-a", location="south"))
engine.run()
assert len(engine.conclusions) == 1
print(engine.conclusions[0]["place"])
print(saved.resolve(Asset.ref("pump-a"))["location"])
# Output: north, south, north, on separate lines.

Save the next example as entity_traversal.ting. Declare both entity features. follow Asset via use selected: identifies the requested asset, and a second follow identifies its backup. The graph contains a cycle, but this rule performs exactly the two written matches. There is no implicit recursive walk. The two embedded tests exercise both directions.

ting source 1
requires semantics 1
module entity_traversal version "1.0.0" role program
requires feature entity-references
requires feature entity-traversal
requires feature seed-facts

schemas:
  schema Asset key(serial):
    serial: text required
    backup: reference Asset required
    location: text required
  schema Request:
    asset: reference Asset required

conclusions:
  conclusion BackupLocation:
    requested: reference Asset required
    location: text required

knowledge:
  seed pump_a: Asset:
    serial: "pump-a"
    backup: ref Asset ["pump-b"]
    location: "north"
  seed pump_b: Asset:
    serial: "pump-b"
    backup: ref Asset ["pump-a"]
    location: "south"

rules:
  rule locate_backup:
    when:
      match Request:
        asset: bind selected
      follow Asset via use selected:
        backup: bind backup
      follow Asset via use backup:
        location: bind location
    then:
      conclude BackupLocation:
        requested: use selected
        location: use location

tests:
  test first_backup:
    given seed
    given:
      fact Request:
        asset: ref Asset ["pump-a"]
    expect:
      conclusion BackupLocation:
        requested: ref Asset ["pump-a"]
        location: "south"
    firings: 1
  test second_backup:
    given seed
    given:
      fact Request:
        asset: ref Asset ["pump-b"]
    expect:
      conclusion BackupLocation:
        requested: ref Asset ["pump-b"]
        location: "north"
    firings: 1

In the IDE, use Full source for the complete example, then Check, Tests and Compile. Editing a follow statement in the Rules section adds both feature declarations automatically. The editor highlights follow and via. From a terminal run ting-build compile entity_traversal.ting -o entity_traversal.tingir. Place this script beside that compiled artifact; the readable source is no longer required.

from ting_reasoner import Engine, EntityRef, load_tingir

engine = Engine(load_tingir("entity_traversal.tingir"))
Request = engine.schema("entity_traversal.Request")
engine.declare(Request(asset=EntityRef("entity_traversal.Asset", ("pump-a",))))
engine.run()
result = engine.conclusions[0]
assert len(engine.explain(result)[0].fact_ids) == 3
print(result["location"])
# Output: south. The request, requested asset and backup asset are premises.

A positive follow contributes its target fact to the rule’s ordinary support and source explanation. Changing or retracting a supporting input uses the existing invalidation and maintenance rules. The ordinary equality indexes narrow candidate targets, including compound keys; all key components are checked with exact types. Candidate rows, constraints, activations and firings use the existing ExecutionBudget counters. An exhausted budget publishes no partially fired conclusion; earlier committed firings remain. Index construction, graph publication and total process memory are governed by their separate contracts, not by this matching budget.

Binding-based follow instructions use program format 10 / semantics 11. Older profiles retain their original meanings and bytes. The verifier checks the reference’s target type, prior initialization, complete key coverage and key order before execution. Deploy the updated compiler and runtime together for these programs. Indexed library format 4 can carry the new program profile and preserve selective linking and source evidence. Use action constructors to build references from scalar bindings; see reference construction.

Construct references from rule bindings

Use Asset.ref(use("serial")) in a Python rule action to construct an entity identity from an established scalar binding. Supply every key component in schema key order; constants and bindings may be mixed. The compiler requires each binding to have the exact key type in every branch. No conversion, nested constructor, array expression, optional key or partial key is accepted.

This complete example registers an asset and its referring assignment in one firing. The assignment may be written first because the complete candidate graph is checked before publication. The second rule follows it, so the target fact appears in the explanation. Updating the request refreshes the derived facts while the saved snapshot keeps its earlier values.

from ting_reasoner import (
    Engine, EntitySchema, FactSchema, KnowledgeBase, ReferenceField,
    Rule, bind, derive, emit, use,
)

Asset = EntitySchema("Asset", {"serial": str, "location": str}, key=("serial",))
NewAsset = FactSchema("NewAsset", serial=str, location=str)
Assignment = FactSchema("Assignment", (ReferenceField("asset", "Asset"),))
register = Rule("register", (
    NewAsset.pattern(serial=bind("serial"), location=bind("place")),
), (
    # Both derived facts are checked together before either is published.
    derive(Assignment, asset=Asset.ref(use("serial"))),
    derive(Asset, serial=use("serial"), location=use("place")),
))
locate = Rule("locate", (
    Assignment.pattern(asset=bind("selected")),
    Asset.follow(use("selected"), location=bind("place")),
), (emit("location", asset=use("selected"), place=use("place")),))
engine = Engine(KnowledgeBase((Asset, NewAsset, Assignment), (register, locate)))
handle = engine.declare(NewAsset(serial="pump-a", location="north"))
engine.run()
result = engine.conclusions[0]
print(result["place"])  # north
assert result["asset"] == Asset.ref("pump-a")
assert len(engine.explain(result)[0].fact_ids) == 2
saved = engine.snapshot()
engine.replace(handle, NewAsset(serial="pump-a", location="south"))
engine.run()
print(engine.conclusions[0]["place"])  # south
print(saved.resolve(Asset.ref("pump-a"))["location"])  # north

In readable source, add requires feature entity-construction alongside entity-references. Inside an action’s ordered ref list, {"use": "serial"} reads the named binding; ordinary JSON strings/integers and {"bytes": "00ff"} are constant keys. These binding objects are valid only in then fields, not in seeds, conditions or expected test values. Add entity-traversal when also using follow.

Save this complete example as entity_construction.ting. Its embedded tests each create an asset, follow the resulting assignment and verify the expected location.

ting source 1
requires semantics 1
module entity_construction version "1.0.0" role program
requires feature entity-references
requires feature entity-construction
requires feature entity-traversal

schemas:
  schema NewAsset:
    serial: text required
    location: text required
  schema Asset key(serial):
    serial: text required
    location: text required
  schema Assignment:
    asset: reference Asset required

conclusions:
  conclusion Location:
    asset: reference Asset required
    location: text required

rules:
  rule register:
    when:
      match NewAsset:
        serial: bind serial
        location: bind place
    then:
      derive Assignment:
        asset: ref Asset [{"use": "serial"}]
      derive Asset:
        serial: use serial
        location: use place
  rule locate:
    when:
      match Assignment:
        asset: bind selected
      follow Asset via use selected:
        location: bind place
    then:
      conclude Location:
        asset: use selected
        location: use place

tests:
  test north:
    given:
      fact NewAsset:
        serial: "pump-a"
        location: "north"
    expect:
      conclusion Location:
        asset: ref Asset ["pump-a"]
        location: "north"
    firings: 2
  test south:
    given:
      fact NewAsset:
        serial: "pump-b"
        location: "south"
    expect:
      conclusion Location:
        asset: ref Asset ["pump-b"]
        location: "south"
    firings: 2

In the IDE, edit the Rules section to add construction; the required feature declarations are inserted automatically. Check and Tests report source diagnostics before Compile publishes an artifact. A failed compile preserves the previous output. Compile with ting-build compile entity_construction.ting -o entity_construction.tingir, then run this script beside the artifact. The original source file is no longer required.

from ting_reasoner import Engine, EntityRef, load_tingir

engine = Engine(load_tingir("entity_construction.tingir"))
NewAsset = engine.schema("entity_construction.NewAsset")
engine.declare(NewAsset(serial="pump-a", location="north"))
engine.run()
result = engine.conclusions[0]
print(result["location"])  # north
assert result["asset"] == EntityRef("entity_construction.Asset", ("pump-a",))
assert len(engine.explain(result)[0].fact_ids) == 2

Identity and evidence: construction does not read a target, add a premise or guarantee existence. An emitted reference is an identity value and may not resolve; resolve() then raises KeyError. A reference stored in a fact’s ReferenceField must resolve in the complete resulting graph. A missing target or conflicting entity rejects that firing with SchemaError and preserves the previous committed state. Earlier successful firings remain committed. Use an explicit target match or follow whenever target properties must support a conclusion.

Dynamic construction uses program format 11 / semantics 12. The verifier bounds the flat key list by the declared schema and charges each key against instruction fuel. Ordinary firing budgets and graph-entry admission still apply; they do not guarantee elapsed-time deadlines or total process memory. Literal-only references retain their earlier profiles. Use matching compiler/runtime versions; source-free indexed libraries and worker transfer preserve exact keys and evidence.

Bound indexes and inspect their cost

MemoryPolicy(max_entity_index_entries=N) bounds the current Engine’s entity and edge entries plus a staged replacement index. One entity and one edge each count as one entry. For a replacement with two current and two candidate entries, allow four. The count is checked before target and incoming-edge maps are allocated. A resource error publishes no partial graph. The limit is independent of numerical byte and store-buffer limits; it does not estimate Python bytes, key sizes, snapshot indexes or process RAM. Use the IDE’s General → Entity index entries control; Unlimited leaves this entry bound unset. Each worker applies its own job policy.

entity_index_report exposes current entities, references, peak_reserved_entries and limit_entries. The peak counts accepted old/new overlap for the Engine’s lifetime; reset preserves that peak. Current and saved-snapshot lookup uses a hash index with cost proportional to key width on average. Graph validation counts and scans candidate rows and edges without recursively expanding targets; a graph mutation rebuilds its indexes. Programs with no entity or reference fields bypass this graph path. Engine-created snapshots share the validated immutable index for their captured generation. An update publishes a new index; older snapshots retain the earlier one until released.

Snapshot ownership and index storage

Use engine.snapshot() to retain one consistent set of facts, conclusions and explanations. The snapshot shares its immutable entity index with the Engine and other snapshots of that generation. It keeps no Engine, packed store, lock or registry alive. Updating the Engine publishes a new graph without changing earlier lookups. Fact records, ordered fact tuples and proof collections still have their own snapshot costs; snapshot creation is not constant-time or a zero-allocation operation.

Read engine.entity_storage_report when you need to inspect current and retained index storage. Inspection scans each previously unmeasured live generation once and caches its shallow total. Subsequent inspections enumerate live generations and reuse those totals. The weak registry does not keep obsolete indexes alive; the returned EntityStorageReport contains counts and scope text, not graph references.

import gc
from ting_reasoner import Engine, EntitySchema, FactSchema, KnowledgeBase, ReferenceField

Asset = EntitySchema("Asset", {"id": str, "location": str}, key=("id",))
Sensor = FactSchema("Sensor", (ReferenceField("asset", "Asset"),))
engine = Engine(KnowledgeBase((Asset, Sensor)))
asset, sensor = engine.declare(
    Asset(id="pump-a", location="north"), Sensor(asset=Asset.ref("pump-a")),
)
first = engine.snapshot()
second = engine.snapshot()
report = engine.entity_storage_report
assert report.live_index_generations == 1  # Both snapshots share this index.
assert report.snapshot_index_reuses == 2
assert report.current_entries == 2  # One entity and one reference.

engine.replace(asset, Asset(id="pump-a", location="south"))
report = engine.entity_storage_report
assert report.live_index_generations == 2
assert report.retained_index_generations == 1  # The old index is counted once.
assert report.retained_entries == 2
assert report.live_index_shallow_bytes == (
    report.current_index_shallow_bytes + report.retained_index_shallow_bytes
)
print(first.resolve(Asset.ref("pump-a"))["location"])  # north
print(engine.resolve(Asset.ref("pump-a"))["location"])  # south

del first, second
gc.collect()  # Explicit collection makes this demonstration portable.
report = engine.entity_storage_report
assert report.retained_index_generations == 0
assert report.retained_index_shallow_bytes == 0
assert report.current_index_shallow_bytes > 0
print(report.as_dict())  # Byte counts depend on the Python implementation/build.
Report fieldMeaning
current_index_shallow_bytesPython shallow sizes for the current published index.
retained_index_shallow_bytesSum for older published generations still reachable, normally through saved snapshots.
live_index_shallow_bytesCurrent plus retained storage; a shared generation is counted once.
live_index_generations / retained_index_generationsDistinct current-plus-retained / older published indexes. An empty entity graph still has an index.
current_entries / retained_entriesEntities plus reference edges in the corresponding generations.
snapshot_index_reusesSuccessful Engine snapshot captures that reused an index over this Engine’s lifetime. It is not the number of snapshots still alive; reset does not clear it.
coverage / excludedExact accounting scope, also included by as_dict().

Measurement scope: shallow sizes include index objects and maps, generated identities/key tuples, fact wrappers/value tuples, incoming-link tuples, and link/handle wrappers. A fact used by several links is counted once per index. Borrowed schemas, key payloads and referenced values are not recursively sized. Snapshot records and their fact tuples, unpublished staging/candidate tracebacks, registry/inspection metadata, other indexes, proofs, allocator overhead and process RAM are excluded. These Python-build-dependent totals are diagnostic estimates, not an admission limit, an exclusive heap allocation total or a prediction of RSS released by deleting snapshots.

The existing max_entity_index_entries limit still covers current plus candidate entries only; retained old generations do not consume that limit. Release unneeded snapshots deliberately. Pickle/deepcopy and the public EngineSnapshot(...) constructor produce detached indexes outside the originating Engine’s report. dataclasses.replace(snapshot, ...) validates the supplied records and rebuilds an independent index. Programs without entity/reference fields report zero graph storage. No source feature or portable-format change is required to use this Python inspection API.

Portable files and explicit errors

Entity programs use Compact-IR format 9 with semantics 10; binding-based follow uses format 10 with semantics 11; dynamic action construction uses format 11 with semantics 12. Existing programs retain their established profiles. Verification checks keys, target schemas, reference constants and complete seed graphs before execution. Use program artifacts, explicit source imports or compiled entity libraries. RuleSpec version 1 serialization of references is rejected explicitly. A SchemaError identifies duplicate keys, an unresolved strong reference or a key-changing replacement; use one transaction to make the intended graph consistent. A ResourceLimitError reports the entry count required by the chosen operation.

Knowledge — facts included in every program session

Use this tab for fixed facts, such as reference thresholds. These are immutable seeds: editing them requires recompiling and starting a new engine.

seed example_item: Item:
  name: "fixed example"
  ready: true

The fact type must exist in Data or be imported. Every field must be supplied. The IDE adds the seed-facts feature when needed. Tests use this knowledge only when they explicitly include given seed.

Rules — when something is true, produce a result

Each rule has a unique name, a when block and a then block. A binding captures a field value; use refers to that binding later.

rule recognize_ready_item:
  when:
    match Item:
      name: bind name
      ready: true
  then:
    conclude ReadyItem:
      name: use name

This rule reports each ready Item by name. Use derive FactType: to produce another fact for further reasoning, or conclude OutputType: to report a typed result. All output fields must be present.

Insert example inserts editable syntax and gives its declaration a distinct name. It does not infer your data model. Adjust the referenced types and fields. Use Check to find incorrect references before running tests.

Decisions — name a reusable assessment

This tab is optional. It stores a decision request with a goal, candidate list, requirements and an evidence conclusion. Ordinary rules produce the supporting or opposing evidence.

decision ChooseItem:
  goal: "choose_item"
  candidates: ["first", "second"]
  requirements: ["ready"]
  evidence: Evidence
  closed: true
  policy: "unique-feasible-v1"

The corresponding Output type must be:

conclusion Evidence:
  goal: text required
  candidate: text required
  requirement: text required
  satisfied: boolean required
  reason: text required

Write rules that conclude Evidence for the named goal, candidate and requirement. satisfied supplies supporting or opposing evidence; reason records a reason code. Set closed: true only when the candidate domain is intentionally complete. Missing evidence does not prove a requirement is satisfied.

The IDE enables the decisions feature when needed. The compiled request is retrieved and assessed through Ting’s runtime API; Compile does not by itself choose a winner. See the separate offline language manual for complete decision examples and the meaning of selected, tied, missing, conflicting and incomplete outcomes.

Tests — examples with expected results

Each test starts in a fresh session. Supply facts under given and expected conclusions under expect. The expected collection must match exactly. firings is an optional exact rule-firing count.

test ready_item_is_reported:
  given:
    fact Item:
      name: "example"
      ready: true
  expect:
    conclusion ReadyItem:
      name: "example"
  firings: 1

For a negative case, use ready: false, an empty expect: block and firings: 0. Add a standalone given seed before given: when the test should include fixed Knowledge. Test fixtures are not deployed as live input facts.

Test runs the root file’s tests. Compile runs them before publication. Imported providers’ tests are not automatically run; open and test those libraries separately. Zero tests is reported explicitly and is not a claim of behavioral coverage.

Running tests retains the facts, conclusions and explanations needed for each test. Numeric memory settings limit numerical buffers; they do not limit the total memory used by these records. Reduce oversized test fixtures or run smaller independent cases when necessary. A resource error means a test could not finish, rather than that its expected conclusion was disproved.

Saved file information

Open the IDE’s File information tab to see which .ting file you are working with. It shows the filename and full path, saved module name/version/role, UTF-8 byte size, line endings, total lines, blank lines and comment-only lines. The timestamp records when the IDE last successfully opened or saved that copy, in your local timezone.

Read the categories

The category list counts local fact types, entity types with keys, declared fields, reference fields, output types, classifications, fixed facts, rules, decisions, tests, import blocks and exports. Fields include both fact and output declarations. Entity types are a subset of fact types; reference fields are a subset of fields. An import block counts once even when it lists several names. Private declarations are included.

For the complete entity_library.ting example, the saved counts are 3 fact types, 1 entity type, 8 fields, 4 reference fields, 1 output type, 3 fixed facts, 1 rule, 2 tests and 4 exports. Test inputs and expected outputs are not counted again as schema or fixed-fact declarations.

Saved contents and unsaved edits

Counts describe the last successfully opened or saved bytes. If you edit any section or apply Full source changes, the badge says Unsaved edits and the saved counts stay unchanged. Save refreshes them; Save As also changes the displayed path. If you edit again while a save is running, the tab reports the saved snapshot and continues to mark the newer edits as unsaved. A failed save retains the previous information.

A new document says Not saved yet until its first successful save. External file changes are checked when saving or reopening; this tab does not continuously monitor disk changes. The counts are a text summary, not a successful compiler check. Imported declarations, runtime facts and inferred results are excluded. Use Check and Tests to validate behavior. Viewing the tab does not edit your source.

Libraries — optional additional source files

  1. Open General → Libraries.
  2. Choose Add library file. Select a .ting file with role library. Its module name and path are filled in.
  3. In the Imports box, name its exact version and selected exported declarations.
  4. Click Apply, then Check.
from shared.types version "1.0.0":
  schema SharedFact

A provider exposes names through its Exports box, for example schema SharedFact. Add mappings for transitive imported modules as well. Merely placing a file in the same directory does not resolve an import. Module identities and exact versions must match.

Path mappings are remembered in local application settings for this source path. To move a project to another computer, bring its source libraries and reattach the mappings there. No hidden sidecar is needed for a self-contained source file. A fully linked program’s .tingir contains its selected definitions; a reusable compiled library may still require other providers during linking.

Reuse entities from compiled libraries

An indexed library lets an application select a named type, rule or fixed entity without loading unrelated payloads. Entity identities remain qualified keys; source paths, row addresses and handles are not part of those identities.

Write and compile a reusable linked-equipment library

Save this complete source as entity_library.ting. The two pumps reference each other. The private spare is unrelated. The exported rule explicitly requires pump_a; selecting that rule also selects pump_b to keep the backup relationship valid. Both embedded tests include fixed knowledge.

ting source 1
requires semantics 1
module entity_library version "1.0.0" role library
requires feature entity-references
requires feature seed-facts

schemas:
  schema Asset key(serial):
    serial: text required
    backup: reference Asset required
    location: text required
  schema Request:
    asset: reference Asset required
  schema Alarm:
    asset: reference Asset required
    code: text required

conclusions:
  conclusion SelectedAlarm:
    asset: reference Asset required
    code: text required

knowledge:
  seed pump_a: Asset:
    serial: "pump-a"
    backup: ref Asset ["pump-b"]
    location: "north"
  seed pump_b: Asset:
    serial: "pump-b"
    backup: ref Asset ["pump-a"]
    location: "south"
  seed spare: Asset:
    serial: "spare"
    backup: ref Asset ["spare"]
    location: "store"

rules:
  rule select_alarm:
    requires:
      seed pump_a
    when:
      match Request:
        asset: bind selected
      match Alarm:
        asset: eq use selected
        code: bind code
    then:
      conclude SelectedAlarm:
        asset: use selected
        code: use code

tests:
  test selected_reference:
    given seed
    given:
      fact Request:
        asset: ref Asset ["pump-a"]
      fact Alarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
      fact Alarm:
        asset: ref Asset ["pump-b"]
        code: "pressure"
    expect:
      conclusion SelectedAlarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
    firings: 1
  test all_alternative_matches:
    given seed
    given:
      fact Request:
        asset: ref Asset ["pump-a"]
      fact Alarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
      fact Alarm:
        asset: ref Asset ["pump-a"]
        code: "pressure"
    expect:
      conclusion SelectedAlarm:
        asset: ref Asset ["pump-a"]
        code: "temperature"
      conclusion SelectedAlarm:
        asset: ref Asset ["pump-a"]
        code: "pressure"
    firings: 2

exports:
  schema Asset
  rule select_alarm
  seed pump_a
  conclusion SelectedAlarm

Compile with ting-build compile entity_library.ting -o entity_library.tingir, or use the IDE with General → Library and Compile. The compiler validates all fixed reference targets and runs the embedded tests before publishing the file. The File information tab describes the saved source and its local declaration counts.

Select and run without the source

Run this complete script beside the compiled library. You may remove the editable source first. A schema selection is useful when the application supplies its own entities; a rule selection can carry explicitly required fixed knowledge.

from ting_reasoner import CompiledCatalog, Engine, EntityRef, load_tingir

# The .ting source is not needed to load or select this library.
library = load_tingir("entity_library.tingir")
catalog = CompiledCatalog({"entity_library": library})

# A type-only selection includes target types but activates no fixed facts.
types = catalog.link(schemas=("entity_library.Asset",))
assert types.knowledge_base.seeds == ()
assert types.knowledge_base.rules == ()

# The rule explicitly requires pump_a. Its backup link also needs pump_b.
linked = catalog.link(rules=("entity_library.select_alarm",))
# Selected bytes include each shared program payload only once.
assert linked.usage.programs > 0
assert 0 < linked.usage.program_bytes < linked.usage.section_bytes
engine = Engine(linked.program)
assert len(engine.facts) == 2
assert ("entity_library", "seed", "spare") not in linked.imports

Request = linked.schema("entity_library.Request")
Alarm = linked.schema("entity_library.Alarm")
selected = EntityRef("entity_library.Asset", ("pump-a",))
engine.declare(Request(asset=selected), Alarm(asset=selected, code="temperature"))
engine.run()
result = engine.conclusions[0]
print(result["code"], engine.resolve(result["asset"])["location"])
# Output: temperature north
assert len(engine.explain(result)[0].fact_ids) == 2
assert linked.explain(engine, result).proofs
assert linked.origin_for("entity_library.pump_b", kind="seed").name == "entity_library.pump_b"

Understand which declarations are loaded

Schema and conclusion selections include the target types required by their reference fields. They do not implicitly select seeds or business rules. Selecting a seed follows its reference values to the fixed target facts, including forward links and cycles; unrelated fixed facts remain unselected. If several named fixed facts describe the same referenced entity, their names travel together. Rules bring their declared requirements and typed outputs. Private declarations can satisfy dependencies but cannot be requested directly as public exports.

Cross-module targets require explicit catalog mappings and the exact compatible versions and semantic contracts used at compilation. Source imports still use explicit source mappings. CompiledCatalog performs runtime selection from compiled files; it does not parse source or search for modules.

Verification, costs and failure behavior

The compiler writes entity libraries in indexed container format 4. Identical closed program fragments share one payload within a module, using program format 9 / semantics 10 for entities or format 10 / semantics 11 for binding-based traversal. Each declaration retains its own source text, export visibility and dependency list. For a cycle in which every seed reaches the same graph, those seeds share the compiled graph. The linker reads and verifies that payload once during each link, then checks each selected declaration against it. No verification cache survives between link operations.

Seed dependencies name immediate fixed targets; the catalog follows them to select the complete graph. Shared storage does not export private seeds, activate unrelated facts or remove source evidence. Differing closures can still overlap in storage: for example, successive roots of a long one-way chain contain different reachable graphs. Such cases can still grow quadratically and remain subject to the configured limits.

Opening verifies the directory. Linking verifies selected sections and their dependencies; library.verify_all() checks every payload. linked.usage.sections counts selected declarations. section_bytes counts their metadata plus each selected shared program once; programs and program_bytes identify the shared program portion. These last two counters are zero for older containers with embedded programs. LibraryLimits constrain file, section, build, dependency depth and selected bytes. The section limit applies to one declaration’s metadata plus its program, even when that program is shared. IRLimits applies to every unique selected program and the final linked program. Large or deeply connected graphs can require deliberate limit increases. A missing dependency, mismatched definition, wrong key, unresolved fixed target or corrupt selected payload rejects linking before an Engine is created. These byte limits bound serialized payloads, not total Python memory. Readers also accept existing entity libraries in format 3. Non-entity libraries retain format 1 or 2. Older Ting versions that lack the format-4 reader reject new entity libraries; update the runtime and compiler together. Module versions and provider contracts still must match exactly.

RuleSpec version 1 still cannot serialize entity references. Match target facts explicitly when their properties affect a rule; Python resolve() does not add evidence or a hidden dependency. Use explicit follow conditions to match target properties with their fact support. Dynamic action construction is available in program profile 11/12; it creates identity without target support. Implicit dereference expressions are unsupported.

Open, save and return to your work

The custom Open dialog shows folders and .ting files on the left and a structure preview on the right. Select a file to see its module, role, size and declarations. Preview is bounded to 500 declaration names and does not run or semantically validate rules. A file with syntax errors can still be opened for repair.

Save writes the current source atomically. Save as chooses another path. The IDE checks whether an opened source has changed on disk and preserves your editing buffer if it detects a conflict. Use Save as to keep those edits or reopen the external version.

The last successfully opened or saved source reopens automatically on launch, along with its locally remembered library mappings. A missing or moved file produces a message in Build feedback and leaves a new starter available. Unsaved edits are protected by Save/Discard/Cancel prompts when switching files or closing.

The IDE reopens the saved file; it does not promise crash recovery of unsaved text. Save regularly. Use ting-ide --fresh to bypass automatic reopening, or ting-ide path/to/program.ting to open a particular file.

Compile and understand feedback

Toolbar actionEffect
CheckChecks source, bindings, types and imported modules without publishing output.
TestChecks the source and runs its embedded tests, using the current editor contents.
CompileSaves source when needed, checks it, runs tests, compiles and verifies staged output, then atomically publishes program.tingir beside program.ting.
StopTerminates the active build process. Cancelled and failed builds do not replace the previous output.

File reads and previews, validation, tests and compilation use background processes. The progress strip names the real stage currently running. It is an activity indicator, not an estimated percentage. The startup splash reports artwork, help, workspace construction and source restoration.

You can continue editing during a build. The result belongs to the snapshot submitted at the start. If newer edits exist, the IDE marks the result as belonging to an earlier snapshot; compile again to update it. Located diagnostics navigate only while their snapshot still matches.

The editor accepts UTF-8 source up to 8 MiB and 250,000 lines, with bounded source-graph and compiler limits. Files open in a worker; editing boxes are populated only when visited. Syntax coloring is omitted for boxes larger than 256,000 characters. Compiler and test execution budgets still apply. This keeps oversized work from silently becoming unbounded.

A .tingir program requires a compatible Ting Reasoner runtime. It is not a native executable. The IDE’s Test action exercises example sessions; an application supplies live facts and invokes the runtime API.

Full source, editing and shortcuts

The compact toolbar above the Full source editor provides Undo, Redo, Cut, Copy, Paste, Select all and Find. Buttons become available as edits, selections and clipboard text change. Find opens the editor’s search box. Apply to tabs accepts your edits; Cancel leaves the current document unchanged.

The IDE does not display hover tooltips. Visible labels, keyboard shortcuts, accessible control descriptions and the offline Help menu explain each operation.

Full source shows the complete file, including comments, features, imports and documentation. Apply refreshes the section tabs. If a file has duplicate or unsupported top-level section headings, the section editors become read-only until Full source repairs it. The IDE preserves unedited regions and does not silently discard unsupported text.

Right-click editing boxes for custom Undo, Redo, Cut, Copy, Paste, Select all and indentation actions. Right-click trees to copy or expand their contents. Open the offline Help menu for control explanations.

ShortcutAction
Ctrl+N / Ctrl+O / Ctrl+SNew / Open / Save
Ctrl+Shift+SSave as
F5 / F6 / F7Compile / Check / Test
Shift+F5Stop build
Ctrl+Shift+EFull source
Ctrl+FFind in current section
Tab / Shift+TabIndent / remove two-space indentation
F1Offline IDE guide

Drag the custom title bar to move a window, double-click it to maximize or restore, or drag any edge/corner to resize. Menus, controls and icons use the same light Ting palette throughout.

Add array values

The first numerical test may need a few seconds of preparation. NumPy and Numba are installed with Ting. Install normally with python -m pip install "./ting_reasoner-0.10.0a11-py3-none-any.whl[ide]"; keep dependencies enabled.

Open a source file containing the following complete example. Check it, run Test, then Compile. The test expects 10.0. Bind the array in Rules and write sum use samples in the result field. Integer arrays need integer outputs; floating-point arrays need float outputs. An empty array gives zero. Overflow, non-finite results and insufficient memory produce an error.

ting source 1
requires semantics 1
module sensor_totals version "1.0.0" role program
requires feature arrays
requires feature array-sum

schemas:
  schema Reading:
    name: text required
    samples: array f64[4] required

conclusions:
  conclusion Total:
    name: text required
    value: float required

rules:
  rule total_reading:
    when:
      match Reading:
        name: bind name
        samples: bind samples
    then:
      conclude Total:
        name: use name
        value: sum use samples

tests:
  test four_samples:
    given:
      fact Reading:
        name: "pump"
        samples: array f64[4] [1.0,2.0,3.0,4.0]
    expect:
      conclusion Total:
        name: "pump"
        value: 10.0
    firings: 1

Only sums over the whole numeric array are available. Keep the editable source when moving to another Ting version. The bundled language manual contains Python examples and explains numeric memory reports.

Use the compiled example from Python

Save the complete example above as numeric_sum.ting and compile it. In the same folder, save the following Python code as run_samples.py. Run it with python run_samples.py using the environment where Ting is installed. load_tingir() opens the verified compiled program; engine.schema() retrieves its fact type.

from pathlib import Path
from ting_reasoner import Engine, NDValue, load_tingir

engine = Engine(load_tingir(Path("numeric_sum.tingir")))
Reading = engine.schema("sensor_totals.Reading")
samples = NDValue.from_flat([1.0, 2.0, 3.0, 4.0], dtype="f64", shape=(4,))
engine.declare(Reading(name="pump", samples=samples))
engine.run()
assert engine.conclusions[0]["value"] == 10.0
print(engine.conclusions[0].as_dict())
print(engine.memory_report.peak_registered_bytes)
print(engine.store_memory_report.as_dict())

memory_report describes numeric operations and their limits. store_memory_report separately inspects the current physical fact-store buffers and estimated container overhead. Neither report measures total process memory. The store report excludes object-column values, including this example’s text and array values, as well as indexes, proofs and saved snapshots. Read each report’s coverage and excluded fields before interpreting its numbers.

Share and inspect array storage

Identical immutable arrays can share storage while facts retain distinct handles and complete rule proofs. Sharing compares element type, shape and canonical bytes, including signed floating-point zero. It applies automatically to Python inputs, fixed knowledge and rule outputs, including compiled programs. It requires no new source feature or General setting.

Open examples/array_storage.ting from the source archive. Check, Test and Compile should report two totals of 10. Build feedback shows the highest unique payload bytes and repeated-reference bytes in any embedded test session. Each maximum is calculated separately across tests; it is not a combined memory peak. Double-click Array storage to open this topic.

Save the following complete example as inspect_arrays.py and run it with the Python where you installed Ting. It keeps two alternative proofs for each result, then replaces one fact without changing a saved snapshot.

from ting_reasoner import (
    ArraySum, Engine, FactSchema, Field, KnowledgeBase, NDField, NDValue,
    Rule, bind, emit, use,
)

Reading = FactSchema("Reading", [
    Field("number", int), NDField("samples", "i64", (4,)),
])
rules = [
    Rule(name, Reading.pattern(number=bind("number"), samples=bind("samples")),
         [emit("Total", number=use("number"), value=ArraySum(use("samples")))])
    for name in ("primary", "verification")
]
engine = Engine(KnowledgeBase([Reading], rules))

def samples(values):
    return NDValue.from_flat(values, dtype="i64", shape=(4,))

# Construct each input array separately. Different numbers make distinct facts.
handles = engine.declare(
    Reading(number=1, samples=samples([1, 2, 3, 4])),
    Reading(number=2, samples=samples([1, 2, 3, 4])),
)
assert handles[0] != handles[1]
engine.run()
saved = engine.snapshot()
before = engine.array_storage_report
assert before.array_value_slots == 2
assert before.referenced_payload_bytes == 64
assert before.unique_payload_bytes == 32
assert before.shared_payload_bytes == 32
assert [row["value"] for row in engine.conclusions] == [10, 10]
assert all(len(engine.explain(row)) == 2 for row in engine.conclusions)

# Replace a complete fact; an immutable array is never edited in place.
engine.replace(handles[1], Reading(number=2, samples=samples([10, 20, 30, 40])))
engine.run()
after = engine.array_storage_report
assert after.unique_payload_bytes == 64
assert [row["value"] for row in engine.conclusions] == [10, 100]
assert saved.fact_items[1][1]["samples"].to_flat_tuple() == (1, 2, 3, 4)
print(before.as_dict())
print(after.as_dict())

engine.array_storage_report inspects arrays in current base and derived fact columns. referenced_payload_bytes counts each field reference; unique_payload_bytes counts each canonical byte buffer once; their difference is shared_payload_bytes. Metadata estimates are reported separately. A report is immutable and keeps only numbers, so read a new one after updating facts.

The optional cache contains at most 4,096 weak references per Engine. It does not keep unused arrays alive. A full cache or unequal fingerprint collision uses ordinary storage; a cache-metadata allocation failure clears and disables this optional cache. All values and proof alternatives remain available. Compare arrays with ==; do not depend on Python object identity.

This inspection is separate from Numeric memory and the fact-store compaction limit. It excludes external arrays, old snapshots, conclusion/proof-only arrays, other store buffers, indexes, verified knowledge and process RAM. Retaining a snapshot can keep an old array alive after current facts stop using it. Repeated-reference bytes are not a measured saving in total computer RAM. Consult Share and inspect arrays in the bundled language manual for every report field and its scope.

Numeric run limits

General has two settings for Test and Compile. Numeric memory covers the array inputs and working buffers used by numerical operations. One MiB is 1,048,576 bytes. Numeric work limits numeric element checks across the request. Build feedback shows the peak numeric bytes used. These settings do not change your source or compiled file.

Numeric buffers separates the peak bytes used for input values, scalar outputs and temporary values. These peaks are already included in Numeric memory. They may occur at different times, so adding the category peaks does not necessarily give the combined peak. A test that does not use numeric operations may have no numeric-memory feedback. Embedded tests retain their bounded logical matching behavior.

This is not a limit on all memory used by Ting. Facts, indexes, proofs, snapshots, Python and first-use preparation need additional memory. Automatic disk storage and recalculation are not available. If a limit stops a test, increase the limit or use smaller inputs. The previous compiled output remains available after a failure or Stop.

When running a compiled program from Python, Ting may reuse a bounded cache of matching rows. If a complete selection cannot fit in that cache, it uses direct matching and still considers every relevant fact. Cache capacity is independent of the Numeric memory setting; indexes and retained explanations require additional memory. Your existing imports, rule definitions and calls remain the same.

Numeric selections and Python execution

Use constant comparisons to select readings by their values. Save this complete program as numeric_selection.ting. Check validates the source, Test checks both range endpoints and an observation outside the range, and Compile writes numeric_selection.tingir after those tests pass. The two rules preserve separate explanations for each selected reading.

ting source 1
requires semantics 1
module numeric_selection version "1.0.0" role program

schemas:
  schema Reading:
    number: integer required
    value: float required

conclusions:
  conclusion Selected:
    number: integer required

rules:
  rule primary:
    when:
      match Reading:
        number: bind number
        value: between [490.0,499.0]
    then:
      conclude Selected:
        number: use number
  rule confirmation:
    when:
      match Reading:
        number: bind number
        value: between [490.0,499.0]
    then:
      conclude Selected:
        number: use number

tests:
  test lower_endpoint:
    given:
      fact Reading:
        number: 490
        value: 490.0
    expect:
      conclusion Selected:
        number: 490
    firings: 2
  test upper_endpoint:
    given:
      fact Reading:
        number: 499
        value: 499.0
    expect:
      conclusion Selected:
        number: 499
    firings: 2
  test outside_range:
    given:
      fact Reading:
        number: 500
        value: 500.0
    firings: 0

Save the following code as run_selection.py beside the compiled program. Run it with python run_selection.py using the Python where Ting is installed. load_tingir() verifies the program before use. The qualified schema name includes its source module. The script needs the compiled file and does not read the editable source.

from pathlib import Path
from ting_reasoner import Engine, MemoryPolicy, load_tingir

# Compile numeric_selection.ting first, then run this file in its folder.
program = load_tingir(Path("numeric_selection.tingir"))
Reading = Engine(program).schema("numeric_selection.Reading")
engine = Engine(program, memory=MemoryPolicy(max_managed_bytes=8192))
engine.declare(*(Reading(number=i, value=float(i)) for i in range(600)))
result = engine.run()
conclusions = engine.conclusions
assert result.firings == 20
assert [row["number"] for row in conclusions] == list(range(490, 500))
assert all(len(engine.explain(row)) == 2 for row in conclusions)
saved = engine.snapshot()
engine.reset()
assert len(saved.fact_items) == 600
assert len(saved.conclusions) == 10
print([row["number"] for row in saved.conclusions])
print([len(saved.explain(row)) for row in saved.conclusions])

The example supplies 600 readings and returns numbers 490 through 499, in order, with two proofs each. Reuse program with additional Engines for independent cases. For changing observations in one case, update that Engine and call run() again. A snapshot retains its facts and explanations after a reset.

Numeric filtering is automatic for eligible constant float comparisons. A structured ExecutionBudget preserves its logical work limits. MemoryPolicy.max_managed_bytes covers registered numeric inputs and working buffers. The optional session store-buffer cap applies separately; indexes, selected-row references, proofs, saved snapshots and total process RAM remain outside these limits. See Numeric run limits before choosing a limit.

When measuring performance, include fact construction, declaration, inference and the outputs your application consumes. Record the first execution separately because it may prepare numerical code. Repeat the same case several times, retain the median and range, and check every result. Check/Test/Compile feedback describes the source operation and its results.

Compare several floating-point fields

Each reading must satisfy both field comparisons. Save this complete program as float_filtering.ting. Check validates its types, Test checks both endpoints and both rejection cases, and Compile writes float_filtering.tingir after the tests pass. The two rules retain separate explanations.

The ordering comparison includes both positive and negative zero. Equality and inequality distinguish their signs: eq(-0.0) selects negative zero specifically. Comparisons follow the schema field order; a failed condition stops the remaining checks for that reading.

ting source 1
requires semantics 1
module float_filtering version "1.0.0" role program

schemas:
  schema Reading:
    number: integer required
    first: float required
    second: float required

conclusions:
  conclusion Selected:
    number: integer required

rules:
  rule primary:
    when:
      match Reading:
        number: bind number
        first: ge -0.0
        second: between [490.0,499.0]
    then:
      conclude Selected:
        number: use number
  rule confirmation:
    when:
      match Reading:
        number: bind number
        first: ge -0.0
        second: between [490.0,499.0]
    then:
      conclude Selected:
        number: use number

tests:
  test lower_endpoint:
    given:
      fact Reading:
        number: 490
        first: -0.0
        second: 490.0
    expect:
      conclusion Selected:
        number: 490
    firings: 2
  test upper_endpoint:
    given:
      fact Reading:
        number: 499
        first: 0.0
        second: 499.0
    expect:
      conclusion Selected:
        number: 499
    firings: 2
  test first_field_rejects:
    given:
      fact Reading:
        number: 495
        first: -1.0
        second: 495.0
    firings: 0
  test second_field_rejects:
    given:
      fact Reading:
        number: 500
        first: 0.0
        second: 500.0
    firings: 0

Use the compiled program from Python

Save this example beside the compiled file. It checks all selected numbers and both explanations for each result. Once compiled, the program runs without the editable source file.

from pathlib import Path
from ting_reasoner import Engine, MemoryPolicy, load_tingir

# Compile float_filtering.ting before running this file in the same folder.
program = load_tingir(Path("float_filtering.tingir"))
engine = Engine(program, memory=MemoryPolicy(max_managed_bytes=8192))
Reading = engine.schema("float_filtering.Reading")
engine.declare(*(Reading(number=i, first=(-0.0 if i % 2 else 0.0),
                        second=float(i)) for i in range(601)))
engine.run()
saved = engine.snapshot()
numbers = [row["number"] for row in saved.conclusions]
proof_counts = [len(engine.explain(row)) for row in saved.conclusions]
assert numbers == list(range(490, 500))
assert proof_counts == [2] * 10
assert engine.memory_report.current_registered_bytes == 0
print(numbers)
print(proof_counts)

Eligible constant float comparisons are batched automatically for sufficiently large inputs. Small inputs, unsupported comparisons and runs with an ExecutionBudget use direct matching. No special source feature or acceleration setting is needed. The max_managed_bytes cap covers numeric buffers. See Store buffer limits for a separate fact-buffer cap. Object values, indexes, proofs, saved snapshots and numerical preparation need additional memory.

Inspect exact equality lookups

When a rule matches a field against a constant or an earlier binding, Ting prepares an exact equality index for that field. The index maps each canonical value to an ordered sequence of matching fact rows. A key with one row uses a compact immutable sequence; adding another matching row expands the sequence. Removing rows can make it compact again. The returned facts, conclusions and explanations retain their logical order.

This equipment example joins two ordinary text fields. Such a join permits several Assets with the same serial and does not enforce target existence or entity-key uniqueness. Both matching facts produce their own complete evidence.

from ting_reasoner import Engine, FactSchema, KnowledgeBase, Rule, bind, emit, eq, use

Request = FactSchema("Request", serial=str)
Asset = FactSchema("Asset", serial=str, location=str)
rule = Rule("locate", [Request.pattern(serial=bind("serial")),
                       Asset.pattern(serial=eq(use("serial")), location=bind("location"))],
            [emit("Located", serial=use("serial"), location=use("location"))])
engine = Engine(KnowledgeBase([Request, Asset], [rule]))
engine.declare(Request(serial="pump-a"), Asset(serial="pump-a", location="north"),
               Asset(serial="pump-b", location="south"))
engine.run()
saved = engine.snapshot()
report = engine.equality_index_memory_report
print(report.posting_buckets, report.singleton_buckets, report.row_reference_slots)
# Output: 2 2 2

extra = engine.declare(Asset(serial="pump-a", location="backup"))
engine.run()
assert [item["location"] for item in engine.conclusions] == ["north", "backup"]
assert all(len(engine.explain(item)) == 1 for item in engine.conclusions)
report = engine.equality_index_memory_report
print(report.posting_buckets, report.singleton_buckets,
      report.multirow_buckets, report.row_reference_slots)
# Output: 2 1 1 3
assert len(saved.conclusions) == 1

engine.retract(extra)
engine.run()
assert engine.equality_index_memory_report.singleton_buckets == 2
assert engine.conclusions[0]["location"] == "north"

Understand the report

engine.equality_index_memory_report returns an immutable EqualityIndexMemoryReport. The report inspects the current indexes under the session lock; reading it takes time proportional to the number of posting buckets. indexed_fields counts fields with prepared equality dictionaries. posting_buckets counts their distinct indexed keys. singleton_buckets and multirow_buckets classify those keys by matching-row count. row_reference_slots counts retained row references, including separate references when several fields are indexed.

The three estimated_*_bytes fields report Python-specific shallow sizes for posting containers, dictionaries and their sum. Keys, key values, row-reference integers, other indexes, routes, temporary update overlap and saved snapshots are excluded. These estimates are separate from initialized store payload and numeric reservations. Neither existing MemoryPolicy byte limit admits these index containers. Read the report’s coverage and excluded fields; do not interpret the estimate as process RAM, peak usage or a whole-Engine limit.

Author and compile the same lookup

Save this complete source as indexed_equipment.ting. In the IDE use Check, Tests and Compile, or run ting-build compile indexed_equipment.ting -o indexed_equipment.tingir. The three tests check one match, several matches and an unmatched request. The second pattern reads the serial bound by the first pattern.

ting source 1
requires semantics 1
module indexed_equipment version "1.0.0" role program

schemas:
  schema Request:
    serial: text required
  schema Asset:
    serial: text required
    location: text required

conclusions:
  conclusion Located:
    serial: text required
    location: text required

rules:
  rule locate:
    when:
      match Request:
        serial: bind serial
      match Asset:
        serial: eq use serial
        location: bind location
    then:
      conclude Located:
        serial: use serial
        location: use location

tests:
  test single_match:
    given:
      fact Request:
        serial: "pump-a"
      fact Asset:
        serial: "pump-a"
        location: "north"
    expect:
      conclusion Located:
        serial: "pump-a"
        location: "north"
    firings: 1
  test several_matches:
    given:
      fact Request:
        serial: "pump-a"
      fact Asset:
        serial: "pump-a"
        location: "north"
      fact Asset:
        serial: "pump-a"
        location: "south"
    expect:
      conclusion Located:
        serial: "pump-a"
        location: "north"
      conclusion Located:
        serial: "pump-a"
        location: "south"
    firings: 2
  test no_matching_asset:
    given:
      fact Request:
        serial: "pump-b"
      fact Asset:
        serial: "pump-a"
        location: "north"
    expect:
    firings: 0

Save the following Python script beside the compiled program and run it. The original source is no longer required. Compiled execution uses the same indexes and provides the same inspection report.

from ting_reasoner import Engine, load_tingir

engine = Engine(load_tingir("indexed_equipment.tingir"))
Request = engine.schema("indexed_equipment.Request")
Asset = engine.schema("indexed_equipment.Asset")
engine.declare(Request(serial="pump-a"), Asset(serial="pump-a", location="north"),
               Asset(serial="pump-a", location="backup"))
engine.run()
assert [item["location"] for item in engine.conclusions] == ["north", "backup"]
report = engine.equality_index_memory_report
print(report.posting_buckets, report.multirow_buckets, report.row_reference_slots)
# Output: 1 1 2

Limit session store buffers

Set MemoryPolicy(max_store_buffer_bytes=...) when constructing an Engine to bound the payload of its numeric and byte fact columns and row metadata. The value is a non-negative integer in bytes; None, the default, applies no session store-buffer limit. It includes unused physical rows and the extra payload copied or appended during a change.

A change may need more room than its final facts occupy. Ting keeps the current store available while preparing an update. Unchanged shared chunks count once; copied chunks count separately until publication. In this example each row contributes 30 payload bytes. Adding the second row needs 30 bytes for the existing row, 30 for its copied chunk and 30 for the new row. The next addition would need 150 bytes, so it fails before changing the session.

from ting_reasoner import (
    Engine, FactSchema, KnowledgeBase, MemoryPolicy, ResourceLimitError,
    Rule, bind, emit, gt, use,
)

Reading = FactSchema("Reading", number=int, value=float, ready=bool)
rule = Rule("select", Reading.pattern(number=bind("number"), value=gt(1.0)),
            [emit("Selected", number=use("number"))])
engine = Engine(KnowledgeBase([Reading], [rule]),
                memory=MemoryPolicy(max_store_buffer_bytes=90))
engine.declare(Reading(number=1, value=1.0, ready=True))
engine.declare(Reading(number=2, value=2.0, ready=True))
engine.run()
saved = engine.snapshot()

try:
    engine.declare(Reading(number=3, value=3.0, ready=True))
except ResourceLimitError as error:
    assert error.resource == "session store buffer bytes"
    assert (error.requested, error.limit) == (150, 90)
else:
    raise AssertionError("The store limit should reject this change")
assert engine.snapshot() == saved
report = engine.store_allocation_report
assert report.staged_buffer_payload_bytes == 0
print(report.current_buffer_payload_bytes, report.peak_reserved_buffer_payload_bytes)
# Output: 60 90. The two rows occupy 60 payload bytes; the update peak was 90.

engine.reset()
assert len(saved.fact_items) == 2  # A snapshot retains its own public values.
print(engine.store_allocation_report.current_buffer_payload_bytes)  # 0

Read the admission report

engine.store_allocation_report returns an immutable StoreAllocationReport. Reading it uses maintained counters and does not scan the store. current_buffer_payload_bytes describes initialized payload in the committed store. peak_reserved_buffer_payload_bytes is the largest reservation bound since this Engine was created, including failed attempts. It can exceed actual allocated payload because admission precedes allocation. staged_buffer_payload_bytes is zero after an operation returns or raises. allocation_reservations counts accepted growth and copy reservations; limit_bytes records the configured cap. Reset releases the current store payload but preserves this lifetime peak.

This cap does not cover total application memory. Container headers, spare capacity, values in object columns (including text, integers and arrays), indexes, proofs, snapshots, error tracebacks, the compiled plan, numerical working buffers and process RAM are outside it. Read coverage and excluded on the report. Use store_memory_report for a separate container inspection; do not add its current payload to the admission report because both describe the same current buffers.

Handle an insufficient limit

ResourceLimitError means the requested operation did not complete. A failed declaration, replacement, retraction, transaction publication, reset or compaction leaves the committed session unchanged. A failing rule publishes none of that rule’s consequences; earlier completed firings remain committed. Reservations are released on errors and interruptions. Allow room for update overlap when choosing a limit. To use a different policy, create a new Engine with the larger limit and redeclare the intended input facts.

The session limit applies throughout the Engine’s lifetime, including immutable seeds and derived facts. The optional max_buffer_bytes on transactions and compaction adds an operation-specific limit; both checks apply. max_managed_bytes and max_numeric_work continue to limit numerical operations independently.

Run a compiled program

Save the complete source from Compare several floating-point fields as float_filtering.ting. Compile it in the IDE, or run ting-build compile float_filtering.ting -o float_filtering.tingir in that directory. Then save the following Python code beside the compiled file and run it. The source file is no longer required. The IDE and compiler terminal commands use their usual defaults; configure this session limit in the Python Engine that runs the compiled program.

from ting_reasoner import Engine, MemoryPolicy, ResourceLimitError, load_tingir

program = load_tingir("float_filtering.tingir")
engine = Engine(program, memory=MemoryPolicy(
    max_managed_bytes=8192, max_store_buffer_bytes=22237,
))
Reading = engine.schema("float_filtering.Reading")
engine.declare(*(Reading(number=i, first=0.0, second=float(i)) for i in range(601)))
engine.run()
saved = engine.snapshot()
assert sorted(item["number"] for item in engine.conclusions) == list(range(490, 500))
assert engine.store_allocation_report.current_buffer_payload_bytes == 22237

try:
    engine.declare(Reading(number=601, first=0.0, second=601.0))
except ResourceLimitError:
    pass
else:
    raise AssertionError("The store limit should reject this change")
assert engine.snapshot() == saved
print(engine.store_allocation_report.current_buffer_payload_bytes)  # 22237

Independent jobs

Pass the same policy through InferenceJob(memory=MemoryPolicy(max_store_buffer_bytes=...)) when using run_batch() or iter_batch(). Each job receives its own session limit, whether it runs inline or in a process. Its SessionResult.store_allocation_report records that session’s counters. These caps do not combine into a shared limit across workers or across multiple Engines. Engines created inside numeric_run() inherit the policy but still have separate store limits.

Transaction buffers

Transactions belong to the Python application that runs a compiled Ting program. Compile the following complete source as transaction_readings.ting. Check validates it, Test runs its two embedded cases, and Compile writes transaction_readings.tingir. Failed tests or a cancelled build preserve an existing compiled output.

ting source 1
requires semantics 1
module transaction_readings version "1.0.0" role program

schemas:
  schema Reading:
    number: integer required
    value: float required

conclusions:
  conclusion Reported:
    number: integer required
    value: float required

rules:
  rule report_reading:
    when:
      match Reading:
        number: bind number
        value: bind value
    then:
      conclude Reported:
        number: use number
        value: use value

tests:
  test two_readings:
    given:
      fact Reading:
        number: 1
        value: 10.0
      fact Reading:
        number: 2
        value: 20.0
    expect:
      conclusion Reported:
        number: 1
        value: 10.0
      conclusion Reported:
        number: 2
        value: 20.0
    firings: 2
  test empty_case:
    firings: 0

Save the following Python code as update_readings.py in the same folder and run it using the Python where Ting is installed. It loads the compiled program, rejects an insufficient publication limit, then retries the update. Its saved snapshot retains the original observations.

from pathlib import Path
from ting_reasoner import Engine, ResourceLimitError, load_tingir

# Compile transaction_readings.ting in the IDE first. Run this script from
# the folder containing transaction_readings.tingir; the source is not needed.
engine = Engine(load_tingir(Path("transaction_readings.tingir")))
Reading = engine.schema("transaction_readings.Reading")
first, second = engine.declare(
    Reading(number=1, value=10.0), Reading(number=2, value=20.0),
)
engine.run()
saved = engine.snapshot()
try:
    with engine.transaction(max_buffer_bytes=0) as rejected:
        rejected.replace(first, Reading(number=1, value=11.0))
except ResourceLimitError:
    assert rejected.closed and rejected.receipt is None
else:
    raise AssertionError("Expected the zero-byte publication limit to reject")
assert engine.snapshot() == saved

maximum = 2 * engine.store_memory_report.current_buffer_payload_bytes
with engine.transaction(max_buffer_bytes=maximum) as update:
    replacement = update.replace(first, Reading(number=1, value=11.0))
assert update.receipt.resolve(replacement) == first
report = update.receipt.buffer_report
assert report.peak_buffer_payload_bound_bytes == 116
engine.run()
assert [row["value"] for row in engine.conclusions] == [11.0, 20.0]
assert saved.fact_items[0][1]["value"] == 10.0
print(report.peak_buffer_payload_bound_bytes)
print([row["value"] for row in engine.conclusions])

max_buffer_bytes limits initialized old/replacement store-buffer payload overlap at commit. The allowance also includes the complete old derived-buffer payload when support maintenance is enabled. It excludes object-column values, staged edits, headers/spare capacity, indexes, proofs, snapshots, JIT and process RAM. A same-schema replacement that keeps the row count can use twice the current payload as a conservative bound; adding rows can need more.

A changed successful receipt provides buffer_report, including the reserved bound and configured limit. An unchanged commit provides None and accepts a zero limit. Rejection closes the transaction without a receipt and preserves the committed engine. Start a new transaction to retry; run inference after a successful commit.

The IDE's Numeric limits controls apply to numeric operations during tests. The transaction publication limit is supplied by your Python application and does not change source or compiled-program meaning.

Compact fact storage from Python

When your Python application removes or replaces many facts, use engine.compact_store() outside a transaction to remove unused physical rows. The call preserves current facts, handles, results, explanations and the engine epoch. It does not remove the original values from saved snapshots.

The optional max_buffer_bytes keyword checks the old and replacement numeric/byte column and row-buffer payloads together before replacement allocation. Pass a non-negative integer in bytes. Omitting it applies no compaction buffer limit. The IDE’s Numeric memory setting remains a limit on numerical tests; this Python argument is a separate setting.

Save this complete example as compact_readings.py and run python compact_readings.py in the environment where Ting is installed:

from ting_reasoner import (
    Engine, FactSchema, KnowledgeBase, ResourceLimitError,
)

Reading = FactSchema("Reading", number=int, value=float)
engine = Engine(KnowledgeBase([Reading]))
handles = engine.declare(
    *(Reading(number=i, value=float(i)) for i in range(300))
)
saved = engine.snapshot()
engine.retract(handles[-1])
before_compaction = engine.snapshot()

# Zero bytes cannot hold the old and replacement buffers together.
try:
    engine.compact_store(max_buffer_bytes=0)
except ResourceLimitError as error:
    print(error.resource, error.requested, error.limit)
assert engine.snapshot() == before_compaction

# Removing unused rows needs no more payload than two current stores.
maximum = 2 * engine.store_memory_report.current_buffer_payload_bytes
metrics = engine.compact_store(max_buffer_bytes=maximum)
report = engine.store_compaction_report
assert report is not None
assert report.overlap_buffer_payload_bytes <= maximum
assert report.peak_staged_rows == 1
assert metrics.tombstone_rows == 0
assert engine.snapshot() == before_compaction
assert len(saved.fact_items) == 300
print(report.as_dict())

store_compaction_report is initially None. After a successful copy it reports input and output buffer payloads, their combined overlap, rows copied and temporary rows staged. At most one row-value tuple is staged during copying; the required mapping and indexes still grow with the number of live rows. A call with no unused rows does not replace the report. A complete store replacement, including reset(), clears it.

The buffer limit excludes container headers and spare capacity, object-column values such as text and arrays, mappings, indexes, proofs, saved snapshots and process RAM. Python can still raise MemoryError for those allocations. estimated_remap_container_bytes is a separate estimate of the mapping container, excluding its referenced integers. Read each report’s coverage and excluded fields. Failure before publication preserves the current store and its last successful report; increase the appropriate limit or reduce retained application data before retrying.

Complete universal evidence

A for_all condition requires every fact of its schema to satisfy the listed constraints. The positive condition retains all witness handles in order. Several rules can support one conclusion; engine.explain() returns their distinct proof edges.

Save the source below as universal_evidence.ting. The General, Data, Rules and Tests editors use its corresponding sections. Check validates it, Test runs both cases, and Compile saves universal_evidence.tingir. Expect two passing tests: all twenty observations produce one Ready conclusion through two rules; a failed observation produces no firing.

ting source 1
requires semantics 1
module universal_checks version "1.0.0" role program

schemas:
  schema Sample:
    number: integer required
    passed: boolean required

conclusions:
  conclusion Ready:
    label: text required

rules:
  rule inspection:
    when:
      for_all Sample:
        passed: true
    then:
      conclude Ready:
        label: "complete"
  rule review:
    when:
      for_all Sample:
        passed: true
    then:
      conclude Ready:
        label: "complete"

tests:
  test all_observations_pass:
    given:
      fact Sample:
        number: 0
        passed: true
      fact Sample:
        number: 1
        passed: true
      fact Sample:
        number: 2
        passed: true
      fact Sample:
        number: 3
        passed: true
      fact Sample:
        number: 4
        passed: true
      fact Sample:
        number: 5
        passed: true
      fact Sample:
        number: 6
        passed: true
      fact Sample:
        number: 7
        passed: true
      fact Sample:
        number: 8
        passed: true
      fact Sample:
        number: 9
        passed: true
      fact Sample:
        number: 10
        passed: true
      fact Sample:
        number: 11
        passed: true
      fact Sample:
        number: 12
        passed: true
      fact Sample:
        number: 13
        passed: true
      fact Sample:
        number: 14
        passed: true
      fact Sample:
        number: 15
        passed: true
      fact Sample:
        number: 16
        passed: true
      fact Sample:
        number: 17
        passed: true
      fact Sample:
        number: 18
        passed: true
      fact Sample:
        number: 19
        passed: true
    expect:
      conclusion Ready:
        label: "complete"
    firings: 2
  test one_observation_fails:
    given:
      fact Sample:
        number: 19
        passed: false
    firings: 0

After Compile succeeds, save the following Python code as inspect_evidence.py in the same folder. Run it with the Python where Ting is installed. For the review environment, use .\ting-test\Scripts\python.exe inspect_evidence.py from that folder. The compiled program can be reopened without the readable source.

from pathlib import Path
from ting_reasoner import Engine, load_tingir

# Run in the folder where the IDE saved universal_evidence.tingir.
program = load_tingir(Path("universal_evidence.tingir"))
engine = Engine(program)
Sample = engine.schema("universal_checks.Sample")
handles = engine.declare(*(Sample(number=i, passed=True) for i in range(20)))
engine.run()
conclusion = engine.conclusions[0]
proofs = engine.explain(conclusion)
assert len(proofs) == 2
assert all(proof.fact_ids == tuple(h.id for h in handles) for proof in proofs)
print(conclusion.as_dict())  # {'label': 'complete'}
print([len(proof.fact_ids) for proof in proofs])  # [20, 20]

saved = engine.snapshot()
engine.replace(handles[-1], Sample(number=19, passed=False))
engine.run()
assert engine.conclusions == ()
assert len(saved.explain(conclusion)) == 2

The output is {'label': 'complete'} and [20, 20]. Each proof retains every supporting handle. Replacing an observation with a failed value withdraws the current conclusion; the saved snapshot keeps its earlier two proofs.

An empty schema satisfies a universal condition. To require observations, also add exists Sample: to the rule's when block. A handle may support several conditions and occur more than once. Equal immutable support lists may share storage, while rule identities, proof alternatives and ordering remain intact. Cache capacity does not truncate evidence.

Complete proofs and saved snapshots can be large. Keep only the snapshots your application needs. Numeric run limits cover numeric buffers; they are not a proof-storage or whole-process memory cap. Failed or stopped compilation preserves the previous compiled output.

Universal checks and counterexamples

A positive for_all condition checks every fact of its schema and keeps all ordered witness handles. A negated universal condition succeeds when one observation fails and records its first counterexample. The whole schema domain is checked; constraints do not implicitly choose a subgroup.

Save the complete source below as universal_validation.ting. Check validates it. Test runs three cases: an empty domain, passing observations and a failed observation. Compile creates universal_validation.tingir. The empty and all-pass cases produce Complete; the failure case produces ReviewNeeded.

ting source 1
requires semantics 1
module universal_validation version "1.0.0" role program

schemas:
  schema Sample:
    number: integer required
    passed: boolean required

conclusions:
  conclusion Complete:
    label: text required
  conclusion ReviewNeeded:
    label: text required

rules:
  rule complete:
    when:
      for_all Sample:
        passed: true
    then:
      conclude Complete:
        label: "complete"
  rule request_review:
    when:
      not:
        for_all Sample:
          passed: true
    then:
      conclude ReviewNeeded:
        label: "failed observation"

tests:
  test empty_domain:
    expect:
      conclusion Complete:
        label: "complete"
    firings: 1
  test all_pass:
    given:
      fact Sample:
        number: 1
        passed: true
      fact Sample:
        number: 2
        passed: true
    expect:
      conclusion Complete:
        label: "complete"
    firings: 1
  test failed_observation:
    given:
      fact Sample:
        number: 1
        passed: true
      fact Sample:
        number: 2
        passed: false
    expect:
      conclusion ReviewNeeded:
        label: "failed observation"
    firings: 1

Run the Python example below from the compiled file's folder. It examines the first failure, removes it, checks the next counterexample and retains an earlier explanation. After both failures are removed, all twenty-four passing observations support Complete.

from pathlib import Path
from ting_reasoner import Engine, load_tingir

program = load_tingir(Path("universal_validation.tingir"))
engine = Engine(program)
Sample = engine.schema("universal_validation.Sample")
engine.declare(*(Sample(number=i, passed=True) for i in range(24)))
failed = engine.declare(Sample(number=24, passed=False),
                        Sample(number=25, passed=False))
engine.run()
result = engine.conclusions[0]
assert result.name == "universal_validation.ReviewNeeded"
assert engine.explain(result)[0].fact_ids == (failed[0].id,)
print(result.as_dict())  # {'label': 'failed observation'}
saved = engine.snapshot()
engine.retract(failed[0])
engine.run()
assert engine.explain(engine.conclusions[0])[0].fact_ids == (failed[1].id,)
assert saved.explain(result)[0].fact_ids == (failed[0].id,)
engine.retract(failed[1])
engine.run()
assert engine.conclusions[0].name == "universal_validation.Complete"
assert len(engine.explain(engine.conclusions[0])[0].fact_ids) == 24
print(len(engine.explain(engine.conclusions[0])[0].fact_ids))  # 24

An empty domain satisfies the positive universal condition; its negation does not fire. Combine exists with a positive universal when at least one observation is required. Use an ordinary failed-observation match rule to report every failure individually.

Execution limits describe incomplete work, not a positive or negative universal answer. Check RunResult.status before treating a session as complete. Saved snapshots retain their original facts and proof records; large complete positive evidence still needs storage. Numeric memory controls retain their documented numeric-buffer scope.

Assurance and verification

Ting provides a scoped assurance profile for typed reference construction, snapshot index ownership, storage inspection and their verification controls. Requirements connect to design explanations, code paths, named tests, failure scenarios and unresolved issues. The checker produces reverse links so an implementation or test can be traced back to its requirement. These records support engineering assessment; they do not establish NASA certification or a deployment’s safety classification.

Check a delivered source release

Extract the source archive and install its declared dependencies. Run these commands from its root. The supplied baseline identifies the controlled files and required test inventory; verify it before interpreting results.

python -m tools.assurance
python -m tools.run_tests --output tests.json
python -m tools.assurance --results tests.json --output assurance.json

The first command checks profile links and configuration integrity. The second executes all required cases and records case IDs, outcomes, failure details, environment versions and loaded-package hashes. The third checks that evidence against the baseline. An error in one command must not be hidden by a later successful command. On headless Linux, set QT_QPA_PLATFORM=offscreen for GUI tests; this does not replace native desktop validation.

With --results, exit code 0 means consistent evidence and a passing complete required suite; 1 means configuration or evidence is invalid; 2 means required-suite acceptance is unmet. Without results, exit 0 means only structural and configuration checks passed. Skips, expected failures, missing cases and stale runtime bytes cannot be accepted. Neither a passing checker nor a content hash authenticates an untrusted producer or grants NASA approval.

Maintain application evidence

Keep the exact rule source, knowledge inputs, compiled program digest, Ting/dependency versions, run settings, expected outputs, actual outcomes and explanations together. Record changes and their effects. A reference constructor identifies an entity; only an explicit match or follow adds the target fact as a premise. Use the complete construction examples to see that distinction.

Applications must define their response to incomplete or conflicting decisions and failed operations. A work budget counts operations; it does not promise a deadline. Numerical-byte and graph-entry limits have stated coverage and do not bound whole-process memory. The entity storage report is a shallow diagnostic measurement, not byte admission or a whole-heap bound. Retain failed results and validate on the intended platform.

Standards and applicability

The profile applies selected traceability, configuration and testing controls from these references. Full compliance requires an intended-use assessment, applicable requirements and approved tailoring, complete product/system traceability, accountable authorities, appropriate safety evidence and independent assurance where required. For identified safety-critical components, ordinary unit or branch coverage does not demonstrate the required 100% Modified Condition/Decision Coverage (MC/DC).

NASA Software Engineering Handbook topic 7.25, Artificial Intelligence And Software Engineering, discusses deterministic verification. Topic 8.25, Artificial Intelligence And Software Assurance, addresses data-driven and agentic systems; its applicability depends on an application’s integration. The NASA insignia in the reference panel identifies the source agency only. Ting is independently developed.