npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nexart/signals

v0.8.2

Published

Minimal, protocol-agnostic signal capture SDK for optional CER context evidence

Readme

@nexart/signals v0.8.0

Protocol-agnostic structured execution context SDK with deterministic capture, integrity hashing, replay-safe diffing, a first-class ExecutionContext object, and a builder-friendly createContext() API designed to make agent execution context capture feel effortless.

Originally a minimal signal capture utility for binding upstream evidence into Certified Execution Records (CERs). v0.2 → v0.8 layered seven additive capability tiers on top of the same simple core, without breaking a single v0.1 user.

2-minute 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,
});

That's it. Auto-step, auto-timestamp, signals injected into the CER for you.

  • Does not define governance semantics or enforce policy
  • Does not interpret the meaning of signals
  • Validates structure only, normalizes to safe defaults, and (optionally) hashes for tamper-detection
  • Fully optional and independent of @nexart/ai-execution

Install

npm install @nexart/signals

Capability tiers

| Version | Layer | What it adds | |---|---|---| | v0.1 | Core | createSignal, createSignalCollector, normalization, defaults, ordered export | | v0.2 | Deterministic mode | deterministic: true option, lock(), validate() — no auto-generated values | | v0.3 | Context integrity | hashSignals(), validateSignals(), exportWithHash() — tamper-evident before certification | | v0.4 | Structured context | findByType, findByStep, filter, diffSignals() — signals as queryable execution context | | v0.5 | Execution Context object | createExecutionContext() — first-class wrapper with validate, equals, toJSON, immutable by default | | v0.6 | Snapshot + replay | exportContext, importContext (with tamper-detection), compareContexts | | v0.7 | Integrity contract | assertContextDeterministic, freezeContext (deep), context.signaturePayload(), context.summary() | | v0.8 | Builder DX layer | createContext() + step, wrap, start/Span, input/output/tool/decision, certify, debug, print |

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


Quick start (v0.1 — still works)

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-...' }

Signal shape

Every NexArtSignal has exactly these fields — all always present, no undefined values:

| Field | Type | Default | Description | |---|---|---|---| | type | string | required | Signal category — free-form (e.g. "approval", "deploy", "audit") | | source | string | required | Upstream system or protocol — free-form (e.g. "github-actions", "linear") | | step | number | 0 / auto | Position in sequence. Auto-assigned in insertion order by the collector | | 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", "skipped") | | payload | Record<string, unknown> | {} | Opaque upstream data — NexArt does not interpret this |


v0.2 — Deterministic mode

Opt in to fully deterministic signal collection. Same code, same inputs → bit-identical signals every run, every machine.

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.
  • actor, status, payload defaults still apply — they are caller-controlled and don't vary across runs.

collector.lock()

Freeze the collector to guarantee immutability before hashing or export:

collector.lock();
collector.add({ /* ... */ });   // throws: 'cannot add() after lock()'

collector.export();             // still works
collector.exportWithHash();     // still works
collector.validate();           // still works
collector.locked;               // true

lock() is idempotent.

collector.validate()

Check the current buffer for structural integrity before binding into a CER:

const result = collector.validate();
// { ok: boolean, errors: string[] }

Checks:

  • All required fields present
  • step is a finite number
  • timestamp is a parseable date
  • payload is a plain object
  • No two signals share the same step

v0.3 — Context integrity

Make signal collections tamper-evident before certification.

hashSignals(signals)

Deterministic content hash over an array of signals:

import { hashSignals } from '@nexart/signals';

const contextHash = hashSignals(collection.signals);
// 'sha256:9f3c...' — stable across environments

Algorithm:

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

The hash excludes exportedAt and any wall-clock metadata. Same signals → same hash, regardless of insertion order or environment.

validateSignals(signals)

Standalone validation for any NexArtSignal[] (e.g. signals received from another process or restored from disk):

import { validateSignals } from '@nexart/signals';

const { ok, errors } = validateSignals(restoredSignals);
if (!ok) throw new Error(errors.join('; '));

collector.exportWithHash()

Export + hash in a single call:

const { signals, count, exportedAt, contextHash } = collector.exportWithHash();
// contextHash === hashSignals(signals)

v0.4 — Structured context

Signals as a queryable, diffable execution context — not just logs.

Query helpers

collector.findByType('approval');
// → NexArtSignal[]  (sorted by step)

