npm package: @nexart/signals
Description: Minimal, protocol-agnostic signal capture SDK for optional CER context evidence
Latest version: 0.1.0

# @nexart/signals v0.1.0

Minimal, protocol-agnostic signal capture SDK.

Captures and normalizes structured upstream signals so they can be bound into **Certified Execution Records (CERs)** as optional context evidence.

- Does **not** define governance semantics or enforce policy
- Does **not** interpret the meaning of signals
- Validates structure only and provides normalization helpers
- Fully optional and independent of `@nexart/ai-execution`

---

## Install

```bash
npm install @nexart/signals
```

---

## Quick start

```ts
import { createSignal, createSignalCollector } from '@nexart/signals';

// ── Standalone signal ──────────────────────────────────────────────────────

const signal = createSignal({
  type: 'approval',
  source: 'github-actions',
  actor: 'ci-bot',
  status: 'ok',
  payload: { pr: 42, approved_by: 'alice' },
});

// {
//   type: 'approval',
//   source: 'github-actions',
//   step: 0,
//   timestamp: '2026-...',
//   actor: 'ci-bot',
//   status: 'ok',
//   payload: { pr: 42, approved_by: 'alice' }
// }


// ── Collected signals ──────────────────────────────────────────────────────

const collector = createSignalCollector({ defaultSource: 'my-pipeline' });

collector.add({ type: 'fetch',     source: 'my-pipeline', payload: { url: '...' } });
collector.add({ type: 'transform', source: 'my-pipeline', actor: 'etl-bot' });
collector.add({ type: 'store',     source: 'my-pipeline', status: 'ok' });

const collection = collector.export();
// {
//   signals: [
//     { type: 'fetch',     step: 0, ... },
//     { type: 'transform', step: 1, ... },
//     { type: 'store',     step: 2, ... },
//   ],
//   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 |

---

## API

### `createSignal(input)`

Creates a single normalized signal. `type` and `source` are required. All other fields are optional with safe defaults.

```ts
const signal = createSignal({
  type: 'review',
  source: 'linear',
  step: 2,
  actor: 'alice',
  status: 'ok',
  payload: { issue: 'NX-123', verdict: 'approved' },
});
```

### `createSignalCollector(options?)`

Creates an ordered signal buffer. Returns a `SignalCollector` with three members:

#### `collector.add(input)`

Adds a signal. Returns the normalized `NexArtSignal` that was stored.

- If `step` is omitted, it is auto-assigned using the insertion index (0, 1, 2, …)
- If `step` is explicitly provided, that value is used and the signal is sorted correctly on export

#### `collector.export()`

Returns a `SignalCollection`:

```ts
interface SignalCollection {
  signals:    NexArtSignal[];  // sorted by step (ascending)
  count:      number;
  exportedAt: string;          // ISO 8601, time of export() call
}
```

Non-destructive — calling `export()` multiple times is safe. The internal buffer is not cleared.

#### `collector.size`

Returns the current number of collected signals.

#### Collector options

```ts
interface CollectorOptions {
  defaultSource?: string;  // applied when signal source is empty/omitted
  defaultActor?:  string;  // applied when signal actor is omitted
}
```

---

## Step ordering

Signals without an explicit `step` get steps assigned in insertion order:

```ts
collector.add({ type: 'a', source: 's' });  // step = 0
collector.add({ type: 'b', source: 's' });  // step = 1
collector.add({ type: 'c', source: 's' });  // step = 2
```

Signals with an explicit `step` are sorted by that value on export, regardless of insertion order:

```ts
collector.add({ type: 'deploy', source: 's', step: 10 });
collector.add({ type: 'build',  source: 's', step: 0  });
collector.add({ type: 'test',   source: 's', step: 5  });

collector.export().signals.map(s => s.type);
// ['build', 'test', 'deploy']
```

---

## Determinism

Pin `timestamp` and `step` for deterministic output that can be hashed:

```ts
const signal = createSignal({
  type: 'approval',
  source: 'ci',
  step: 0,
  timestamp: '2026-03-17T00:00:00.000Z',  // pinned
  actor: 'bot',
  payload: { pr: 42 },
});
// same input → identical object every time
```

---

## Integration with @nexart/ai-execution (v0.10.0+)

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

```ts
import { createSignalCollector } from '@nexart/signals';
import { certifyDecision, certifyLangChainRun, 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();

// ── certifyDecision ──────────────────────────────────────────
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,  // ← sealed into certificateHash; omit = identical hash as before
});

verifyCer(bundle).ok;               // true
bundle.context?.signals.length;     // 2

// ── certifyLangChainRun ──────────────────────────────────────
const { bundle: lcBundle } = certifyLangChainRun({
  provider: 'openai',
  model: 'gpt-4o-mini',
  input: { messages: [{ role: 'user', content: 'Summarise.' }] },
  output: { text: 'Summary...' },
  signals,  // ← same field, same semantics
});
```

Signals are evidence-only — they do not affect execution behavior or parameters. See `@nexart/ai-execution` README for the full `CerContextSignal` shape and tamper-detection guarantees.

---

## Version history

| Version | Description |
|---|---|
| v0.1.0 | Initial release: `createSignal`, `createSignalCollector`, `NexArtSignal`, `SignalCollection`, `CollectorOptions`, `SignalCollector` types |