# Signals SDK Reference (@nexart/signals)

URL: https://docs.nexart.io/docs/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.

| Version | Layer | What it adds |
| --- | --- | --- |
| v0.1 | Core | createSignal, createSignalCollector, normalization, defaults, ordered export |
| v0.2 | Deterministic mode | deterministic: true, lock(), validate() |
| v0.3 | Context integrity | hashSignals(), validateSignals(), exportWithHash() |
| v0.4 | Structured context | findByType, findByStep, filter, diffSignals() |
| v0.5 | Execution Context object | createExecutionContext() with validate, equals, toJSON |
| v0.6 | Snapshot + replay | exportContext, importContext (tamper-detection), compareContexts |
| v0.7 | Integrity contract | assertContextDeterministic, freezeContext, signaturePayload(), summary() |
| v0.8 | Builder DX layer | createContext() + step, wrap, start, input, output, tool, decision, certify |

## Signal shape

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

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| type | string | required | Signal category, free-form (e.g. "approval", "deploy") |
| source | string | required | Upstream system or protocol, free-form |
| step | number | 0 / auto | Position in sequence. Auto-assigned in insertion order |
| timestamp | string | current time | ISO 8601 |
| actor | string | "unknown" | Who produced this signal, free-form |
| status | string | "ok" | Outcome, free-form (e.g. "ok", "error", "pending") |
| payload | Record<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:

- Sort signals by `step` (ascending, stable).
- Canonicalize each signal: object keys sorted alphabetically at every level.
- Serialize as canonical JSON.
- 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

- Capture signals via `createSignalCollector({ deterministic: true })`.
- `collector.lock()` and build an `ExecutionContext` from `collector.export().signals`.
- Optional: `freezeContext(ctx)` for deep-immutability.
- Bind `ctx.contextHash` (or `ctx.signaturePayload()`) into your execution record.
- 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&#x27;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&#x27;s `certificateHash` covers the signals in canonical form, identical to (and independently checkable with) `hashSignals(signals)`.

## API reference

### Functions

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

### ContextBuilder methods (v0.8)

| Method / property | Description |
| --- | --- |
| 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?) → Span | Open 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 / locked | Live read-only views of the underlying collector. |
| lock() | Lock the collector. |
| snapshot() → ExecutionContext | Build the immutable v0.5 ExecutionContext. |
| debug() → ContextDebugView | { count, hash, types, timeline } |
| print() → string | Pretty-print the timeline. |

### ExecutionContext methods

| Method | Since | Description |
| --- | --- | --- |
| validate() | v0.5 | validateSignals() over the underlying signals |
| equals(other) | v0.5 | Hash-based equality |
| toJSON() | v0.5 | Canonical ContextSnapshot |
| signaturePayload() | v0.7 | Canonical 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.

## Related

- Execution Context and Signals: how signals attach to a CER.
- Context Signals: protocol-level concept.
- AI Execution SDK: `certifyDecision` and the `signals` parameter.
- Verification Semantics: how hash-bound vs supplemental signals are reported.