collector.findByStep(2);
// → NexArtSignal[]  (one element per matching step)

collector.filter({ type: 'deploy', status: 'ok' });
// → NexArtSignal[]  (top-level field match, sorted by step)

collector.filter((s) => s.payload.severity === 'high');
// → NexArtSignal[]  (custom predicate)

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

diffSignals(a, b)

Compare two signal collections, matched by step:

import { diffSignals } from '@nexart/signals';

const before = previousRun.signals;
const after  = currentRun.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
//          → { step, before, after }

All output arrays are sorted by step ascending. Use validateSignals() first if you need to enforce unique steps in either input.


Execution Context (v0.5+)

Once you have a signal collection, the next layer up is a first-class ExecutionContext — a sorted, hashed, validatable, portable object that represents the canonical evidence bundle for a single piece of execution work.

import { createExecutionContext } from '@nexart/signals';

const collector = createSignalCollector({ deterministic: true });
collector.add({ type: 'approval', source: 'gh', step: 0, timestamp: '2026-03-17T00:00:00.000Z' });
collector.add({ type: 'deploy',   source: 'ci', step: 1, timestamp: '2026-03-17T00:01:00.000Z' });

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 — same as exportContext(ctx)

What contextHash means

contextHash is a sha256:<64-hex> digest computed from the canonical serialization of the signals array — sorted by step, with object keys sorted alphabetically at every level (including nested payload), undefined stripped, no whitespace.

It does not include createdAt or any wall-clock metadata. Two contexts with the same signals produce the same contextHash, regardless of when or where they were built. This is the property that makes context bindable to execution: the certifying record (CER, signed envelope, audit log entry, etc.) commits to contextHash, and any downstream verifier can reconstruct it from the signals alone.

How context binds to execution

The intended pattern:

  1. Capture signals during execution 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 alongside the actual outputs.
  5. To verify: reconstruct the context from stored signals and check hashSignals(signals) === storedContextHash.

The signals package is passive — it never influences execution. It only describes it, in a form that survives travel and can be re-checked.


Snapshot, replay, and comparison (v0.6+)

import { exportContext, importContext, compareContexts } from '@nexart/signals';

// Persist
const snapshot = exportContext(ctx);
fs.writeFileSync('ctx.json', JSON.stringify(snapshot));

// Replay
const restored = importContext(fs.readFileSync('ctx.json', 'utf8'));
restored.equals(ctx);     // true — identity preserved across the wire

// Compare
const r = compareContexts(ctx, restored);
// {
//   equal:     true,        // strict canonical equality
//   hashEqual: true,        // contextHash match (cheapest check)
//   diff:      { added: [], removed: [], changed: [] },  // structured diff
// }

importContext performs tamper-detection automatically: it recomputes the hash from the snapshot's signals and throws contextHash mismatch if it doesn't match the stored contextHash. Round-tripping a clean snapshot always preserves identity.

Replay use cases

  • Reproduce a past run: persist the context snapshot alongside the output; on rerun, import and compare.
  • Audit trail comparison: compareContexts(yesterdayCtx, todayCtx) to see which signals changed between runs.
  • Cross-environment verification: ship a snapshot from prod to a staging verifier; tamper-detection ensures fidelity.
  • Pre-certification freeze: freezeContext() before passing to a CER builder so nothing can mutate between hash and signature.

Integrity contract (v0.7+)

Three primitives make the context safe to bind to a verifiable execution record:

assertContextDeterministic(ctx)

Throws if the context fails the determinism contract:

  • structural validation (validateSignals) returns errors — missing fields, non-finite step, unparseable timestamp, malformed payload, duplicate steps
  • the stored contextHash no longer matches the recomputed hash (i.e. signals were tampered with after construction)
assertContextDeterministic(ctx);   // throws on any violation

freezeContext(ctx)

Deep-freezes the signals array, every signal object, and every signal's payload (recursively). Idempotent. Returns the same context for chaining.

const ctx = freezeContext(createExecutionContext({ signals }));
ctx.signals[0].payload.x = 1;   // throws (in strict mode) — fully immutable

context.signaturePayload()

Returns a canonical string suitable for signing or CER binding. Includes contextHash and signals only — excludes createdAt, because the signature must depend on content alone.

const payload = ctx.signaturePayload();
const sig = sign(privateKey, payload);
// Persist { snapshot: ctx.toJSON(), signature: sig }

