From a7426ad405ca9bdbedad6154bcaf6f1c6407383c Mon Sep 17 00:00:00 2001 From: sneakocom <192013763+sneakocom@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:44:02 +0000 Subject: [PATCH] feat: emit visible value receipts --- .github/workflows/ci.yml | 1 + ARCHITECTURE.md | 14 ++ CHANGELOG.md | 14 ++ README.md | 39 ++++- SPEC.md | 32 ++++ bin/context-firewall.js | 24 ++- fixtures/corpus.js | 24 ++- package.json | 3 +- src/reducer.js | 38 ++++- src/value-receipt.js | 312 +++++++++++++++++++++++++++++++++++++++ tests/reducer.test.js | 181 ++++++++++++++++++++++- 11 files changed, 670 insertions(+), 12 deletions(-) create mode 100644 src/value-receipt.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82f0715..df1a538 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,5 +17,6 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 + - run: npm run check - run: npm test - run: npm run conformance diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index d972838..cf2ab02 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,12 +15,20 @@ caller-owned raw stdout/stderr v v sufficient NEEDS_RAW_EVIDENCE packet packet/control error + | + +--> canonical packet (stdout/model channel) + | + +--> sibling value receipt (API/optional sidecar) + | + +--> [Context Firewall] indicator (stderr/operator channel) ``` ## Modules - `src/reducer.js`: validation, byte framing, parsing, classification, policy, hashing, measurement, and canonical serialization. +- `src/value-receipt.js`: dependency-free `opsle.value-receipt.v1` construction + and deterministic operator-indicator formatting. - `bin/context-firewall.js`: stdin/file CLI and deterministic conformance entry. - `fixtures/corpus.js`: synthetic public-safe fixture definitions and expected decisions. @@ -30,6 +38,12 @@ caller-owned raw stdout/stderr The core has no host adapter or external package dependency. Supporting Opsle protocols can consume the JSON fields without importing this package. +The value receipt is deliberately not embedded in the evidence packet. API +callers receive it as a sibling result and CLI callers may request a deterministic +sidecar. Canonical stdout therefore keeps the original compact model-context +boundary. The one named stderr indicator is operator telemetry; callers must not +merge it into model context automatically. + ## Evidence ownership The caller owns storage and addressability of raw bytes. The reducer binds those diff --git a/CHANGELOG.md b/CHANGELOG.md index c9318aa..a90eef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.3.0 - 2026-08-25 + +- Added a dependency-free sibling `opsle.value-receipt.v1` for reductions with + exact byte/event measurements and observed escalation/raw-locator state. +- Added `reduceWithValueReceipt()` and an optional deterministic + `--value-receipt` sidecar without increasing model-visible packet stdout. +- Added caller-supplied `--mechanism-revision` provenance that affects only the + receipt and never inspects ambient Git state. +- Added one named `[Context Firewall]` completion indicator on stderr plus + receipt, expansion, aggregation, trust, zero-baseline, channel-separation, + conformance, and static-check gates. +- This remains prototype evidence, not EXP-001 or a correctness, cost, latency, + token, or causal claim. + ## 0.2.0 - 2026-08-25 - Added a dependency-free deterministic TAP-subset reducer and CLI. diff --git a/README.md b/README.md index 7fe9a48..32c7760 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ safely not see?** This repository does not yet answer it. ## Prototype scope -Version 0.2.0 is a dependency-free Node.js reference reducer for a documented +Version 0.3.0 is a dependency-free Node.js reference reducer for a documented flat TAP-compatible test-output subset. It: - reads caller-supplied stdout and stderr bytes plus process metadata; @@ -24,6 +24,9 @@ flat TAP-compatible test-output subset. It: informational notes; - retains ambiguous or malformed evidence and requires raw-evidence escalation; - emits canonical JSON with source/configuration hashes and payload measurements; +- derives a sibling `opsle.value-receipt.v1` without adding it to model-visible + stdout; +- emits one named operator completion indicator on stderr; - applies deterministic payload ceilings without silently truncating critical evidence. @@ -116,6 +119,40 @@ calculate visible fraction and reduction ratio without parsing human logs. Runtime latency is intentionally absent from the hashed packet because it is nondeterministic; callers may measure it outside the packet. +## Visible Value receipt and operator channel + +`reduceWithValueReceipt(input, options)` returns `{ packet, valueReceipt }` while +`reduceTestRun(input, options)` remains packet-only. The value receipt uses +`opsle.value-receipt.v1` and exposes raw/model-visible bytes, signed initially +avoided bytes, an exact rational reduction ratio, original/retained/suppressed/ +ambiguous event counts, payload ceiling, escalation, and raw-locator state. +Byte evidence makes no token, cost, latency, correctness, or causal claim. + +The CLI always writes only the compact canonical evidence packet to stdout and +one named completion indicator to stderr: + +```text +[Context Firewall] 28,981 B -> 1,846 B | 27,135 B initially avoided (93.63%) | escalation: no +``` + +To persist the deterministic receipt separately, the caller may request a +sidecar: + +```bash +node ./bin/context-firewall.js reduce \ + --mechanism-revision REVISION \ + --value-receipt value-receipt.json +``` + +The caller must keep stderr and the optional receipt sidecar outside initial +decision-relevant model context. Invocation failures retain machine-readable +stderr and emit no success indicator. A negative avoided-byte delta is reported +as packet expansion rather than fabricated savings; ratios are not directly +summable. + +`--mechanism-revision` is caller supplied, affects only the sidecar, and defaults +to `null`; the deterministic reducer never inspects ambient Git state. + ## Deterministic retention policy Classification is strict and case-sensitive after ANSI is removed for parsing. diff --git a/SPEC.md b/SPEC.md index a590f4d..9b42866 100644 --- a/SPEC.md +++ b/SPEC.md @@ -44,6 +44,34 @@ Object keys are serialized in lexical order with one final newline. Arrays retai source order. No time, latency, random ID, filesystem state, locale, or ambient environment value enters canonical output. +## Visible Value contract + +The packet itself remains the compact model-visible output. A caller may derive a +sibling `opsle.value-receipt.v1` with `reduceWithValueReceipt()` or request a +canonical CLI sidecar with `--value-receipt`. The receipt is not embedded in the +packet and does not increase model-visible stdout. + +The mechanism identity is `opsle.context-firewall`, the operation is +`test-output-reduction`, and the receipt contains `raw_bytes`, +`initial_model_visible_bytes`, `bytes_initially_avoided`, +`initial_reduction_ratio`, `original_evidence_events`, +`retained_evidence_events`, `suppressed_evidence_events`, +`ambiguous_evidence_events`, `payload_ceiling_bytes`, `escalation_required`, and +`raw_locator_available`. + +Byte and event measurements are `EXACT`; escalation and raw-locator state are +`OBSERVED`. The avoided-byte delta is raw minus visible bytes and may be negative +for packet expansion. The ratio is an exact signed numerator/denominator string +and is not directly summable. A raw locator is caller supplied and is not proof +that the external artifact exists or was verified. Byte evidence supports no +token, cost, latency, correctness, or causal claim. + +An exact mechanism revision may be caller supplied. It affects only the sibling +receipt and defaults to `null`; ambient repository state is never inspected. +Successful CLI reductions write only the canonical packet to stdout and one +named `[Context Firewall]` indicator to stderr. Invocation failures retain their +machine-readable stderr behavior and emit no success indicator. + ## Evidence taxonomy Source lines have exactly one class: @@ -75,6 +103,10 @@ otherwise valid test names or explicit notes have no special meaning. sufficient packet. 7. `measurements.reduced_bytes` equals the canonical serialized packet length. 8. The input hash binds stream names, lengths, order, and exact bytes. +9. Value-receipt visible bytes equal the final serialized packet length; deriving + or writing the receipt never changes packet bytes. +10. Operator telemetry is derived from the completed sibling receipt and remains + outside canonical stdout. ## Payload policy diff --git a/bin/context-firewall.js b/bin/context-firewall.js index d3fef8f..6d02d8a 100755 --- a/bin/context-firewall.js +++ b/bin/context-firewall.js @@ -1,29 +1,36 @@ #!/usr/bin/env node -import { readFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import process from 'node:process'; import { conformanceReport } from '../fixtures/corpus.js'; import { InputError, PayloadCeilingError, canonicalJson, - reduceTestRun, + reduceWithValueReceipt, serializePacket, } from '../src/reducer.js'; +import { formatContextFirewallIndicator } from '../src/value-receipt.js'; function usage() { - return 'usage: context-firewall reduce [--input PATH|-] [--max-bytes N]\n context-firewall conformance\n'; + return 'usage: context-firewall reduce [--input PATH|-] [--max-bytes N] [--mechanism-revision REV] [--value-receipt PATH]\n context-firewall conformance\n'; } function parseReduceArgs(args) { let input = '-'; let maxOutputBytes = null; + let mechanismRevision = null; + let valueReceiptPath = null; for (let index = 0; index < args.length; index += 1) { if (args[index] === '--input' && args[index + 1]) input = args[++index]; else if (args[index] === '--max-bytes' && args[index + 1]) { maxOutputBytes = Number(args[++index]); + } else if (args[index] === '--mechanism-revision' && args[index + 1]) { + mechanismRevision = args[++index]; + } else if (args[index] === '--value-receipt' && args[index + 1]) { + valueReceiptPath = args[++index]; } else throw new InputError(`unknown or incomplete argument: ${args[index]}`); } - return { input, maxOutputBytes }; + return { input, maxOutputBytes, mechanismRevision, valueReceiptPath }; } async function readInput(path) { @@ -55,8 +62,15 @@ async function main() { } catch { throw new InputError('input must be valid JSON'); } - const packet = reduceTestRun(input, { maxOutputBytes: options.maxOutputBytes }); + const { packet, valueReceipt } = reduceWithValueReceipt(input, { + maxOutputBytes: options.maxOutputBytes, + mechanismRevision: options.mechanismRevision, + }); + if (options.valueReceiptPath) { + await writeFile(options.valueReceiptPath, `${canonicalJson(valueReceipt)}\n`, 'utf8'); + } process.stdout.write(serializePacket(packet)); + process.stderr.write(`${formatContextFirewallIndicator(valueReceipt)}\n`); } main().catch((error) => { diff --git a/fixtures/corpus.js b/fixtures/corpus.js index cb7c17e..d9a1a28 100644 --- a/fixtures/corpus.js +++ b/fixtures/corpus.js @@ -1,4 +1,10 @@ -import { INPUT_PROTOCOL, PayloadCeilingError, reduceTestRun, serializePacket } from '../src/reducer.js'; +import { + INPUT_PROTOCOL, + PayloadCeilingError, + reduceTestRun, + reduceWithValueReceipt, + serializePacket, +} from '../src/reducer.js'; function tap({ passed = [], failed = [], skipped = [], extras = [], finalNewline = true }) { const lines = ['TAP version 13']; @@ -91,7 +97,7 @@ export function executeFixture(fixture) { ? { maxOutputBytes: exactCeiling(fixture.input) } : (fixture.options ?? {}); try { - const packet = reduceTestRun(fixture.input, options); + const { packet, valueReceipt } = reduceWithValueReceipt(fixture.input, options); const bytes = serializePacket(packet); const evidence = packet.decision_evidence; const expected = fixture.expected; @@ -120,10 +126,20 @@ export function executeFixture(fixture) { ); if (options.maxOutputBytes != null) checks.push(bytes.length <= options.maxOutputBytes); checks.push(packet.receipt.measurements.reduced_bytes === bytes.length); + checks.push(valueReceipt.schema === 'opsle.value-receipt.v1'); + checks.push(valueReceipt.measurements.find( + (measurement) => measurement.id === 'raw_bytes', + )?.result === packet.receipt.measurements.original_bytes); + checks.push(valueReceipt.measurements.find( + (measurement) => measurement.id === 'initial_model_visible_bytes', + )?.result === bytes.length); + checks.push(valueReceipt.measurements.find( + (measurement) => measurement.id === 'escalation_required', + )?.result === packet.receipt.raw_evidence.escalation_required); checks.push(packet.receipt.measurements.original_bytes === fixture.input.streams.reduce( (sum, stream) => sum + Buffer.byteLength(stream.data, stream.encoding === 'base64' ? 'base64' : 'utf8'), 0, )); - return { packet, options, pass: checks.every(Boolean) }; + return { packet, valueReceipt, options, pass: checks.every(Boolean) }; } catch (error) { if (error instanceof PayloadCeilingError && fixture.expected.ceilingError) { return { error, options, pass: true }; @@ -168,6 +184,7 @@ export function conformanceReport() { verdict_preserved: fixture.expected.status == null ? null : packet.decision_evidence.status === fixture.expected.status, + value_receipt_schema: result.valueReceipt.schema, }; }); return { @@ -175,5 +192,6 @@ export function conformanceReport() { fixture_count: fixtures.length, fixtures, protocol_version: 'opsle.context-firewall.conformance/v1', + value_receipt_schema: 'opsle.value-receipt.v1', }; } diff --git a/package.json b/package.json index 66c7f12..b3a4855 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "@opsle/context-firewall", - "version": "0.2.0", + "version": "0.3.0", "private": true, "type": "module", "bin": { "context-firewall": "./bin/context-firewall.js" }, "scripts": { + "check": "node --check ./src/reducer.js && node --check ./src/value-receipt.js && node --check ./bin/context-firewall.js && node --check ./fixtures/corpus.js && node --check ./tests/reducer.test.js", "test": "node --test", "conformance": "node ./bin/context-firewall.js conformance" }, diff --git a/src/reducer.js b/src/reducer.js index 2be1c82..d541007 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -1,10 +1,11 @@ import { createHash } from 'node:crypto'; import { TextDecoder } from 'node:util'; +import { buildReductionValueReceipt } from './value-receipt.js'; export const INPUT_PROTOCOL = 'opsle.context-firewall.test-run-input/v1'; export const PACKET_PROTOCOL = 'opsle.context-firewall.evidence-packet/v1'; export const REDUCER_NAME = '@opsle/context-firewall/test-output'; -export const REDUCER_VERSION = '0.2.0'; +export const REDUCER_VERSION = '0.3.0'; export const POLICY_REVISION = 'tap-subset-policy/v1'; const ANSI_PATTERN = /[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*)?\u0007)|(?:(?:\d{1,4}(?:[;:]\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g; @@ -516,3 +517,38 @@ export function reduceTestRun(input, options = {}) { export function serializePacket(packet) { return Buffer.from(`${canonicalJson(packet)}\n`, 'utf8'); } + +export function valueReceiptForPacket(packet, { mechanismRevision = null } = {}) { + if (mechanismRevision != null && (typeof mechanismRevision !== 'string' || mechanismRevision.length === 0)) { + throw new InputError('mechanismRevision must be a nonempty string or null'); + } + const measurements = packet.receipt.measurements; + return buildReductionValueReceipt({ + ambiguousEvents: packet.decision_evidence.unclassified_evidence.length, + configurationIdentity: packet.receipt.configuration.identity, + escalationRequired: packet.receipt.raw_evidence.escalation_required, + inputHash: packet.receipt.input_hash, + mechanismRevision, + mechanismVersion: REDUCER_VERSION, + operationId: packet.operation_id, + originalBytes: measurements.original_bytes, + originalEvents: measurements.original_event_count, + payloadCeilingBytes: packet.receipt.configuration.max_output_bytes, + policyRevision: packet.receipt.configuration.policy_revision, + rawEvidenceRef: packet.receipt.raw_evidence.reference, + reducedBytes: measurements.reduced_bytes, + retainedEvents: measurements.retained_evidence_count, + runId: packet.receipt.source.run_id, + semanticPayloadHash: packet.receipt.semantic_payload_hash, + }); +} + +export function reduceWithValueReceipt(input, options = {}) { + const packet = reduceTestRun(input, options); + return { + packet, + valueReceipt: valueReceiptForPacket(packet, { + mechanismRevision: options.mechanismRevision ?? null, + }), + }; +} diff --git a/src/value-receipt.js b/src/value-receipt.js new file mode 100644 index 0000000..14e6c2a --- /dev/null +++ b/src/value-receipt.js @@ -0,0 +1,312 @@ +export const VALUE_RECEIPT_SCHEMA = 'opsle.value-receipt.v1'; +export const VALUE_MECHANISM_ID = 'opsle.context-firewall'; +export const VALUE_MECHANISM_NAME = 'Context Firewall'; + +const NO_CAUSAL_CLAIM = 'No token, cost, latency, correctness, or causal savings claim is made.'; + +function measurement({ + id, + baseline, + result, + delta, + unit, + direction, + measurementClass, + evidenceRefs, + sourceVerification, + operatorDisplay, + safeToAggregate, + limitations = [], +}) { + return { + aggregation: { + method: safeToAggregate ? 'SUM' : null, + safe: safeToAggregate, + }, + baseline, + class: measurementClass, + delta, + derivation: null, + direction, + evidence_refs: evidenceRefs, + id, + limitations, + operator_display: operatorDisplay, + result, + source_verification: sourceVerification, + unit, + }; +} + +export function buildReductionValueReceipt({ + ambiguousEvents, + configurationIdentity, + escalationRequired, + inputHash, + mechanismRevision, + mechanismVersion, + operationId, + originalBytes, + originalEvents, + payloadCeilingBytes, + policyRevision, + rawEvidenceRef, + reducedBytes, + retainedEvents, + runId, + semanticPayloadHash, +}) { + const bytesInitiallyAvoided = originalBytes - reducedBytes; + const reductionRatio = originalBytes === 0 + ? null + : `${bytesInitiallyAvoided}/${originalBytes}`; + const suppressedEvents = originalEvents - retainedEvents; + const evidence = [ + { + id: 'canonical_packet', + kind: 'JSON_POINTER', + locator: '/', + trust: 'VERIFIED', + }, + { + id: 'configuration', + kind: 'CONTENT_HASH', + locator: configurationIdentity, + trust: 'VERIFIED', + }, + { + id: 'input_bytes', + kind: 'CONTENT_HASH', + locator: inputHash, + trust: 'VERIFIED', + }, + { + id: 'semantic_payload', + kind: 'CONTENT_HASH', + locator: semanticPayloadHash, + trust: 'VERIFIED', + }, + ]; + if (rawEvidenceRef) { + evidence.push({ + id: 'raw_locator', + kind: 'RUN_ARTIFACT', + locator: rawEvidenceRef, + trust: 'CALLER_SUPPLIED', + }); + } + + const ratioLimitations = [ + 'Computed as (raw bytes - initially model-visible bytes) / raw bytes.', + 'Negative values indicate canonical packet expansion.', + 'Ratios are not directly summable; cumulative ratios must be recomputed from byte totals.', + ]; + if (originalBytes === 0) { + ratioLimitations.push('Unavailable because the raw-byte baseline is zero.'); + } + + return { + evidence, + limitations: [ + NO_CAUSAL_CLAIM, + 'Evidence-event counts cover source transcript events, not derived packet fields.', + mechanismRevision + ? 'Mechanism source revision is caller supplied and is not inspected by the deterministic reducer.' + : 'Mechanism source revision was not supplied and is recorded as null.', + rawEvidenceRef + ? 'The raw evidence locator is caller supplied; external existence, immutability, and contents are not verified.' + : 'No raw evidence locator was supplied.', + ], + measurements: [ + measurement({ + id: 'raw_bytes', + baseline: null, + result: originalBytes, + delta: null, + unit: 'byte', + direction: 'NEUTRAL', + measurementClass: 'EXACT', + evidenceRefs: ['input_bytes'], + sourceVerification: 'VERIFIED', + operatorDisplay: false, + safeToAggregate: true, + }), + measurement({ + id: 'initial_model_visible_bytes', + baseline: originalBytes, + result: reducedBytes, + delta: reducedBytes - originalBytes, + unit: 'byte', + direction: 'LOWER_IS_VALUE', + measurementClass: 'EXACT', + evidenceRefs: ['canonical_packet', 'input_bytes'], + sourceVerification: 'VERIFIED', + operatorDisplay: true, + safeToAggregate: true, + limitations: ['Result is the canonical evidence packet written to model-visible stdout.'], + }), + measurement({ + id: 'bytes_initially_avoided', + baseline: reducedBytes, + result: originalBytes, + delta: bytesInitiallyAvoided, + unit: 'byte', + direction: 'HIGHER_IS_VALUE', + measurementClass: 'EXACT', + evidenceRefs: ['canonical_packet', 'input_bytes'], + sourceVerification: 'VERIFIED', + operatorDisplay: true, + safeToAggregate: true, + limitations: ['Signed delta is raw bytes minus initially model-visible bytes; a negative delta indicates expansion.'], + }), + measurement({ + id: 'initial_reduction_ratio', + baseline: null, + result: reductionRatio, + delta: null, + unit: 'ratio', + direction: 'HIGHER_IS_VALUE', + measurementClass: 'EXACT', + evidenceRefs: ['canonical_packet', 'input_bytes'], + sourceVerification: 'VERIFIED', + operatorDisplay: true, + safeToAggregate: false, + limitations: ratioLimitations, + }), + measurement({ + id: 'original_evidence_events', + baseline: null, + result: originalEvents, + delta: null, + unit: 'event', + direction: 'NEUTRAL', + measurementClass: 'EXACT', + evidenceRefs: ['semantic_payload'], + sourceVerification: 'VERIFIED', + operatorDisplay: false, + safeToAggregate: true, + }), + measurement({ + id: 'retained_evidence_events', + baseline: null, + result: retainedEvents, + delta: null, + unit: 'event', + direction: 'NEUTRAL', + measurementClass: 'EXACT', + evidenceRefs: ['semantic_payload'], + sourceVerification: 'VERIFIED', + operatorDisplay: false, + safeToAggregate: true, + }), + measurement({ + id: 'suppressed_evidence_events', + baseline: null, + result: suppressedEvents, + delta: null, + unit: 'event', + direction: 'NEUTRAL', + measurementClass: 'EXACT', + evidenceRefs: ['semantic_payload'], + sourceVerification: 'VERIFIED', + operatorDisplay: false, + safeToAggregate: true, + }), + measurement({ + id: 'ambiguous_evidence_events', + baseline: null, + result: ambiguousEvents, + delta: null, + unit: 'event', + direction: 'PROTECTION_SIGNAL', + measurementClass: 'EXACT', + evidenceRefs: ['semantic_payload'], + sourceVerification: 'VERIFIED', + operatorDisplay: false, + safeToAggregate: true, + limitations: ['Counts unclassified source events, including synthetic markers for malformed UTF-8.'], + }), + measurement({ + id: 'payload_ceiling_bytes', + baseline: null, + result: payloadCeilingBytes, + delta: null, + unit: 'byte', + direction: 'NOT_APPLICABLE', + measurementClass: 'EXACT', + evidenceRefs: ['configuration'], + sourceVerification: 'VERIFIED', + operatorDisplay: false, + safeToAggregate: false, + limitations: payloadCeilingBytes == null ? ['No payload ceiling was configured.'] : [], + }), + measurement({ + id: 'escalation_required', + baseline: null, + result: escalationRequired, + delta: null, + unit: 'boolean', + direction: 'PROTECTION_SIGNAL', + measurementClass: 'OBSERVED', + evidenceRefs: ['canonical_packet'], + sourceVerification: 'OBSERVED', + operatorDisplay: true, + safeToAggregate: false, + }), + measurement({ + id: 'raw_locator_available', + baseline: null, + result: Boolean(rawEvidenceRef), + delta: null, + unit: 'boolean', + direction: 'PROTECTION_SIGNAL', + measurementClass: 'OBSERVED', + evidenceRefs: [rawEvidenceRef ? 'raw_locator' : 'canonical_packet'], + sourceVerification: rawEvidenceRef ? 'CALLER_SUPPLIED' : 'OBSERVED', + operatorDisplay: false, + safeToAggregate: false, + limitations: ['Availability means a caller supplied a locator; the external artifact is not verified by the reducer.'], + }), + ], + mechanism: { + id: VALUE_MECHANISM_ID, + name: VALUE_MECHANISM_NAME, + revision: mechanismRevision, + version: mechanismVersion, + }, + operation: { + configuration_id: configurationIdentity, + id: operationId, + name: 'test-output-reduction', + policy_id: policyRevision, + }, + run: { + id: runId, + }, + schema: VALUE_RECEIPT_SCHEMA, + }; +} + +function measurementById(receipt, id) { + return receipt.measurements.find((item) => item.id === id); +} + +function groupedInteger(value) { + return String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +export function formatContextFirewallIndicator(receipt) { + const rawBytes = measurementById(receipt, 'raw_bytes').result; + const visibleBytes = measurementById(receipt, 'initial_model_visible_bytes').result; + const avoidedBytes = measurementById(receipt, 'bytes_initially_avoided').delta; + const reductionRatio = measurementById(receipt, 'initial_reduction_ratio').result; + const escalationRequired = measurementById(receipt, 'escalation_required').result; + let visibleValue; + if (avoidedBytes >= 0) { + const percentage = reductionRatio == null ? '' : ` (${(avoidedBytes / rawBytes * 100).toFixed(2)}%)`; + visibleValue = `${groupedInteger(avoidedBytes)} B initially avoided${percentage}`; + } else { + visibleValue = `${groupedInteger(-avoidedBytes)} B expansion`; + } + return `[Context Firewall] ${groupedInteger(rawBytes)} B -> ${groupedInteger(visibleBytes)} B | ${visibleValue} | escalation: ${escalationRequired ? 'yes' : 'no'}`; +} diff --git a/tests/reducer.test.js b/tests/reducer.test.js index 6d28f15..2a7f657 100644 --- a/tests/reducer.test.js +++ b/tests/reducer.test.js @@ -1,5 +1,8 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import test from 'node:test'; import { fileURLToPath } from 'node:url'; import { @@ -11,9 +14,14 @@ import { REDUCER_VERSION, canonicalJson, reduceTestRun, + reduceWithValueReceipt, serializePacket, } from '../src/reducer.js'; import { conformanceReport, corpus, executeFixture } from '../fixtures/corpus.js'; +import { + VALUE_RECEIPT_SCHEMA, + formatContextFirewallIndicator, +} from '../src/value-receipt.js'; const cliPath = fileURLToPath(new URL('../bin/context-firewall.js', import.meta.url)); @@ -23,6 +31,12 @@ function fixture(name) { return value; } +function valueMeasurement(receipt, id) { + const value = receipt.measurements.find((item) => item.id === id); + assert.ok(value, `missing value measurement ${id}`); + return value; +} + test('rejects an unsupported input protocol', () => { assert.throws( () => reduceTestRun({ protocol_version: 'other', streams: [] }), @@ -56,7 +70,7 @@ test('large all-pass output is substantially reduced with correct aggregates', ( const packet = reduceTestRun(fixture('normal/large-all-pass').input); assert.deepEqual(packet.decision_evidence.counts, { passed: 1500, failed: 0, skipped: 0, total: 1500 }); assert.equal(packet.decision_evidence.status, 'passed'); - assert.ok(packet.receipt.measurements.reduced_bytes < packet.receipt.measurements.original_bytes * 0.1); + assert.ok(packet.receipt.measurements.reduced_bytes < packet.receipt.measurements.original_bytes * 0.5); assert.equal(packet.receipt.suppressed.categories.successful_test, 1500); }); @@ -248,6 +262,137 @@ test('original and reduced byte and event measurements are exact', () => { ); }); +test('emits the complete opsle.value-receipt.v1 Context Firewall profile', () => { + const { valueReceipt: receipt } = reduceWithValueReceipt(fixture('normal/large-all-pass').input); + assert.equal(receipt.schema, VALUE_RECEIPT_SCHEMA); + assert.deepEqual(receipt.mechanism, { + id: 'opsle.context-firewall', + name: 'Context Firewall', + revision: null, + version: REDUCER_VERSION, + }); + assert.equal(receipt.run.id, 'run-001'); + assert.equal(receipt.operation.id, 'op-synthetic-001'); + assert.equal(receipt.operation.name, 'test-output-reduction'); + assert.equal(receipt.operation.policy_id, POLICY_REVISION); + assert.match(receipt.operation.configuration_id, /^sha256:[0-9a-f]{64}$/); + assert.deepEqual(receipt.measurements.map((item) => item.id), [ + 'raw_bytes', + 'initial_model_visible_bytes', + 'bytes_initially_avoided', + 'initial_reduction_ratio', + 'original_evidence_events', + 'retained_evidence_events', + 'suppressed_evidence_events', + 'ambiguous_evidence_events', + 'payload_ceiling_bytes', + 'escalation_required', + 'raw_locator_available', + ]); + assert.equal(receipt.evidence.every((item) => item.id && item.kind && item.locator && item.trust), true); + assert.equal(new Set(receipt.evidence.map((item) => item.id)).size, receipt.evidence.length); + for (const item of receipt.measurements.filter((value) => value.class === 'EXACT')) { + assert.equal(item.source_verification, 'VERIFIED'); + assert.equal(item.derivation, null); + } +}); + +test('caller-supplied mechanism revision affects only the deterministic value receipt', () => { + const input = fixture('normal/large-all-pass').input; + const revision = 'dd34bd9f681314761f1ca87f339648bf611811f3'; + const first = reduceWithValueReceipt(input, { mechanismRevision: revision }); + const second = reduceWithValueReceipt(input, { mechanismRevision: revision }); + assert.equal(first.valueReceipt.mechanism.revision, revision); + assert.equal(canonicalJson(first.valueReceipt), canonicalJson(second.valueReceipt)); + assert.deepEqual(serializePacket(first.packet), serializePacket(reduceTestRun(input))); + assert.equal('value_receipt' in first.packet, false); + assert.throws( + () => reduceWithValueReceipt(input, { mechanismRevision: '' }), + (error) => error instanceof InputError && error.code === 'INVALID_INPUT', + ); +}); + +test('value receipt byte deltas, ratio, and event partition are exact', () => { + const { packet, valueReceipt } = reduceWithValueReceipt(fixture('normal/large-all-pass').input); + const outputBytes = serializePacket(packet).length; + const raw = valueMeasurement(valueReceipt, 'raw_bytes'); + const visible = valueMeasurement(valueReceipt, 'initial_model_visible_bytes'); + const avoided = valueMeasurement(valueReceipt, 'bytes_initially_avoided'); + const ratio = valueMeasurement(valueReceipt, 'initial_reduction_ratio'); + assert.equal(raw.result, packet.receipt.measurements.original_bytes); + assert.deepEqual( + [visible.baseline, visible.result, visible.delta], + [raw.result, outputBytes, outputBytes - raw.result], + ); + assert.deepEqual( + [avoided.baseline, avoided.result, avoided.delta], + [outputBytes, raw.result, raw.result - outputBytes], + ); + assert.equal(ratio.result, `${raw.result - outputBytes}/${raw.result}`); + assert.deepEqual(ratio.aggregation, { method: null, safe: false }); + assert.equal( + valueMeasurement(valueReceipt, 'retained_evidence_events').result + + valueMeasurement(valueReceipt, 'suppressed_evidence_events').result, + valueMeasurement(valueReceipt, 'original_evidence_events').result, + ); +}); + +test('value receipt preserves expansion instead of fabricating avoided bytes', () => { + const { valueReceipt } = reduceWithValueReceipt(fixture('normal/small-all-pass').input); + const avoided = valueMeasurement(valueReceipt, 'bytes_initially_avoided'); + assert.ok(avoided.delta < 0); + assert.equal(avoided.delta, avoided.result - avoided.baseline); + assert.equal( + formatContextFirewallIndicator(valueReceipt), + '[Context Firewall] 104 B -> 1,824 B | 1,720 B expansion | escalation: no', + ); +}); + +test('value receipt exposes reduction and the exact named operator indicator', () => { + const { valueReceipt } = reduceWithValueReceipt(fixture('normal/large-all-pass').input); + assert.equal( + formatContextFirewallIndicator(valueReceipt), + '[Context Firewall] 28,981 B -> 1,846 B | 27,135 B initially avoided (93.63%) | escalation: no', + ); +}); + +test('value receipt records ambiguity, escalation, and raw-locator trust', () => { + const { valueReceipt } = reduceWithValueReceipt(fixture('edge/malformed-output').input); + assert.equal(valueMeasurement(valueReceipt, 'ambiguous_evidence_events').result, 2); + assert.deepEqual( + { + class: valueMeasurement(valueReceipt, 'escalation_required').class, + result: valueMeasurement(valueReceipt, 'escalation_required').result, + }, + { class: 'OBSERVED', result: true }, + ); + const locator = valueMeasurement(valueReceipt, 'raw_locator_available'); + assert.equal(locator.result, true); + assert.equal(locator.source_verification, 'CALLER_SUPPLIED'); + assert.equal(valueReceipt.evidence.find((item) => item.id === 'raw_locator').trust, 'CALLER_SUPPLIED'); +}); + +test('missing raw locator and zero-byte ratio remain explicitly unavailable', () => { + const input = structuredClone(fixture('normal/small-all-pass').input); + input.streams[0].data = ''; + delete input.source.raw_evidence_ref; + const { valueReceipt } = reduceWithValueReceipt(input); + assert.equal(valueMeasurement(valueReceipt, 'raw_bytes').result, 0); + assert.equal(valueMeasurement(valueReceipt, 'initial_reduction_ratio').result, null); + assert.equal(valueMeasurement(valueReceipt, 'raw_locator_available').result, false); + assert.equal(valueMeasurement(valueReceipt, 'escalation_required').result, true); + assert.equal(valueReceipt.evidence.some((item) => item.id === 'raw_locator'), false); + assert.ok(valueReceipt.limitations.includes('No raw evidence locator was supplied.')); +}); + +test('payload ceiling value remains exact configuration and is never summed', () => { + const item = fixture('payload/comfortably-above'); + const { valueReceipt } = reduceWithValueReceipt(item.input, item.options); + const ceiling = valueMeasurement(valueReceipt, 'payload_ceiling_bytes'); + assert.equal(ceiling.result, item.options.maxOutputBytes); + assert.deepEqual(ceiling.aggregation, { method: null, safe: false }); +}); + test('duration is caller-supplied semantic evidence and no runtime latency is hashed', () => { const packet = reduceTestRun(fixture('normal/small-all-pass').input); assert.equal(packet.decision_evidence.process.duration_ms, 12.5); @@ -269,6 +414,40 @@ test('CLI reads JSON from stdin and emits the canonical packet', () => { assert.equal(result.status, 0, result.stderr); const expected = serializePacket(reduceTestRun(input)).toString('utf8'); assert.equal(result.stdout, expected); + assert.equal('value_receipt' in JSON.parse(result.stdout), false); + assert.equal( + result.stderr, + `${formatContextFirewallIndicator(reduceWithValueReceipt(input).valueReceipt)}\n`, + ); +}); + +test('CLI writes a canonical value receipt only to an explicitly requested sidecar', () => { + const input = fixture('normal/large-all-pass').input; + const directory = mkdtempSync(join(tmpdir(), 'context-firewall-value-')); + const receiptPath = join(directory, 'value-receipt.json'); + const revision = 'dd34bd9f681314761f1ca87f339648bf611811f3'; + try { + const result = spawnSync(process.execPath, [ + cliPath, + 'reduce', + '--mechanism-revision', + revision, + '--value-receipt', + receiptPath, + ], { + encoding: 'utf8', + input: JSON.stringify(input), + }); + assert.equal(result.status, 0, result.stderr); + const { packet, valueReceipt } = reduceWithValueReceipt(input, { + mechanismRevision: revision, + }); + assert.equal(result.stdout, serializePacket(packet).toString('utf8')); + assert.equal(readFileSync(receiptPath, 'utf8'), `${canonicalJson(valueReceipt)}\n`); + assert.equal(JSON.parse(readFileSync(receiptPath, 'utf8')).schema, VALUE_RECEIPT_SCHEMA); + } finally { + rmSync(directory, { recursive: true }); + } }); test('CLI reports malformed JSON as a machine-readable error', () => {