nexart.iodocs

    Signals SDK

    @nexart/signals v0.8.2 — protocol-agnostic structured execution context with deterministic capture, integrity hashing, replay-safe diffing, and a builder API.

    Overview

    @nexart/signals captures structured execution context as an ordered, hashable, replay-safe array of signals. It is protocol-agnostic, fully optional, and independent of @nexart/ai-execution.

    • Does not define governance semantics or enforce policy.
    • Does not interpret the meaning of signals.
    • Validates structure, normalizes to safe defaults, and (optionally) hashes for tamper-detection.

    For how signals attach to a CER, see Execution Context and Signals and Context Signals.

    Install

    Install
    npm install @nexart/signals

    2-minute quickstart

    Builder quickstart
    import { createContext } from '@nexart/signals';
    
    const ctx = createContext();
    
    ctx.step('fetch', { url });
    ctx.step('transform');
    ctx.step('store');
    
    await ctx.certify({
      provider: 'openai',
      model:    'gpt-4o-mini',
      input,
      output,
    });

    Auto-step, auto-timestamp, signals injected into the CER for you. @nexart/ai-execution is a peer dependency for ctx.certify().

    Capability tiers

    All layers are additive. v0.1 code keeps working unchanged.

    VersionLayerWhat it adds
    v0.1CorecreateSignal, createSignalCollector, normalization, defaults, ordered export
    v0.2Deterministic modedeterministic: true, lock(), validate()
    v0.3Context integrityhashSignals(), validateSignals(), exportWithHash()
    v0.4Structured contextfindByType, findByStep, filter, diffSignals()
    v0.5Execution Context objectcreateExecutionContext() with validate, equals, toJSON
    v0.6Snapshot + replayexportContext, importContext (tamper-detection), compareContexts
    v0.7Integrity contractassertContextDeterministic, freezeContext, signaturePayload(), summary()
    v0.8Builder DX layercreateContext() + step, wrap, start, input, output, tool, decision, certify

    Signal shape

    Every NexArtSignal has exactly these fields. All fields are always present, no undefined values.

    FieldTypeDefaultDescription
    typestringrequiredSignal category, free-form (e.g. "approval", "deploy")
    sourcestringrequiredUpstream system or protocol, free-form
    stepnumber0 / autoPosition in sequence. Auto-assigned in insertion order
    timestampstringcurrent timeISO 8601
    actorstring"unknown"Who produced this signal, free-form
    statusstring"ok"Outcome, free-form (e.g. "ok", "error", "pending")
    payloadRecord<string, unknown>{}Opaque upstream data, NexArt does not interpret this

    v0.1 Core — createSignalCollector

    The original capture API. Still works unchanged.

    Core collector
    import { createSignal, createSignalCollector } from '@nexart/signals';
    
    const collector = createSignalCollector({ defaultSource: 'my-pipeline' });
    
    collector.add({ type: 'fetch',     payload: { url: '...' } });
    collector.add({ type: 'transform', actor: 'etl-bot' });
    collector.add({ type: 'store',     status: 'ok' });
    
    const collection = collector.export();
    // { signals: [...], count: 3, exportedAt: '2026-...' }

    v0.2 Deterministic mode

    Same code, same inputs, bit-identical signals every run.

    Deterministic collector
    const collector = createSignalCollector({ deterministic: true });
    
    collector.add({
      type: 'approval',
      source: 'ci',
      step: 0,                                  // required in deterministic mode
      timestamp: '2026-03-17T00:00:00.000Z',    // required in deterministic mode
    });

    In deterministic mode:

    • step MUST be supplied explicitly on every add(). No insertion-index fallback.
    • timestamp MUST be supplied explicitly on every add(). No Date.now() fallback.
    • add() throws synchronously when these constraints are violated.

    collector.lock()

    Freeze the collector to guarantee immutability before hashing or export. Idempotent.

    Lock
    collector.lock();
    collector.add({ /* ... */ });   // throws: 'cannot add() after lock()'
    
    collector.export();             // still works
    collector.exportWithHash();     // still works
    collector.validate();           // still works
    collector.locked;               // true

    collector.validate()

    Check the current buffer for structural integrity. Returns { ok, errors }. Verifies required fields, finite step, parseable timestamp, plain-object payload, no duplicate steps.

    v0.3 Context integrity

    Make signal collections tamper-evident before certification.

    Hashing and validation
    import { hashSignals, validateSignals } from '@nexart/signals';
    
    const contextHash = hashSignals(collection.signals);
    // 'sha256:9f3c...' — stable across environments
    
    const { ok, errors } = validateSignals(restoredSignals);
    if (!ok) throw new Error(errors.join('; '));
    
    // Or both at once:
    const { signals, count, exportedAt, contextHash: h } = collector.exportWithHash();

    Hash algorithm:

    1. Sort signals by step (ascending, stable).
    2. Canonicalize each signal: object keys sorted alphabetically at every level.
    3. Serialize as canonical JSON.
    4. sha256 to sha256:<64-hex>.

    The hash excludes exportedAt and any wall-clock metadata.

    v0.4 Structured context

    Signals as a queryable, diffable execution context.

    Query helpers
    collector.findByType('approval');
    collector.findByStep(2);
    collector.filter({ type: 'deploy', status: 'ok' });
    collector.filter((s) => s.payload.severity === 'high');

    The predicate-object form matches strict equality on top-level fields only. For payload-aware matching, use the function form.

    diffSignals
    import { diffSignals } from '@nexart/signals';
    
    const { added, removed, changed } = diffSignals(before, after);
    // added:   signals in 'b' whose step is not in 'a'
    // removed: signals in 'a' whose step is not in 'b'
    // changed: signals at the same step whose canonical content differs

    v0.5 Execution Context object

    A first-class wrapper: sorted, hashed, validatable, portable, immutable by default.

    createExecutionContext
    import { createExecutionContext } from '@nexart/signals';
    
    const ctx = createExecutionContext({ signals: collector.export().signals });
    
    ctx.signals;        // sorted by step, frozen array
    ctx.contextHash;    // 'sha256:...' — stable across runs and machines
    ctx.createdAt;      // ISO 8601 of construction
    ctx.validate();     // { ok, errors }
    ctx.equals(other);  // hash-based equality
    ctx.toJSON();       // canonical, portable snapshot

    How context binds to execution

    1. Capture signals via createSignalCollector({ deterministic: true }).
    2. collector.lock() and build an ExecutionContext from collector.export().signals.
    3. Optional: freezeContext(ctx) for deep-immutability.
    4. Bind ctx.contextHash (or ctx.signaturePayload()) into your execution record.
    5. To verify: reconstruct from stored signals and check hashSignals(signals) === storedContextHash.

    v0.6 Snapshot and replay

    Snapshot, import, compare
    import { exportContext, importContext, compareContexts } from '@nexart/signals';
    
    // Persist
    const snapshot = exportContext(ctx);
    fs.writeFileSync('ctx.json', JSON.stringify(snapshot));
    
    // Replay (tamper-detection runs automatically)
    const restored = importContext(fs.readFileSync('ctx.json', 'utf8'));
    restored.equals(ctx);     // true
    
    // Compare
    const r = compareContexts(ctx, restored);
    // { equal, hashEqual, diff: { added, removed, changed } }

    importContext recomputes the hash from the snapshot's signals and throws contextHash mismatch if it does not match the stored hash.

    v0.7 Integrity contract

    Integrity primitives
    import {
      assertContextDeterministic,
      freezeContext,
    } from '@nexart/signals';
    
    // Throws on validation errors or hash mismatch
    assertContextDeterministic(ctx);
    
    // Deep-freeze signals + payloads (recursive). Idempotent.
    const frozen = freezeContext(ctx);
    
    // Canonical signing payload — excludes createdAt
    const payload = ctx.signaturePayload();
    
    // Lightweight overview for UI surfaces
    ctx.summary();
    // { count, types: { approval: 1, deploy: 1 }, stepRange: { min, max } }

    v0.8 Builder API — createContext()

    The high-level entry point. Backward-compatible with everything below.

    Capture with step()

    step
    const ctx = createContext();
    
    // Convenience form
    ctx.step('approval', { actor: 'alice', approved: true });
    ctx.step('deploy',   { env: 'prod' });
    ctx.step('verify');
    
    // Power-user form (full CreateSignalInput)
    ctx.step({
      type:   'review',
      source: 'github',
      actor:  'bob',
      status: 'pending',
      payload: { pr: 42 },
    });
    
    ctx.signals     // current signal array, sorted by step
    ctx.hash        // canonical contextHash
    ctx.size        // count
    ctx.locked      // boolean
    ctx.lock()      // freeze the underlying collector
    ctx.snapshot()  // → immutable ExecutionContext (v0.5)

    Auto-instrumentation: wrap() and start()

    wrap and start
    const result = await ctx.wrap('llm_call', async () => {
      return await openai.chat(...);
    });
    // emits llm_call.start, then llm_call.end with { status, duration_ms }
    // re-throws errors after emitting end with status='error'
    
    // Manual span
    const span = ctx.start('tool_call', { name: 'search' });
    try {
      const results = await search(query);
      span.end({ status: 'ok', payload: { count: results.length } });
    } catch (e) {
      span.end({ status: 'error' });
      throw e;
    }
    Not available in deterministic mode
    wrap() and start() record wall-clock duration and are disabled in deterministic mode. Use step() with explicit step + timestamp instead.

    Agent-friendly helpers

    Lightweight wrappers around step(). They do not enforce meaning.

    Semantic helpers
    ctx.input({ q: 'who is the user' });
    ctx.tool('search', { query: 'foo' });
    ctx.decision('route', { selected: 'fraud-check' });
    ctx.output({ answer: '...' });

    Direct certification: ctx.certify()

    ctx.certify
    const bundle = await ctx.certify({
      provider:   'openai',
      model:      'gpt-4o-mini',
      input,
      output,
      parameters: { temperature: 0, maxTokens: 1024, topP: null, seed: null },
    });
    
    // For tests or full decoupling, inject a custom certifier
    await ctx.certify(
      { provider, model, input, output },
      { certifier: (params) => myCustomCertify(params) },
    );
    
    // Optional pre-freeze for tamper-evident handoff
    await ctx.certify(decisionInput, { freeze: true });

    Internally, ctx.certify() lazily imports @nexart/ai-execution and calls certifyDecision({ ...input, signals: ctx.signals }). Install @nexart/ai-execution as a peer when you need certification.

    Debugging

    debug() and print()
    const view = ctx.debug();
    // {
    //   count, hash, types: { fetch: 1, ... },
    //   timeline: [{ step, type, timestamp, status, actor, source }, ...]
    // }
    
    ctx.print();
    // ExecutionContext (3 signals)
    //   hash: sha256:...
    //   types: fetch=1, transform=1, store=1
    //   timeline:
    //     [  0] 2026-04-26T...  fetch      status=ok  actor=agent  source=agent

    Replay-safe signals — full example

    End-to-end replay-safe capture
    import {
      createSignalCollector,
      hashSignals,
      validateSignals,
      diffSignals,
    } from '@nexart/signals';
    
    // ── Capture ────────────────────────────────────────────────
    const c = createSignalCollector({ deterministic: true });
    
    c.add({ type: 'approval', source: 'gh', step: 0, timestamp: '2026-03-17T00:00:00.000Z', actor: 'alice', payload: { pr: 42 } });
    c.add({ type: 'deploy',   source: 'ci', step: 1, timestamp: '2026-03-17T00:01:00.000Z', actor: 'ci-bot', payload: { env: 'prod' } });
    
    c.lock();
    
    const v = c.validate();
    if (!v.ok) throw new Error(v.errors.join('; '));
    
    const { signals, contextHash } = c.exportWithHash();
    
    // ── Persist 'signals' and 'contextHash' somewhere ──────────
    
    // ── Later: verify nothing was tampered with ────────────────
    if (hashSignals(signals) !== contextHash) {
      throw new Error('Signal collection has been tampered with');
    }
    
    // ── Compare against a prior run ────────────────────────────
    const diff = diffSignals(previousSignals, signals);
    console.log(`+${diff.added.length} -${diff.removed.length} ~${diff.changed.length}`);

    Integration with @nexart/ai-execution

    NexArtSignal[] is structurally identical to CerContextSignal[] in @nexart/ai-execution. No casting needed.

    Direct interop
    import { createSignalCollector } from '@nexart/signals';
    import { certifyDecision, verifyCer } from '@nexart/ai-execution';
    
    const collector = createSignalCollector({ defaultSource: 'github-actions' });
    collector.add({ type: 'approval', actor: 'alice', status: 'ok', payload: { pr: 42 } });
    collector.add({ type: 'deploy',   actor: 'ci-bot', status: 'ok', payload: { env: 'prod' } });
    
    const { signals } = collector.export();
    
    const bundle = certifyDecision({
      provider: 'openai',
      model: 'gpt-4o-mini',
      prompt: 'Summarise.',
      input: userQuery,
      output: llmResponse,
      parameters: { temperature: 0, maxTokens: 512, topP: null, seed: null },
      signals,
    });
    
    verifyCer(bundle).ok;            // true
    bundle.context?.signals.length;  // 2

    The CER's certificateHash covers the signals in canonical form, identical to (and independently checkable with) hashSignals(signals).

    API reference

    Functions

    SymbolSinceDescription
    createSignal(input)v0.1Normalize a single CreateSignalInput
    createSignalCollector(options?)v0.1Build a SignalCollector
    hashSignals(signals)v0.3Deterministic sha256 over a signal array
    canonicalJson(value)v0.3Sorted-key, undefined-stripped JSON
    canonicalize(value)v0.3Recursive key-sorted clone
    sortSignals(signals)v0.3Stable sort by step (does not mutate)
    validateSignals(signals)v0.3Structural integrity check
    diffSignals(a, b)v0.4Step-keyed diff
    createExecutionContext(input)v0.5Build a frozen ExecutionContext
    exportContext(ctx)v0.6Canonical snapshot
    importContext(json)v0.6Reconstruct with hash tamper-detection
    compareContexts(a, b)v0.6{ equal, hashEqual, diff }
    assertContextDeterministic(ctx)v0.7Throws on invalid signals or hash mismatch
    freezeContext(ctx)v0.7Deep-freeze signals + payloads
    createContext(options?)v0.8Builder-friendly mutable context
    SIGNALS_VERSIONv0.1Package version string constant

    ContextBuilder methods (v0.8)

    Method / propertyDescription
    step(type, payload?) / step(input)Add a signal. Auto-step + auto-timestamp by default.
    wrap(type, fn)Auto-instrument an async fn. Disabled in deterministic mode.
    start(type, payload?) → SpanOpen a span. Disabled in deterministic mode.
    input(data) / output(data)Convenience for step('input'/'output', { data }).
    tool(name, payload?)Convenience for step('tool', { name, ...payload }).
    decision(name, payload?)Convenience for step('decision', { name, ...payload }).
    certify(input, options?)Lazily calls @nexart/ai-execution.certifyDecision, injecting signals.
    signals / hash / size / lockedLive read-only views of the underlying collector.
    lock()Lock the collector.
    snapshot() → ExecutionContextBuild the immutable v0.5 ExecutionContext.
    debug() → ContextDebugView{ count, hash, types, timeline }
    print() → stringPretty-print the timeline.

    ExecutionContext methods

    MethodSinceDescription
    validate()v0.5validateSignals() over the underlying signals
    equals(other)v0.5Hash-based equality
    toJSON()v0.5Canonical ContextSnapshot
    signaturePayload()v0.7Canonical signing payload, excludes createdAt
    summary()v0.7{ count, types, stepRange }

    Integrity model

    • Passive. Signals describe execution. They never influence it.
    • Deterministic-capable. With deterministic: true and pinned timestamps, same input produces identical signals and identical hash.
    • Protocol-agnostic. No business meaning, no policy engine, no framework coupling.
    • Composable. Works with any pipeline, agent, or workflow that produces or consumes a NexArtSignal[].
    • Tamper-evident. contextHash changes if any signal field changes anywhere.
    • Stable serialization. Canonical JSON (sorted keys at every level), undefined stripped, no whitespace.

    Backward compatibility

    Every v0.1 API and behavior is preserved exactly:

    • createSignal() and createSignalCollector() signatures unchanged.
    • All v0.1 fields (type, source, step, timestamp, actor, status, payload) unchanged.
    • All defaults unchanged.
    • New methods are pure additions on the same returned object.