context.summary()

Lightweight overview useful for UI surfaces and verify layers:

ctx.summary();
// {
//   count:     2,
//   types:     { approval: 1, deploy: 1 },
//   stepRange: { min: 0, max: 1 },
// }

Builder API (v0.8) — createContext()

The new high-level entry point. Returns a mutable builder over a SignalCollector with the lowest possible cognitive load. Backward-compatible with everything below — createSignal, createSignalCollector, createExecutionContext, snapshots, diffs all still work unchanged.

Capture — step()

Two forms. Convenience first:

const ctx = createContext();
ctx.step('approval',  { actor: 'alice', approved: true });
ctx.step('deploy',    { env: 'prod' });
ctx.step('verify');

Power-user form (full CreateSignalInput) when you need explicit source, actor, or status:

ctx.step({
  type:   'review',
  source: 'github',
  actor:  'bob',
  status: 'pending',
  payload: { pr: 42 },
});

Inspection:

ctx.signals     // current signal array, sorted by step
ctx.hash        // canonical contextHash over the current signals
ctx.size        // count
ctx.locked      // boolean
ctx.lock()      // freeze the underlying collector
ctx.snapshot()  // → immutable ExecutionContext (v0.5)

Auto-instrumentation — 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 an end signal with status='error'

For more control, open a span explicitly:

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;
}

wrap() and start() are not available in deterministic mode (they record wall-clock duration). Use step() with explicit step + timestamp instead.

Zero-config mode

const ctx = createContext({ auto: true });

The auto: true flag is purely declarative — auto-step, auto-timestamp, default actor/source are the default behavior already. The flag exists so the intent is obvious in code review.

Agent-friendly semantic helpers

Lightweight wrappers around step(). They do not enforce meaning or add validation rules — purely convenience for common AI/agent patterns:

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

Emits signals with type of input, tool, decision, output respectively, with the supplied data placed in the payload.

Direct certification — ctx.certify()

One call. Signals are injected automatically:

const bundle = await ctx.certify({
  provider:   'openai',
  model:      'gpt-4o-mini',
  input,
  output,
  parameters: { temperature: 0 },
});

Internally lazily imports @nexart/ai-execution and calls certifyDecision({ ...input, signals: ctx.signals }). The signals package itself has no hard dependency on @nexart/ai-execution — install it as a peer when you need certification.

For tests or full decoupling, pass an injectable 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 });

Debugging UX

Structured view:

const view = ctx.debug();
// {
//   count:    3,
//   hash:     'sha256:...',
//   types:    { fetch: 1, transform: 1, store: 1 },
//   timeline: [{ step, type, timestamp, status, actor, source }, ...],
// }

Human-readable timeline (logs to console and returns the string):

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
//     [  1] 2026-04-26T...  transform             status=ok  actor=agent  source=agent
//     [  2] 2026-04-26T...  store                 status=ok  actor=agent  source=agent

Progressive complexity

| Layer | Use when | |---|---| | createContext() + step() | You're building an agent / pipeline and want minimum boilerplate | | + wrap() / start() | You want auto-duration and status capture | | + certify() | You're emitting CERs and want signals injected automatically | | createSignalCollector({ deterministic: true }) + createExecutionContext | You need bit-for-bit reproducible context for verification / replay |

The deterministic mode and the builder are not in tension — createContext({ deterministic: true }) is a valid combination. It just disables the wall-clock helpers (wrap, start) and forces explicit step + timestamp on every step() call.


