---
title: CI call-site validation (scailog-ci)
path: sdk/ci-validation
status: published
---

# CI call-site validation (scailog-ci)

`scailog-ci` is a build-time linter for your ScaiLog call sites. It statically
scans your source, checks that every `call_site_id` is constant, unique, and
well-formed, that messages carry no interpolated variables, and that every
`pi()` marker is accompanied by a subject — then fails the build if any of those
rules is violated. It can also push a call-site manifest to the server so drift
(a site emitting PI it never declared) shows up in operations.

`scailog-ci` ships with the base SDK and is **zero-dependency**: it uses only the
standard library (`ast` for extraction, `argparse` for the CLI, `urllib` for the
push), so it is available wherever `pip install scailog` has run — no extra
needed.

## What it checks

The extractor AST-scans Python source (`*.py`, recursively through directories)
for emit calls on a ScaiLog logger — `log`/`Logger(...)`/`default_logger()`
receivers, so plain stdlib `logging` calls are ignored — and the `pi()` markers
inside them. The validator (spec §11.2) then enforces these rules; each violation
is a build error unless noted:

| Code | Rule |
| --- | --- |
| `SITE_NON_CONSTANT` | `call_site_id` must be a string constant, never computed from runtime data |
| `SITE_INVALID` | `call_site_id` must match `^[A-Z0-9]+(-[A-Z0-9]+){2,}$` |
| `SITE_RESERVED` | `call_site_id` must not use the reserved `SCAILOG-` / `AUTO-` prefixes |
| `SITE_DUPLICATE` | `call_site_id` must be unique within the scanned service |
| `MESSAGE_INTERPOLATED` | the message must be a constant template — no f-string, `%`, or `.format()`; put variable data in `fields` |
| `PI_WITHOUT_SUBJECT` | a `pi()` marker requires `subject=` at the call site |
| `PI_NESTED` | a `pi()` value must be a scalar or flat list, not a nested structure |

## `check` — fail the build

Run `check` in your pipeline over the directories that contain call sites. It
prints each finding and exits non-zero if any error is present:

```bash
scailog-ci check src/
```

```text
src/checkout.py:42: ERROR [SITE_DUPLICATE] call_site_id 'SCAI-CHECKOUT-PAID-001' is not unique (also at: src/checkout.py:42, src/refund.py:88)
scailog-ci: scanned 37 call site(s), 37 named; 1 error(s), 0 warning(s)
```

## `manifest` — emit the call-site manifest

`manifest` produces the JSON registry of your call sites for a given service and
version — each site with the PI types it declares and its source locations —
without contacting the server. Use it to inspect what would be pushed, or to
commit the manifest as an artifact:

```bash
scailog-ci manifest src/ --service web --version 1.4.2 -o manifest.json
```

Add `--owner <team>` to stamp an owner on the sites. With no `-o`, the manifest
is printed to stdout.

## `push` — upload the manifest to the server

`push` validates first (unless you pass `--no-check`), then uploads the manifest
to `POST /v1/sites/manifest`:

```bash
scailog-ci push src/ --service web --version 1.4.2
```

It refuses to push if validation finds errors. The server URL and API key are
resolved from `SCAILOG_SERVER` / `SCAILOG_API_KEY`, falling back to `server_url`
/ `api_key` in `~/.config/scailog/config.toml` (override the config path with
`SCAILOG_CONFIG`); the default server is `http://127.0.0.1:8080`. Once pushed,
`scailog sites --diff` flags drift — sites observed emitting PI they never
declared, and `AUTO-*` sites that were never registered.
