About Signal Ledger
What this is, why it was built this way, the stack behind it, and - generated from the actual test run, not typed by hand - every test that passed before this page shipped.
What's new (Oct 2026)
- Service area fix on Live records. Every captured Rootly incident was attached to one sandbox service, and the Jira ticket's first label was the same value, so all of them showed "db production database". Replay now maps a service value outside this app's own areas by a documented keyword rule on the record's title, or to "other" when no rule matches, and shows the original value beside the mapped one. The Jira-Rootly link still forms at 97%; the translation did not change it.
- Readiness. A synthetic service catalogue with a nine-item readiness checklist and a visible rule for Ready, Conditional or Not ready. Services live for 30 days or fewer show the incidents raised since go-live, linked to the queue, and a computed panel compares checklist gaps with those incidents. It says so when there is no pattern. Checklist edits stay in your browser and are written into the hash-chained history.
- Changes and DORA measures. A synthetic change feed. A signal on the same service shortly after a change is linked to it, with the time gap and a confidence score shown, and the window is editable. Change failure rate, the share of incidents linked to a change and mean time to restore are computed from those invented records, so they show the method, not how any real team performs. Time to restore averages high and critical signals only, because restoring service after a failure is not the same as closing a routine request. A separate time to resolve averages every resolved ticket, and low-priority tickets that wait for days pull that one up.
- Suppliers, the overnight batch and a review pack. Invented suppliers on some synthetic signals, a Suppliers tab on SLA health (signals, breaches, time to restore, repeat issues), an overnight settlement batch with a computed completion-target flag, and a print-friendly one-page service review pack.
- A Security Operations source and problem candidates. One synthetic detection alert, handed to incident management and reported at the service desk, now forms its own correlation group (its similarity scores come from Claude through the same precompute script as the rest). The Correlation page also lists problem candidates, separate groups in one service with shared wording. On this data it finds none, and says so.
Numbers that moved. The queue holds 31 signals (18 synthetic plus 13 captured), up from 28, because of the three Security Operations signals, and there are five correlation groups instead of four. The unit suite is 423 tests, up from 360 before these changes, and the Playwright suite is 66 tests (57 at desktop width and 9 at phone width).
About the video. The walkthrough below was re-recorded after these additions. It covers the Readiness page, the change measures, the Suppliers tab and the review pack, but not the Security Operations source or problem candidates.
Two-minute walkthrough
Read the transcript
A company runs a service desk, an incident platform, and a partner channel. Each holds part of the picture. Signal Ledger unifies them without replacing any.
One queue, thirty-one signals, six sources. The service desk is invented; in real life it could be ServiceNow, Zendesk or Jira Service Management. The incident platform is Rootly, with a few real incidents from my own sandbox account. It could be incident.io or PagerDuty.
The partner channel stands for a partner portal, a shared Slack or Teams channel, or a support inbox. Each row keeps its source badge.
Correlation links the same problem reported in two tools. Here a Jira ticket and a Rootly incident group at ninety-seven percent confidence, with the reasoning shown. The root cause is AI-suggested, and labelled unverified.
The threshold is yours: drag it, and the groups re-form instantly. I deliberately left sixteen signals unlinked, because a tool that links everything is worse than one that doesn't.
SLA health scores internal, consumer and partner separately. Internal beats its goal at ninety-six percent; consumer and partner fall short. One blended average would hide that.
To connect a tool, map its fields onto one shape. Pick a template, such as ServiceNow or Rootly, and each path is read against a sample record, live. The rest of the app never sees a platform's own format. That boundary is the architecture.
The About page covers how it was built, the security choices, and the tests: four hundred and twenty-three passing, generated from the real run. I built it with Claude Code.
Readiness is about running software that engineering built. Each invented service has a nine-point checklist, and its status is computed from it: any item not met means not ready. A panel then checks whether the gaps match the early incidents, and says so plainly when they do not.
Changes are linked to incidents by how close they land, giving change failure rate and time to restore, labelled as the DORA measures they are. Suppliers get their own scorecard, and one button builds a print-ready review pack for the monthly forum.
Signal Ledger replaces nothing. It sits above the tools you already have, and shows what they can't show alone.
How this was built
The problem
This kind of fragmentation tends to compound as a company grows. A business that starts out serving customers directly often ends up running a second business alongside the first, such as a platform other companies build on, each expanding its own tooling as it scales, frequently across more than one ITSM platform rather than one shared system. The stakes rise again once that company starts pushing automation, or AI-assisted decisions, into a regulated, customer-facing front end sitting on operations that were never unified to begin with.
A company running a service desk, an incident platform and a partner-facing channel sees the same underlying problem three times, in three unrelated lists - staff report a slow partner portal, engineering logs an elevated-latency incident, a partner emails about timeouts - and nobody connects them, because each tool only ever sees its own list. Replacing any of those tools is expensive and usually unnecessary; the gap is in the connecting layer, not the tools themselves.
What was built
Six screens - a unified queue (with an orientation card for a visitor who has never seen this before), a correlation view, an SLA health dashboard, a service readiness view, a working "Connect a source" form, and this page - on top of a connector abstraction that normalises any source's native records into one Signal shape. Four sources are synthetic by default (a service desk, an incident platform, a partner channel and security operations); two more (Rootly and Jira) have real, working connectors, with a handful of real captured records replayed alongside the synthetic ones - never correlated with them. A third (ServiceNow) has a working connector with no captured snapshot yet.
How it works
Correlation runs three cheap gates - different source, opened within a time window, same or related service area - before paying for a text-similarity call, then blends the three into one confidence score. SLA scoring is a pure function of (signals, config, as-of clock), so it's deterministic and testable without mocking a clock. Real data is captured once, then replayed - a script fetches a real account read-only, keeps only the fields the mapping actually reads, scrubs emails/links/tokens from any free text, and prints every record for review before it's committed; the deployed app never holds a live credential.
Architecture
Every connector - mock or live - normalises into the same Signal[] shape, so the correlation and SLA engines, and every screen that reads them, can't tell a captured record from an invented one. That boundary is the architecture; everything else hangs off it.
Security
- Credentials never reach the deployed app. Real data is captured once by a script run locally, reading the owner's own API key from
.env.local(never committed), then replayed from a static file. The deployed copy holds no live credential and makes no calls to Rootly, Jira or ServiceNow. - No SSRF surface on the Connect form. Its base-URL and token fields are read into the browser only - the server never fetches a visitor-supplied URL. An e2e test asserts this, not just a code comment.
- Free text is scrubbed before anything real is committed. The capture script strips emails, links, IPv4 addresses and token-shaped strings from captured ticket text, and prints every record for human review before it's committed - see
src/lib/connectors/project.ts. - Local actions are hash-chained, not just logged. Dismissing or restoring a correlation group, or changing a readiness checklist item, appends to a SHA-256 chain that breaks visibly if a past entry is edited outside the app. Edits to SLA targets are not in it. Honestly scoped: single-browser, no login, so it's a local tamper-evident log, not a signed multi-user audit trail.
- Dependencies are scanned weekly, not just installed once. Dependabot watches both npm and the GitHub Actions versions pinned in CI, and GitHub's own Dependabot vulnerability alerts and automated security fixes are switched on for this repo. Native secret scanning isn't available here - like the branch protection named under Build decisions below, it's a private-repo limit on the current GitHub plan, not a choice.
npm auditis triaged, not just run. It currently reports findings rooted in one transitive dependency, all of them reachable only through dev tooling (the component-scaffolding CLI and the lint config) and never imported by the app or the connector scripts - confirmed withnpm audit --omit=dev(clean) and a grep for the affected packages undersrc/andscripts/. Nothing that ships to production is affected.- The git history has been checked, not just the current tree. A full history scan confirms
.env.localwas never committed and no secret-shaped string appears in any past commit - a scrub that only checked the current tree could have missed something still reachable in an old one. - A security.txt is published. RFC 9116's standard location (
/.well-known/security.txt) points a real report at this repo's issue tracker rather than leaving no contact path at all. - Baseline response hardening is set, a CSP isn't yet. Every response sets
X-Frame-Options,X-Content-Type-Options, aReferrer-Policyand a locked-downPermissions-Policy, and drops the framework-fingerprintingX-Powered-Byheader - seenext.config.ts. A real Content-Security-Policy is the obvious next step and isn't in place yet; named here rather than left for someone else to find.
How this relates to ITIL and full ITSM platforms
This is not a ServiceNow or Jira Service Management replacement, and it is not a CMDB. What it does implement, at demonstrator scale, is a working slice of three ITIL practices: Incident Management (the queue), Problem Management (grouping incidents into a problem with a root-cause hypothesis), and Service Level Management (audience-tiered targets and attainment, not one blended number). The full glossary below maps each ITIL term to the actual code behind it - and names what's deliberately missing, like a Known Error database or a real CMDB, rather than implying it's all there.
The point being demonstrated isn't "this could replace your ITSM platform" - it's that the connector pattern underneath (one normalisation boundary, any number of real tools behind it) is how a Problem- and SLA-management layer could sit across a ServiceNow, a Jira Service Management and a support inbox without replacing any of them.
There's a second reading worth naming directly: the FCA's operational resilience rules (SYSC 15A) ask firms to understand how an incident in any one underlying system can affect a named important business service end-to-end, not just track each system's own uptime in isolation - and by now that's a live expectation, not a future one; the transition period for full compliance closed last year. The correlation pattern here is a small, working example of exactly that kind of cross-system visibility - not an implementation of impact tolerances or resilience testing, which is a materially bigger undertaking, but the visibility layer those depend on.
The role of AI
Built with Claude Code. The standard held to wasn't "plausible code," it was verified behaviour: when the Rootly and Jira connectors were pointed at real accounts mid-build, both turned out to need a fix that reading the API response, not the preset's original assumptions, actually surfaced - Rootly's API returns a service only as a bare relationship reference, not embedded the way the connector first assumed, and Jira removed its old search endpoint entirely partway through this build, requiring a migration to a token-paginated replacement with a differently-bounded query. Both were found by querying the live API and reading the actual response, not by trusting the first plausible-looking implementation.
Claude text-similarity scores are precomputed and printed for review before being committed, never silently accepted. Correlation and SLA logic are covered by tests that exercise real branching - a rejected record, a near-miss pair, a resolved ticket missing its resolution time (excluded from attainment rather than guessed) - not just the happy path.
Build decisions
- The normalisation boundary is protected. Every connector, mock or live, returns the same
Signal[]shape - nothing downstream can tell a mock from a real account. - Real and synthetic signals never correlate with each other. Enforced in
buildCandidatePairs, not just by convention - linking a captured ticket to an invented one would be meaningless. - The lexical fallback scorer is deliberately weak. It links nothing on the seed data at the default threshold - it exists so the app runs without an API key, not to make the demo look good, and the UI says plainly when it's in use.
- Dates are never formatted with Intl. A fixed-table
formatUtcavoids a real hydration mismatch: the ICU data behindIntl.DateTimeFormatcan differ between server and browser even with the same locale pinned. - CI runs the full suite, not just a lint pass. Lint, typecheck, Vitest, a production build and the full Playwright suite all run on every push - but without branch protection (not available on a private repo without GitHub Pro), a failing run doesn't yet block Vercel's own deploy, which happens independently on the same push.
Every test that runs
Tech stack
| Layer | Used for | Why |
|---|---|---|
| Next.js 16 (App Router) | Every screen, the /api/dataset route, and this page. | Server Components build the dataset - including any live-captured records - entirely server-side, the same place a live connector's credentials stay (nothing in this app is actually secret, but the pattern matches). |
| React 19 | Client state: the correlation threshold, dismissals, SLA edits, the added-sources form. | useSyncExternalStore backs the localStorage hook (src/lib/use-stored-json.ts) cleanly - the server and first client render agree, so there's no hydration flash. |
| TypeScript | The whole codebase, especially the Signal/FieldMapping/Connector types. | A field-mapping preset (src/lib/connectors/presets.ts) is a fairly deep nested object; a typo in a dot-path is a compile error here, not a silent empty field discovered in production. |
| Tailwind v4 + shadcn/ui + Radix | Every visual surface. | Matches the visual language of control-evidence-demo, the sibling project - same portfolio, same design tokens, no reason to diverge. Radix gives the Collapsible/Table primitives real keyboard and screen-reader behaviour for free. |
| Zod | Validating a source definition submitted on the Connect screen. | One schema (src/lib/connectors/definition.ts) is shared by the form, for instant feedback, and the API route, for the actual gate - the two can't quietly disagree about what's valid. |
| Vitest | The unit suite - 423 tests across 22 files. | Native ESM/TS with no transpile step to configure, and a mocking API that made stubbing a connector's fetch (src/lib/connectors/live/http.ts) and a stubbed Anthropic client both straightforward. |
| Playwright | End-to-end coverage of all six screens, desktop and phone width - wired into CI, not just a local check. | Runs in Pacific/Auckland specifically to catch a UTC-formatting regression that GitHub Actions' UTC-timezone runner would never surface on its own - see the formatUtc comment in src/lib/format.ts. |
| GitHub Actions | CI on every push and PR: lint, typecheck, the Vitest suite, a production build, then the full Playwright suite. Dependabot watches npm and the Actions versions pinned in the workflow, weekly. | A broken flow failing silently until someone notices in production is worse than a few minutes of CI time. The Playwright run reuses the same auto-started dev server a local run uses, so CI and local behaviour can't drift apart. |
| Anthropic API (Claude) | Text-similarity scoring between candidate signal pairs, and the root-cause hypothesis shown on a correlation group. | Scores are precomputed and shipped in src/data/similarity-cache.json, reviewed before committing (scripts/precompute-similarity.ts prints every score), so a deployed copy needs no key and makes no live calls for the shipped dataset. |
| Vercel | Hosting, and Vercel Authentication while this stays private. | Push-to-deploy on master, with the same portfolio pattern as the sibling project. |
ITIL glossary
| ITIL term | What it maps to here | Where |
|---|---|---|
| Incident Management | The Unified Queue - every open or resolved Signal, across sources, in one list. | src/components/queue-view.tsx |
| Incident record | A Signal - the normalised shape every connector produces, regardless of source. | src/lib/types.ts |
| Problem Management | The Correlation screen - grouping related incidents and surfacing a likely root cause. | src/components/correlation-view.tsx |
| Problem record | A CorrelationGroup - signals linked above the confidence threshold, with matched-feature evidence. | src/lib/types.ts, src/lib/correlation/group.ts |
| Root cause analysis | suggestedRootCause - a one-line hypothesis Claude generates for a high-confidence group, labelled as AI-generated and unverified on screen. | src/lib/correlation/similarity/claude.ts (generated), src/lib/correlation/group.ts (assembled onto the group) |
| Known Error | Not implemented. The closest analogue is a dismissed group's change history (a visitor's own "these aren't related" verdict, hash-chained locally) - that's a local judgement call, not a shared knowledge base. | src/lib/audit.ts |
| Service Level Management | The SLA health screen - three audience-segmented scorecards, never one blended number. | src/lib/sla.ts, src/components/sla-view.tsx |
| Priority-based SLA targets | Resolution-time targets and attainment goals, tiered by audience and severity - live-editable on the SLA screen. | src/lib/config.ts, src/lib/sla-overrides.ts |
| Service Desk | One of the four default source systems (the others are an incident platform, a partner channel and security operations). | src/lib/seed/signals.ts |
| Service Acceptance (readiness) | The Readiness page: a nine-item checklist per synthetic service and a visible rule for Ready, Conditional or Not ready. It is a view of readiness, not an approval workflow. | src/lib/readiness.ts, src/components/readiness-view.tsx |
| Change Enablement | Not implemented as a workflow: there is no request, approval or scheduling. What exists is a synthetic change feed, links from signals to a recent change on the same service, and failure-rate and time-to-restore measures computed from it. | src/lib/changes.ts, src/lib/seed/changes.ts |
| Supplier Management | A scorecard per invented supplier (signals, SLA breaches, time to restore, repeat issues) and an overnight batch completion flag. There are no contracts or service credits behind it. | src/lib/suppliers.ts |
| Configuration Item (CI) | serviceArea - a flat string tag ("payments", "partner-api") used for correlation gating, not a graph of dependent components. | src/lib/types.ts |
| Configuration Management Database (CMDB) | Not implemented - there is no CI inventory or dependency graph behind serviceArea, deliberately: modelling a real CMDB was out of scope for what this demonstrates. | - |
Sources referenced
- Rootly's incident API - used live mid-build to confirm the real shape of a service relationship.
- Jira Cloud's REST API v3 - including its enhanced-search migration notice, hit live as a 410 response mid-build, which prompted the move off the deprecated search endpoint.
- ServiceNow's Table API - connector built and tested against the documented shape; a live capture is pending.
- ITIL v4 foundation concepts (Incident, Problem and Service Level Management) - used as the vocabulary for the glossary above, not a certification claim about this codebase.
- The Anthropic API - for text-similarity scoring and the root-cause hypothesis shown on a correlation group.
What this doesn't prove
- A visitor's edits - dismissals, SLA target changes, the change history - live only in that browser's localStorage. There is no account system, and nothing here is shared between visitors.
- The SLA history behind the scorecards is generated by a seeded script, not measured from a real system.
- Only two of three live connectors have a captured snapshot; ServiceNow's is built and tested but has no real data behind it yet.
- The real records that are captured come from the owner's own trial/sandbox accounts, not a real organisation's production traffic.
- The correlation threshold and feature weights are illustrative choices tuned against this seed data, not validated against incident volumes at real scale.
- CI passing doesn't gate a deploy. Vercel deploys on push independently of GitHub Actions; branch protection would close that gap but isn't available on a private repo without GitHub Pro.
- There's no Content-Security-Policy yet. Baseline response headers are set (see Security above); CSP needs more care to get right without breaking hydration, and hasn't had it yet.
- Proving a handful of real mechanisms thoroughly - a real API, a real migration, a real captured pair that genuinely correlates - is the point. It's not an attempt at comprehensive ITSM coverage.