Replay-safe signals — full example

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}`);

API reference

Functions

| Symbol | Since | Description | |---|---|---| | createSignal(input) | v0.1 | Normalize a single CreateSignalInput into a NexArtSignal | | createSignalCollector(options?) | v0.1 | Build a SignalCollector | | hashSignals(signals) | v0.3 | Deterministic sha256:<hex> over a signal array | | canonicalJson(value) | v0.3 | Sorted-key, undefined-stripped JSON serialization | | canonicalize(value) | v0.3 | Recursive key-sorted clone of a value | | sortSignals(signals) | v0.3 | Stable sort by step (does not mutate) | | validateSignals(signals) | v0.3 | Structural integrity check returning { ok, errors } | | diffSignals(a, b) | v0.4 | Step-keyed diff returning { added, removed, changed } | | createExecutionContext(input) | v0.5 | Build a frozen ExecutionContext from signals | | exportContext(ctx) | v0.6 | Canonical snapshot — same as ctx.toJSON() | | importContext(json) | v0.6 | Reconstruct a context 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. Idempotent | | createContext(options?) | v0.8 | Builder-friendly mutable context (DX layer) | | 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. Emits <type>.start and <type>.end (with status, duration_ms). Re-throws errors. Disabled in deterministic mode. | | start(type, payload?) → Span | Open a span. span.end({ status?, payload? }) emits the end signal. 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. Accepts { certifier } for testing. | | signals / hash / size / locked | Live read-only views of the underlying collector. | | lock() | Lock the collector — no further mutations. | | snapshot() → ExecutionContext | Build the immutable v0.5 ExecutionContext. | | debug() → ContextDebugView | { count, hash, types, timeline }. | | print() → string | Pretty-print the timeline. Logs and returns the string. |

ExecutionContext methods

| Method | Since | Description | |---|---|---| | validate() | v0.5 | validateSignals() over the underlying signals | | equals(other) | v0.5 | Hash-based equality with another context | | toJSON() | v0.5 | Canonical ContextSnapshot{ signals, contextHash, createdAt } | | signaturePayload() | v0.7 | Canonical signing payload, excludes createdAt | | summary() | v0.7 | { count, types, stepRange } |

Collector methods

| Method / property | Since | Description | |---|---|---| | add(input) | v0.1 | Append a signal. Throws after lock(). Throws on missing required fields in deterministic mode. | | export() | v0.1 | { signals, count, exportedAt } — sorted by step | | exportWithHash() | v0.3 | Same as export() plus contextHash | | validate() | v0.2 | Check current buffer; returns { ok, errors } | | lock() | v0.2 | Freeze collector. Idempotent. | | findByType(type) | v0.4 | Signals matching type, sorted by step | | findByStep(step) | v0.4 | Signals matching step | | filter(predicate) | v0.4 | Predicate object (top-level match) or function | | size | v0.1 | Current signal count | | locked | v0.2 | true after lock() |

Types

NexArtSignal, CreateSignalInput, CollectorOptions, SignalCollection, SignalCollector, SignalCollectionWithHash, SignalsValidationResult, SignalChange, SignalsDiff, SignalFilterPredicate, ExecutionContext, CreateExecutionContextInput, ContextSnapshot, ContextComparison, ContextSummary, ContextBuilder, ContextBuilderOptions, ContextCertifyInput, ContextCertifyOptions, ContextDebugView, ContextTimelineEntry, Span.


Integrity model

| Property | Guarantee | |---|---| | Passive | Signals describe execution. They never influence it. | | Deterministic-capable | With deterministic: true + pinned timestamps, same input → identical signals → 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. |


Integration with @nexart/ai-execution

NexArtSignal[] is structurally identical to CerContextSignal[] in @nexart/ai-execution — no casting or conversion needed. Pass collection.signals directly to any certify call and it will be sealed into the certificateHash alongside the execution record.

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).


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
  • add(), export(), size work exactly as in v0.1
  • All defaults (step: 0/auto, actor: "unknown", status: "ok", payload: {}) unchanged
  • defaultSource and defaultActor collector options unchanged

The new methods (exportWithHash, validate, lock, findByType, findByStep, filter, locked) are pure additions on the same returned object. Existing code that only uses v0.1 methods is byte-for-byte equivalent in behavior.


Version history

| Version | Description | |---|---| | v0.8.0 | Phases 7–12 (DX layer): createContext() + step/wrap/start/input/output/tool/decision/certify/debug/print | | v0.7.0 | Phase 6: assertContextDeterministic, freezeContext, context.signaturePayload(), context.summary() | | v0.6.0 | Phase 5: exportContext, importContext (with tamper-detection), compareContexts | | v0.5.0 | Phase 4: createExecutionContext, ExecutionContext.{validate,equals,toJSON}, immutable-by-default | | v0.4.0 | Phase 3: findByType, findByStep, filter, diffSignals, normalization guarantees | | v0.3.0 | Phase 2: hashSignals, canonicalJson, validateSignals, exportWithHash, sortSignals | | v0.2.0 | Phase 1: deterministic mode, lock(), validate(), locked | | v0.1.0 | Initial release: core capture, normalization, defaults, ordered export |