Lace / docs

Interlang policy authoring

Lace policy describes selected checked record sets and whole-record exposure. Interlang is the normal human authoring surface. Lace lowers it to canonical Datalog for evaluation and exchange-plan identity; raw Datalog is the advanced surface, not the starting language.

The authoritative syntax and lowering rules are in Lace-120 · Interlang. The record-fact, Datalog, and exchange contracts remain in Lace-020, Lace-030, and Lace-040. This guide introduces the decisions an application author makes without duplicating those contracts.

Begin with one record family

A coordinate pattern constrains the Group, App, and Name fields of checked records. It does not identify one mutable row or bypass record validation.

converge {
  Task in //community//todo/task//task/{*} => include Task
}

This selects every checked record with:

Group = community
App   = todo/task
Name  = task/<non-empty path tail>

Use App for the record family and Name for its app-local names. Keep App exact when later clauses assume one record shape; an App-prefix wildcard also matches other contracts below that prefix.

The hash remains record identity, so several record versions may match one coordinate. {*} is the whole-field or final-tail wildcard; an unbraced * is literal data. Use the exact coordinate and wildcard rules from Lace-120 rather than treating coordinates as URL globs.

Start with one selected family and one deliberately valid non-match. Test the policy through list or between two memory participants before adding evidence, links, negation, ordering, or transport authority.

converge selects and exposes

A top-level converge entry contributes records to active exchange and exposes the same checked local records to peer-origin rules:

converge {
  Task in //community//todo/task//task/{*} => include Task
}

These are distinct effects:

Exposure alone does not transfer a record. Selection by one participant alone does not transfer it either: standard interlace uses the intersection of both policy operands.

Exposure is whole-record declassification. If peer-authored rules must not inspect a record’s facts, do not expose that record. Hiding or deselecting it later cannot erase bytes already copied.

Use expose for supporting inspection scope

Sometimes peer-origin policy must inspect checked evidence that should not be selected as a direct entry by that particular clause. Put that additional scope in an explicit expose block:

expose {
  Member in //community//membership//member/{*} => include Member

  converge {
    Task in //community//todo/task//task/{*} => include Task
  }
}

The direct Member entry contributes exposure only. The nested converge entries contribute active selection and exposure. This is a privacy boundary, not an optimization hint: review what a peer may infer from every exposed whole record.

Do not add a broad expose block by default. A simple top-level converge already exposes its selected checked records.

Gate records on checked evidence

Evidence is ordinary validated record data. Select evidence records too when a recipient must receive and validate them before dependent records become eligible.

writer_member(Member, writer_key) :=
  Member in //community//membership//member/{*}
  | Member.By == 'V.EXAMPLE_AUTHORITY_BY_B64A.H3'
  | Member.Role == 'writer'
  | Member.Member == writer_key

writer(writer_key) := writer_member(_, writer_key)

converge {
  writer_member(Member, _) => include Member

  Task in //community//todo/task//task/{*}
  | Task.By == writer_key
  | writer(writer_key)
  => include Task
}

Here a task depends on a checked local membership record associating its By value with the writer role. An advertisement is only a discovery claim; it cannot stand in for checked membership evidence. Converging the membership records allows a later round to validate the evidence locally and unlock matching tasks.

A valid Mark proves that the matching By-secret value produced the Mark over those exact record bytes. Application policy still decides what the record’s By value means; a Mark alone does not prove identity, authorship, truth, or authorization.

Follow hashes named by checked support

Use include ... by hash when checked local support already names the target hash, such as a manifest’s record-link field:

manifest(Manifest) :=
  Manifest in //files//manifest//manifest/{*}

chunk(Chunk) :=
  manifest(Manifest)
  | Manifest['Chunk+Link'](range) == Chunk

converge {
  manifest(Manifest) => include Manifest
  chunk(Chunk) => include Chunk by hash
}

The manifest is discovered from advertised fields. Once a checked manifest binds a chunk target hash, include Chunk by hash can select and request that exact target without pretending the missing chunk’s own fields are already known. Use ordinary include for records discovered from their advertised coordinate or fields.

See the chunk-manifest-closure scenario for a staged example.

Select current or bounded sets

Use a TopK binding clause over a candidate helper when policy needs a bounded set. Put every pre-ranking restriction and projected ordering field in that helper:

state_candidate(State, tai) :=
  State in //community//settings//state/{*}
  | State.By == 'V.EXAMPLE_AUTHORITY_BY_B64A.H3'
  | State.TAI == tai

current_state(State) :=
  greatest 1 from state_candidate(State, tai) by lex tai

Omitting per makes one global partition. Add explicit partitions for one winner per application key:

current_resource(Resource, name) :=
  greatest 1 per (name)
    from resource_candidate(Resource, name, tai)
    by lex tai

TopK selects a set; it does not order list results or promise transfer order, pagination, or a hidden coordinate winner. Equal primary keys use every complete candidate component in argument order as the deterministic tie-break. Candidate argument order therefore matters.

TAI is application-supplied ordering data, not proof of trusted wall-clock time. Choose which authority supplies it and constrain that authority in the candidate helper. A clause joined after TopK filters after ranking; restrictions that must affect the ranked population belong inside the candidate helper.

See the name-current-key-rotation scenario for current checked state gating newly advertised content, and latest-message-window for a bounded tracker window plus checked-support hash closure.

Practice in the scenario workbench

The scenario workbench shows policy source, records on each side, exposure, staged additions, events, and expected results. Useful starting scenarios are:

On the published site these source links are rewritten to the interactive workbench. In a source checkout, the JSON and Interlang templates are executable fixtures; follow scenario-tools/README.md to build and run the browser or Node harness.

For each policy, ask:

  1. Which exact checked records actively converge?
  2. Which evidence must already be checked locally?
  3. Which evidence must itself converge so the other side can check it?
  4. Which whole local records may peer-origin rules inspect?
  5. Which valid records must remain unselected as boundary cases?

Use raw Datalog only for an advanced boundary

Interlang already supports coordinate patterns, helpers, field conditions, checked evidence, links, common text/order/cardinality tests, exposure, and field- or hash-discovered inclusion. Prefer it for application policy.

Move to raw Lace Datalog when the required behavior genuinely needs explicit 040 facets, custom advertisement reasoning, or another lower-level surface that Interlang cannot express. Raw Datalog requires understanding:

Do not hand-author Datalog just to spell a coordinate selector in a more verbose form.

From policy source to exchange

local policy sources → one operand
one operand from each participant → exchange plan

A policy frame performs the first step. Interlace performs the second.

Both operands must select a record before it moves. Each Lace controls exposure of its own records.

The runtime bindings and C FFI expose the policy-frame step.

Validate before connecting peers

Keep the first policy loop local:

  1. create the smallest selected and unselected fixtures;
  2. run list against a memory Lace;
  3. run a direct memory interlace;
  4. inspect exposure and missing-evidence behavior;
  5. add durable storage; and only then
  6. add a connected transport.

The lace CLI provides explicit check interlang, frame checking, and finalization commands. Runtime bindings also expose standard-Interlang validation and policy-frame operations. Validation checks source and profile rules; executable scenarios prove that the selected set matches the application’s intent.

Return to Runtime and application integration to choose storage and transport, or run Your first interlace for the smallest complete policy/runtime example.