# ksl-tender-events

KSL tender events (Kognai job ec7b155a, SEAT7): record every tender step as an event in an append only JSONL stream, and write the daily report section in Markdown. TypeScript, Node 20 or newer, no runtime dependencies.

```ts
emit(event)                      // validates, then appends one line
reportSection(events, day)       // -> Markdown for that day (UTC)
```

## Quick start (about one minute)

```sh
curl -sO https://agentgate-source.pages.dev/ksl-tender-events/ksl-tender-events.bundle
git clone ksl-tender-events.bundle ksl-tender-events && cd ksl-tender-events
npm ci && npm test   # 11 tests, 0 failing, no network
npm run mutants   # planted bug check: 10 of 10 planted bugs caught
```

Source: https://agentgate-source.pages.dev/ksl-tender-events/ (browsable tree, git bundle with full history, tarball https://agentgate-source.pages.dev/ksl-tender-events/ksl-tender-events.tar.gz). This is the public home of the code while our GitHub account is under review.

## Events

Every event has `type`, `job_id` and `at` (ISO date-time with a zone). The 8 steps and their fields:

| type | fields |
| --- | --- |
| `listing` | `title?`, `ceiling_usdc?` |
| `airlock_decision` | `bid_id`, `decision` (`pass` or `block`), `note?` |
| `bid` | `bid_id`, `agent_did`, `price_usdc` |
| `selection` | `bid_id` |
| `award` | `bid_id` |
| `delivery` | `bid_id`, `url` (https) |
| `review` | `bid_id`, `verdict` (`accepted` or `rejected`), `note?` |
| `payment` | `bid_id`, `amount_usdc`, `rail?` (`x402`) |

Validation refuses an unknown type, a missing or malformed field, a non-https link, a negative amount, and **any field not in the table**, so a contact, a key or a header can never slip into the stream. A refused event writes nothing.

## Append only, and provably so

```ts
import { openEventLog, emit } from 'ksl-tender-events';

const log = openEventLog('tender-events.jsonl');
log.emit({ type: 'bid', job_id: 'j1', at: '2026-10-21T02:00:00Z', bid_id: 'b1', agent_did: 'did:pkh:eip155:8453:agent', price_usdc: 40 });
log.read(); // every stored event, checked

emit(event); // the same, on the stream named by KSL_EVENTS_PATH (default ./tender-events.jsonl)
```

The file is only ever opened for append. Each stored line carries `seq`, `prev` (the hash of the line before) and `hash` (sha256 of seq, prev and the canonical event), so `read()` and `readStream(path)` throw if a line was changed, removed or moved, and `openEventLog` refuses to append to a broken stream.

## The daily report section

```ts
reportSection(events, '2026-10-21');
```

```markdown
### Tenders on 2026-10-21

6 events across 2 jobs; 10.00 USDC paid.

| Step | Count |
| --- | --- |
| Listings | 1 |
| Airlock decisions | 1 |
| Bids | 2 |
| Selections | 1 |
| Awards | 0 |
| Deliveries | 0 |
| Reviews | 0 |
| Payments | 1 |

**Open anomalies at the end of the day (2)**

- j1 / b2: bid without an airlock decision
- j2 / b7: payment without acceptance
```

It counts each step on the day (with blocked airlock decisions and rejected reviews called out), totals the USDC paid, and lists the anomalies still open at the end of the day:

- a bid without an airlock decision (the spec's first example)
- a payment without acceptance, or after a rejected review (the spec's second example)
- a selection of a bid the airlock blocked, or of a bid never recorded
- an award without a selection, a delivery without an award, a review without a delivery
- a payment over the bid price

Anomalies look at every event up to the end of the day, so a decision made on an earlier day counts and a later fix clears it from that day on. The output is the same whatever the input order.

## Tests

```bash
npm ci
npm test          # 11 tests: append only, tamper detection, validation, counts, every anomaly
npm run test:live # records every open job on the live LAX registry as a listing event and reports on it
```

## Planted bug check

Kognai's reviewer plants a bug and expects the tests to catch it, so the repo does the same to itself. `npm run mutants` copies the repo to a scratch folder once per entry in `scripts/mutants.json`, applies that one bug (an off by one, a removed check, a skipped rule), runs `npm test` and requires the suite to fail. Last run: **10 of 10 caught**, 0 survived.

## License

MIT
