CodeNSM SDKs — Python, Node, PHP & Java
CodeNSM instruments the functions in your code and rolls up how often each one runs, how reliably, and how slowly. One product, one wire protocol, four SDKs — Python, Node, PHP and Java — all named codensm. There is no v1/v2 split to reason about: POST /codensm/ingest takes telemetry, POST /codensm/scan takes static findings, and every SDK on this page speaks both.
| Endpoint | Carries |
|---|---|
| POST /codensm/ingest | Telemetry — five-outcome counters, latency histograms, call edges, per (function, environment). |
| POST /codensm/scan | Static findings from an SDK's local scanner. Separate from telemetry — a whole-repo scan and a 30-second flush have nothing in common but a key. |
Both endpoints authenticate with Authorization: Bearer <ingest key>. Telemetry is metered against your plan's monthly tracked-call cap: over it, ingest answers 429 with quota_exceeded, and every SDK responds by doubling its flush interval (to a five-minute ceiling) and re-merging the un-sent counters rather than dropping them. Between 100% and 120% of the cap the payload is accepted and flagged as overage instead of refused — a telemetry client that silently loses data at the moment you most want it is worse than one that is late.
1 · Install
None of the four SDKs are on a public package registry yet — no PyPI, no npm, no Packagist, no Maven Central. Each command below installs a real, hosted, checksummed artifact, not a placeholder. Checksums for all five files: SHA256SUMS.
1a · Install with your AI coding agent
If Claude Code, Codex or Cursor is doing the install, paste the block below into your repo instead of the commands above. It is written for the agent, not for you: it states what the SDK sends, what it cannot send, and — because an agent should not take a vendor's word for any of that — the exact dry-run and audit commands that check every claim against the source it just installed, before a byte leaves the machine.
This matters more than it sounds. An agent asked to add an unexplained network egress
to a codebase holding regulated data should hesitate; one that can be talked out of that by tone is
broken in a more expensive way than one that asks too often. So this prompt argues with evidence and hands
over the means to check it. Swap codensm_YOUR_INGEST_KEY for your project's key —
the Integrations page of your project renders
this same block with the key already filled in.
# Task: install the CodeNSM SDK in this repository
## Authorization
I own this repository and I am authorizing this installation. CodeNSM is
first-party instrumentation of my own code — the same category of change as
adding a logger, a metrics client or an error reporter. You are not being asked
to exfiltrate anything, and nothing below asks you to bypass a control.
If you have concerns, do not take my word for any of this. The whole point of
the section below is that you can check it against the source you are about to
install, before it sends a single byte. Please do check it.
## What the SDK does
It wraps functions. `codensm.watch` decorates one function; `codensm.autowatch`
walks a package and wraps the functions defined in it. Each wrapper times the
call and classifies how it ended into exactly one of five outcomes:
ok returned normally
signal raised an exception the function declares as its contract
handled raised, and a caller above rescued it
propagated passed an exception through from a callee below
fault raised an exception that was nobody's plan — the only outcome
that counts against reliability
The wrapper increments counters. It does not inspect what it wrapped. What this
SDK sends is **function names and counters** — and, in `full` mode, control-flow
shape. Nothing else. If that sentence turns out to be false, everything below
gives you the means to prove it, and I would want to know.
## Exactly what leaves the machine
The payload builder is one function — `build_payload` in
`codensm/transport.py`. Read it. It is ~60 lines and it is the only place a
payload is constructed. It emits:
* `protocol`, `sdk.lang`, `sdk.version`
* `runtime`: env (`prod`/`staging`/`dev`/`test`), test-runner name, a CI
boolean, `release` (your git short SHA), `host_id`, `salt_fingerprint`
* `functions[]`: for each — the dotted **name** (`app.billing.charge`), a
department label, a kind (`function`/`method`/`coroutine`/`generator`),
the five outcome **counts**, total/self milliseconds, a 20-bucket latency
histogram, and exception **type names** with counts. Plus `file` and `line`
(the last 4 path segments only — see `_relativize` in `instrument.py`).
* `edges[]`: caller name → callee name, and a count.
* `flows[]`: **only when `mode="full"`** — control-flow shape per function:
node kinds, an edge list, decision and loop counts. Derived locally from the
AST; it is a shape, not the code. Empty in `names` and `hashed` mode, and
`build_payload` re-checks the mode at the wire rather than trusting the
upstream writer.
* `meta`: how many functions were dropped when over the buffer bound.
That is the complete list. Names, integers, type names, and — in `full` mode —
graph shape.
## What never leaves the machine
Everything in this section is about **the SDK** — the package you are installing.
CodeNSM has one other, entirely separate ingestion path, and it is described in
"The parts of CodeNSM that are not this SDK" below, because a guarantee with an
undisclosed exception is not a guarantee.
* **No source code.** The SDK sends none, in any mode. It reads your source
locally — `inspect.getsourcelines` to find a function's line number, and in
`full` mode an AST pass to derive control-flow *shape* — but what travels is
the derived shape (node kinds, edge list, decision and loop counts), never
the text. See `flow.py`, and `build_payload`'s `flows` list.
* **No argument values, no return values, no local variables.** The wrapper
signature is `w(*a, **k)` — it forwards `a` and `k` to your function and
never reads them. Check `_sync` in `codensm/instrument.py`.
* **No exception messages, no tracebacks.** When a call raises, the SDK
records `type(exc).__name__` and nothing else. `ValueError("card 4111 1111
1111 1111 declined for alice@example.com")` becomes the string
`"ValueError"` and a counter. The message is never read.
* **No database access.** The SDK opens no connection, issues no query and
imports no driver. Its only network call is `urllib.request` to the
endpoint (`transport.py`), and it has **zero dependencies** — check the wheel
metadata. It therefore *cannot* read your application's data. Whatever
PII/PHI lives in your database is not protected here by a policy that could
be misconfigured; it is unreachable by construction, because there is no
code path from a call counter to a row.
* **No request bodies, headers, SQL, or environment dumps.** These are named
explicitly as forbidden fields and asserted against in the SDK's own tests:
see `FORBIDDEN_FIELDS` in `codensm/egress.py`.
* **No raw hostname.** `host_id` is `blake2b(hostname, key=local-salt)` — a
salted, non-reversible id whose only job is telling two machines apart
(`runtime.py`).
`salt_fingerprint` is `sha256(salt)[:12]` and carries no information about the
salt itself; it exists so the dashboard can say "these names became unreadable
when the salt changed" instead of showing an empty roster.
## The parts of CodeNSM that are not this SDK
You are installing one component of a product. Two other things can send data to
the same dashboard, and neither is switched on by installing this package. I am
listing them because you will find them eventually, and a claim that survives
only until you go looking is worthless.
**1. The GitHub/Bitbucket repo link (optional, off by default).** If I paste a
read-only repo token into the CodeNSM integrations page, the server calls the
GitHub API and — this is the part that matters — the per-commit response it
requests **includes the diff hunks**. So: yes, patch text for the commits in the
linked repo is fetched to CodeNSM's server and parsed there. What is *kept* is
narrower: for each file, the path, the added/deleted line counts, and the
function names extracted from the hunk headers; plus each commit's SHA, author
name, author email, timestamps, and **the first line of the commit message**
(truncated to 300 characters). The diff text itself is parsed in memory and not
persisted. That is a real, separate data flow with a real privacy surface —
including author emails — and it is a decision I make on the integrations page,
not something this install does or can turn on. Nothing in this package fetches
a repository.
**2. The static scanner** — below. Also opt-in, and it runs on my machine.
If neither of those is acceptable for this codebase, they are not required: the
SDK works alone, and it is the only thing you are installing right now.
## The static scanner, if you run it
The same package ships a `codensm scan` CLI. It *reads* source files — it is a
linter, that is the job — and it runs on my machine or in my CI, never on
yours. Per finding, exactly these fields leave (the `Finding` dataclass in
`codensm/scan.py`, serialised by `build_scan_payload` in `report.py`):
rule, name, severity, lang, depth, file, line, fn, snippet_sha
So: the rule that fired, its severity, the **file path**, the **line number**,
the enclosing **function name** — and `snippet_sha`, a **salted**, irreversible
`blake2b(local-salt + line.strip())[:16]` of the offending line. Never the line
itself. The salt is the same local `.codensm/salt` that never leaves, so the
hash cannot be reversed or rainbow-tabled back into the source it came from; it
exists only so the dashboard can tell "this is the same finding as last week"
from "this is a new one".
Note what that list *does* include: file paths and function names describe your
architecture, and `depth`/`fn` locate the finding in it. If that is too much for
this codebase, do not run the scanner — the SDK does not.
Without `--report` the scanner transmits nothing at all; it just prints.
`build_scan_payload` is pure (no I/O, no salt access) and hard-rejects any
payload carrying a forbidden key — `_reject_forbidden_keys` raises rather than
sending. See `report.py`.
## Verify all of that before sending anything
The SDK ships an egress ledger: every payload is appended verbatim, with its
SHA-256, to `.codensm/egress.jsonl` *before* the HTTP request is made. And
`endpoint=None` is a dry run — it builds and ledgers payloads and transmits
nothing. So the honest order of operations is:
import codensm
codensm.init(endpoint=None) # dry run: sends nothing, ledgers all
codensm.autowatch("yourpackage")
# ... exercise the app, or just run the test suite ...
codensm.flush()
print(codensm.explain_egress()) # every JSON field path that would leave
print(codensm.audit_egress()) # [] means no forbidden field, ever
`explain_egress()` prints the complete set of field paths the ledger has ever
contained. If `source`, `args`, `message` or `traceback` appeared there, the
product would be lying and you would be holding the receipt. `audit_egress()`
returns the forbidden paths it found; an empty list is the pass, and it is the
same assertion the SDK's test suite makes in `tests/test_privacy.py`.
Run the dry run first. If the output does not match the claims above, stop and
tell me — that is a bug I need to know about, and you will have found it.
## Install (Python)
pip install https://thinknorth.consulting/static/codensm/dist/codensm-1.0.0-py3-none-any.whl
Then, once, at the earliest point of process startup — before the modules you
want instrumented are imported:
import codensm
codensm.init(
ingest_key="codensm_YOUR_INGEST_KEY",
endpoint="https://thinknorth.consulting/codensm/ingest",
mode="names",
env="prod",
)
codensm.autowatch("yourpackage")
`mode="names"` is the default and is what is configured here: dotted names,
counters, histograms, exception type names. `mode="hashed"` additionally
replaces every name with `HMAC-SHA256(local-salt, name)[:16]`; the salt is
generated on this machine, stored at `.codensm/salt` with mode 0600, and is
never a field in any payload — so in that mode the server *cannot* recover the
names. Not "does not": cannot. Read `codensm/names.py`.
### Node
npm install https://thinknorth.consulting/static/codensm/dist/codensm-1.0.0.tgz
const codensm = require("codensm");
codensm.init({ ingestKey: "codensm_YOUR_INGEST_KEY", endpoint: "https://thinknorth.consulting/codensm/ingest", mode: "names", env: "prod" });
### PHP / Java
Same wire protocol, same guarantees; artifacts and snippets are on the docs
page. PHP: `CodeNSM::init([...])`. Both are dependency-free ports of the module
layout above, and the conformance vectors in `clients/conformance/` assert that
all four languages produce byte-identical bucketing.
## Import order matters (the one real footgun)
Instrumentation works by rebinding a module's attributes. A module that has
already captured a *reference* to a function keeps the uninstrumented original
forever:
# urls.py, evaluated at import time
urlpatterns = [path("", views.landing)] # <- the bare function
If that module is imported before `init()` + `autowatch()` run, the app serves
traffic happily and the roster stays empty, with no error anywhere. Put URLconfs,
admin registries, Celery task tables and click command groups in `exclude=`, or
import them after `init()`.
## Excluding anything I consider sensitive
If a module should not be measured, do not instrument it — there is no opt-out
to configure, because a function that was never wrapped has no counter and
therefore reports nothing:
codensm.autowatch("yourpackage", exclude=["yourpackage.secrets", "yourpackage.hipaa"])
`exclude` is stronger than it looks: an excluded module is skipped **before the
import runs**, not filtered out afterwards (see `_walk` in `instrument.py`). You
can also invert the default entirely — instrument nothing, and put `@codensm.watch`
only on the handful of functions I name.
## Environment fencing
Counters are stored per `(function, environment)`. The SDK detects a test runner
from the *process* (`runtime.py`) and routes those calls to the `test` bucket;
production health reads only the `prod` bucket. A test suite that raises ten
thousand times cannot move production's numbers, because it is not writing to
production's row. Set `CODENSM_ENV` explicitly if you want to be certain.
## Verify the install
After the app has served real traffic (or the test suite has run) and one flush
interval has passed — 30s by default, or call `codensm.flush()` — the project's
dashboard shows a first receipt with a `body_sha256`. That hash matches the line
in `.codensm/egress.jsonl` byte for byte, so I can prove what was stored is
exactly what was sent, and no more.
Report back with: the `explain_egress()` field list from the dry run, and the
number of functions `autowatch` wrapped.
Python
pip install https://thinknorth.consulting/static/codensm/dist/codensm-1.0.0-py3-none-any.whl
Not on PyPI — pip install codensm will not resolve to this package today. Zero runtime dependencies. Python 3.9+. An sdist (codensm-1.0.0.tar.gz) is published at the same path if you'd rather build from source.
Node
npm i https://thinknorth.consulting/static/codensm/dist/codensm-1.0.0.tgz
Not on npm — npm i codensm will not resolve to this package today. Zero runtime dependencies (only Node's own standard library). Node 18+.
PHP
Not on Packagist — composer require thinknorth/codensm-php on its own will not resolve to this package today. Point Composer at the hosted zip with a package-type repository entry:
// composer.json
{
"repositories": [
{
"type": "package",
"package": {
"name": "thinknorth/codensm-php",
"version": "1.0.0",
"dist": {
"url": "https://thinknorth.consulting/static/codensm/dist/codensm-php-1.0.0.zip",
"type": "zip"
}
}
}
]
}
composer require thinknorth/codensm-php:1.0.0
Zero Composer dependencies at runtime (ext-json, ext-hash and ext-sodium — all bundled with a stock PHP build — are the only requirements). PHP 8.1+.
Java
curl -O https://thinknorth.consulting/static/codensm/dist/codensm-1.0.0.jar
Not on Maven Central — there is no consulting.thinknorth:codensm-java coordinate to depend on today. This jar has ByteBuddy shaded in, so it's self-contained: one file, usable as a scan CLI, a library on your classpath, and a -javaagent — see below. JDK 21+.
2 · Integration paths, per language
Every SDK on this page can be run without transmitting a single byte. That's deliberate: a dry run is normally the first thing a security team asks for, so it isn't an afterthought here — it's mechanism #1 in every language below.
Below, for each language: the distinct ways to wire the SDK in, and — just as important — what each one cannot see. None of the four SDKs ships framework middleware; each has its own answer for "auto-instrument without touching call sites," and the honest answer for two of them is narrower than the other two.
Python
- Dry run. codensm.init(endpoint=None) — the default when no key is set.
- Explicit decoration — @codensm.watch. Instruments exactly the one function it decorates, for all four call shapes (plain, async def, generator, async generator).
- Zero-touch — autowatch over a package prefix. Walks a package recursively (pkgutil.walk_packages) and rebinds every function/method it finds. What it cannot see: a module that captured a bare reference to a function before autowatch() ran — a Django urls.py built at import time (urlpatterns = [path("", views.landing)]) keeps the original, uninstrumented function forever if it imports before its views get wrapped. Exclude such modules, or import them after init().
- Contracts — raises=(...) on @codensm.watch, or a separate @codensm.contract(raises=(...)). Declares which exceptions are the function's job, not its failure.
- The CLI, for CI — codensm scan / codensm scan --report (installed as a console script by the wheel).
import codensm codensm.init( ingest_key="codensm_…", endpoint="https://thinknorth.consulting/codensm/ingest", ) codensm.autowatch("apps", departments={"apps.pay": "ledger"}, exclude=("apps.urls",)) @codensm.watch(department="ledger", raises=(CardDeclined,)) def charge(order): ...
No framework middleware ships — autowatch is the zero-touch path; @watch is the precise one.
Node
- Dry run. codensm.init({ endpoint: null }) — also the default with no key.
- Explicit decoration — codensm.watch(fn, opts) returns a wrapped function; handles plain, async, generator and async-generator shapes.
- Zero-touch — autowatch over CommonJS globs. A Module._load require hook wraps every exported function/method in a CJS module whose resolved path matches a glob, for modules required before or after autowatch() runs. What it cannot see: ES Modules. A require hook has no visibility into import, and an ESM namespace object is immutable — you cannot reassign ns.foo after the fact. There is no ESM auto-instrumentation; wrap at the definition site instead (codensm.watch) or call codensm.instrumentModule() on a plain mutable exports object.
- Contracts — raises on watch(), or codensm.contract(fn, { raises }).
- The CLI, for CI — npx codensm scan / npx codensm scan --report (resolves the local install's bin/codensm.js; the package isn't on npm, so this only works after installing the tarball above).
const codensm = require("codensm"); codensm.init({ ingestKey: "codensm_…", endpoint: "https://thinknorth.consulting/codensm/ingest", }); codensm.autowatch(["./src/**", "**/services/**"], { departments: { "src/pay": "ledger" } }); const charge = codensm.watch(chargeImpl, { department: "ledger", raises: [CardDeclined] });
No framework middleware ships. both require("codensm") and import codensm from "codensm" work — same object either way.
PHP
- Dry run. CodeNSM::init(['endpoint' => null]) — also the default with no key.
- Explicit decoration — CodeNSM::watch($callable, $opts) returns an instrumented Closure. Works for named functions, closures, [$obj, 'method'], ['Class', 'staticMethod'] and invokables. This is the primary, reliable path.
- Attribute-based discovery, not true auto-instrumentation. PHP has no import hook, so there is nothing that transparently wraps every method in a package the way Python's or Node's autowatch does. What exists instead: mark methods with #[Watch], call CodeNSM::autowatch([Billing::class]) to register their metadata and contracts (it returns a count, discovers, does not intercept), then CodeNSM::wrap($target) to get a forwarding proxy whose #[Watch] methods actually run through the taxonomy. Honest limit: the proxy is not instanceof the wrapped class, so it will not satisfy a type-hint expecting the original — use it at a composition seam (a container binding), not as a drop-in replacement.
- Contracts — 'raises' => [...] in watch()'s options, or CodeNSM::contract($fn, $raises).
- The CLI, for CI — vendor/bin/codensm scan / vendor/bin/codensm scan --report (falls back to a bundled PSR-4 loader and works from a bare checkout with no Composer install at all — php bin/codensm scan .).
use CodeNSM\CodeNSM; use CodeNSM\Watch; CodeNSM::init([ 'ingestKey' => getenv('CODENSM_INGEST_KEY'), 'endpoint' => 'https://thinknorth.consulting/codensm/ingest', ]); #[Watch(department: 'ledger', raises: [CardDeclined::class])] class Billing { public function charge(Order $o): Receipt { ... } } CodeNSM::autowatch([Billing::class]); // discovers #[Watch] metadata $billing = CodeNSM::wrap(new Billing()); // intercepts through it
No framework middleware ships, and there is deliberately no source-rewriting autoloader — an SDK that silently defeats opcache is worse than one that asks you to name your boundaries. Fibers are not supported; do not wrap code that suspends across Fiber::suspend().
Java
- Dry run. CodeNSM.init(CodeNSM.options().endpoint(null)) — also the default with no key.
- Explicit decoration — CodeNSM.watch(name, () -> ...) / CodeNSM.run(name, act). Preserves and re-throws the exact exception object; checked exceptions are fine in the body.
- Zero-touch — a real -javaagent. The only one of the four SDKs that does true bytecode weaving: ByteBuddy advice around every method in the packages you name, injected at class-load, with the SDK's own packages, ByteBuddy and the JDK always excluded. What it cannot see: any package not named on the command line — -javaagent:codensm-1.0.0.jar=com.acme.pay instruments com.acme.pay and nothing else. The agent is also inert until CodeNSM.init(...) runs — weaving and measurement are independent, so call init early in main.
- Contracts — CodeNSM.contract(name, CardDeclined.class).
- The CLI, for CI — the same jar. The jar's manifest sets both Premain-Class (the agent) and Main-Class (the scan CLI), so java -jar codensm-1.0.0.jar scan . runs the static scanner with no second artifact.
import consulting.thinknorth.codensm.CodeNSM; CodeNSM.init(CodeNSM.options() .ingestKey("codensm_...") .endpoint("https://thinknorth.consulting/codensm/ingest")); int total = CodeNSM.watch("consulting.pay.Service.charge", () -> service.charge(order)); CodeNSM.contract("consulting.pay.Service.charge", CardDeclined.class);
java -javaagent:codensm-1.0.0.jar=com.acme.pay,com.acme.checkout \
-Dcodensm.departments=com.acme.pay=ledger,com.acme.auth=gateway \
-jar your-app.jar
No framework middleware ships. Honest limit: the frame stack is a plain ThreadLocal and does not follow a CompletableFuture.thenApplyAsync (or any executor) hop onto a different thread — a callback scheduled that way is measured as its own independent call, not a child of the frame that scheduled it. Instrument the callback method itself (agent or watch) to see it. Virtual threads (JDK 21) are supported correctly — each starts with its own empty stack.
3 · The five outcomes
A test that throws on purpose is not an outage, and a declared Http404 does not make a service look broken.
A tool that records "did an exception cross this function, yes or no" cannot tell a route that's supposed to raise Http404 apart from a payment processor that just went down. CodeNSM partitions every instrumented call into exactly one of five outcomes, so calls = ok + signal + handled + propagated + fault:
| Outcome | In plain language | Is it an error? |
|---|---|---|
| ok | The function ran and returned normally. | No |
| signal | It raised an exception, but that's part of the job — a page that legitimately doesn't exist, an exception declared as part of the function's contract, a test exercising its own failure path on purpose. | No |
| handled | It raised something nobody declared, but an instrumented ancestor already knew how to catch and recover from it. | No |
| propagated | Something below this function broke, and this function was simply standing in the way while it passed through — not the cause. | No |
| fault | It raised something nobody declared, nobody caught, and nobody recovered from — the exception got all the way out. | Yes — the only one |
fault_rate = fault / calls. reliability = 1 − fault_rate. The classification happens in the process, on the stack, as the exception moves — never afterwards from a function's name. There is no regex anywhere in this, on either end.
4 · Environments
Every counter is stored per (function, environment), where environment ∈ {prod, staging, dev, test}, detected from the process — never guessed from a function's own name:
| env | Detected from |
|---|---|
| prod | The default, when nothing else matches. |
| staging | CODENSM_ENV |
| dev | CODENSM_ENV / NODE_ENV / APP_ENV, etc. |
| test | A test runner detected from the process itself — pytest, jest/vitest/mocha/node:test, phpunit/pest, JUnit/TestNG — never from a function's own name. |
This is a property of the storage key, not a filter someone has to remember to apply on a dashboard. Counters are keyed by function name only inside a payload; the environment rides once, at the top, as a property of the process that sent it, and the server fans it out into per-(function, env) rows. A pytest run reporting env=test is physically unable to write into a prod row — the environment is half of the row's identity, not a tag a query could forget to check. A test suite that throws ten thousand times on purpose cannot move production reliability by so much as one call, and nobody has to remember why.
5 · The static scan
Every SDK on this page also carries a local static scanner — twelve rules, shared byte-for-byte across all four languages from one JSON contract (some rules are null for a language whose syntax the rule doesn't apply to; the report says so explicitly rather than silently skipping) — looking for the kind of thing a linter finds (SQL built by string concatenation, a bare catch, a hardcoded secret) before a single request has ever hit the code.
codensm scan # scan the current tree, print findings, exit 1 on a critical finding codensm scan path/ --json # emit the findings array as JSON instead of a human report codensm scan --fail-on warn # fail the run on warn or worse, not just critical codensm scan --report # also transmit findings — see below
codensm scan never leaves your machine — findings print to your terminal and nothing is sent. codensm scan --report transmits them, reading CODENSM_INGEST_KEY / CODENSM_ENDPOINT the same way the telemetry SDK does; point CODENSM_ENDPOINT at your ingest URL and the scan endpoint is derived automatically (the /ingest suffix becomes /scan). With neither configured, --report writes to .codensm/scan.jsonl locally instead of opening a socket — and either way, a transmission attempt never changes the command's exit code, which is decided from the findings alone before --report gets anywhere near a socket.
The complete flag
A finding that stops being reported has exactly two possible explanations: somebody fixed it, or the scanner never looked at that file this time. Those are opposite facts that show up as the same absence — silence. Only the client knows which one happened, so the client has to say so.
complete is true only when you ran codensm scan against the whole tree with no path narrowed and no --exclude filters. Only then may the server treat a finding that used to be open and is no longer reported as resolved. Scan a subdirectory, or add an --exclude, and complete is false: the server upserts whatever you found and resolves nothing else — a partial scan can add findings, it can never clear them by omission.
The ranking is the product
Every one of the twelve rules is something a plain linter can already find. What a linter cannot do is tell you which instance of a finding matters. CodeNSM's ranked findings report re-ranks every open finding by the runtime traffic actually flowing through the function it sits in, and says, in the report itself, whenever it changed a severity:
- Raised one severity notch when the finding sits in a function carrying the top decile of the project's measured production value — load-bearing code.
- Lowered to the weakest severity when the finding sits in a function that has never recorded a single production call — dead code carries no runtime risk right now, whatever its shape suggests.
- Lowered to the weakest severity when the finding sits in a test function — a test throwing the exact exception it's asserting on is the test doing its job, not production debt.
Findings with nowhere to be attributed — a module-level constant, a stray top-level statement — are listed separately as unranked, not silently dropped and not folded into the nearest function's score. See the ranked report on your project dashboard.
6 · Privacy — what leaves your machine
Every SDK ships three modes. The default is the least private of the three and still ships no source:
| Mode | What leaves the machine |
|---|---|
| names | (default) Function names, counters, latency histograms, exception type names, call edges. |
| hashed | The same, but every function name becomes blake2b/HMAC-SHA256(local_salt, name)[:16], and file paths are dropped entirely. The salt lives in .codensm/salt (mode 0600) and is never itself a field in any payload — the server cannot read your function names. Domain exception types are hashed too; public ones (TimeoutError, java.util.concurrent.TimeoutException) still travel intact. |
| full | Additionally, a locally-derived control-flow shape — node/edge kinds, decision and loop counts, never source. Only the Python SDK emits this today. Node, PHP and Java accept full without erroring, but currently send the same payload as names — each SDK's own README says so, rather than silently sending nothing where a shape was expected. |
Never transmitted, in any mode
Travels even in names mode
Static findings carry even less
A scan finding never carries a line of your source — not even in names mode. In place of the line it carries snippet_sha: blake2b(salt + line.strip())[:16], hex-encoded, 16 characters. That's enough to recognise the same finding again tomorrow, or notice that the line moved — and not enough to recover one character of what was on it. (Java's runtime ships no BLAKE2b provider, so its build uses SHA-256 truncated the same way and says so plainly rather than calling it the same digest; the value is never compared across languages, only against itself run to run, so this does not weaken anything a customer relies on.)
Enforced twice, not once
mode="hashed" hashes function names and drops file paths — enforced in the SDK, and separately refused at the server for a project configured for hashed mode: a payload carrying a file field is rejected, not silently accepted and stored. The server is the half of this system a customer cannot inspect by reading a repository, so it is the half that has to hold the line on its own rather than trusting the client to have applied the rule correctly.
The egress ledger
Every payload — sent or dry-run — is appended verbatim, with its own SHA-256, to .codensm/egress.jsonl on your disk before the request is made. A security team doesn't have to trust this page: they can read that file and see exactly what left, byte for byte.
codensm.init(endpoint=None) # dry run: writes .codensm/dryrun/*.json, sends nothing codensm.flush() codensm.explain_egress() # every JSON field path that has ever left this machine codensm.audit_egress() # [] is the pass condition — asserts no source/args/messages
Same shape in every SDK: explainEgress()/auditEgress() in Node, explainEgress()/auditEgress() in PHP, explainEgress()/auditEgress() in Java.
7 · The honest migration note
CodeNSM has been accumulating FunctionStat.calls / .errors / .remedied since before this taxonomy existed. Those columns still feed the dashboard, so read this before you assume a chart that moves means production changed.
What's preserved. errors keeps being written as calls − ok — every non-ok outcome, exactly what the retired instrumentation would have reported — so a function's historical series continues along the line it was already on rather than visibly bending the day outcome telemetry starts. remedied — a column no SDK, retired or current, has ever written a nonzero value into — finally gets populated, as handled: the frame raised, an instrumented ancestor caught it, the system recovered.
What is not preserved, and this is the part not to skim. The retired instrumentation caught except Exception. The current one catches except BaseException. SystemExit, KeyboardInterrupt, GeneratorExit and asyncio.CancelledError are not Exception subclasses — the retired wrapper recorded every one of them as a clean, error-free call, silently. The current one sees them, classifies them as signal, and that non-ok outcome is exactly what the legacy errors count above counts, for continuity. Separately, the retired wrapper never instrumented a generator's body at all — it timed the call that created the generator and never saw what happened during iteration; the current instrumentation instruments every resumption and records what the body actually raised.
So: an async service or a generator-heavy codebase will see its errors count step up on the day it upgrades its instrumentation, without anything in production having changed. That is not a bug — it is two classes of event the retired wrapper was blind to, now finally visible. But if you are watching that number expecting continuity, expect this instead.
Both divergences are demonstrated against the retired wrapper's own code — vendored verbatim — rather than asserted from the bridge's own arithmetic, in apps/nsm_protocol/tests/test_legacy_divergence.py. The honest, narrower claim: the bridge reproduces the retired instrumentation exactly for every Exception-derived outcome on a non-generator function, and newly counts two classes of event it could never see — it is not a claim that nothing about the series changes. FunctionStat.protocol records whether a row has ever reported outcome telemetry, so any surface can say whether what it's showing is measured or inherited — rather than presenting both with the same confidence.
One more trap worth knowing even if you never touch it directly: for a function with outcome telemetry, errors − remedied equals signal + propagated + fault — not fault. If you want the one number that actually reflects reliability, that's fault itself (or the dashboard's own reliability figure), never a subtraction on the legacy columns.
8 · The front-end tag
There is a fifth SDK, and it is not a package you install — it is one line of HTML, the way you'd add Analytics or Clarity. It speaks the very same five-outcome wire protocol to the very same /codensm/ingest, so the office, the roster, the flow view and the health score show your front-end with nothing new to learn. One static file, no build step:
<script src="https://thinknorth.consulting/static/codensm/tag.js"
data-key="codensm_…"
data-endpoint="https://thinknorth.consulting/codensm/ingest"
data-app="web"></script>
It reports under environment browser — a first-class citizen alongside prod, held out of production health for the same reason test is: a visitor's browser throwing a TypeError is not your service failing.
What becomes a function
- A page view is a function named web.page.<path>, with dynamic segments collapsed so a million profiles are one function, not a million: /users/12345/orders → web.page.users/:id/orders. A session is capped at 200 distinct page names; the overflow lands in web.page.__other rather than growing without bound.
- An interaction is a function named web.ui.<name> — but only for an element you have explicitly opted in by giving it a data-cn attribute. There is no automatic click harvesting; an unmarked button is invisible to the tag.
- A back-end call — every fetch and XMLHttpRequest — becomes a function web.api.<METHOD> <path> and a runtime edge from the page that fired it, so the office shows the front-end reaching across to the endpoint it depends on.
How the five outcomes map
| Front-end event | Function | Outcome |
|---|---|---|
| page rendered / marked interaction | web.page.* / web.ui.* | ok |
| JS error (onerror / unhandledrejection) | web.page.* | fault |
| fetch/XHR answered 2xx–3xx | web.api.* | ok |
| fetch/XHR answered 4xx | web.api.* | handled |
| fetch/XHR answered 5xx | web.api.* / web.page.* | fault / propagated |
| network failure / timeout | web.api.* / web.page.* | fault / propagated |
A 4xx is handled, not a fault: the front-end asked for something it wasn't entitled to and dealt with the answer — the same reasoning that keeps a declared exception off the reliability score. A 5xx or a dead socket is a fault on the endpoint that broke and propagated on the page that was merely standing next to it — a bystander, not the cause.
What leaves the browser
The same posture as every other SDK, drawn tighter for a place you don't control. No cookies, no localStorage, no user, session or device identifier is ever created or read. The only strings that travel are URL paths with their dynamic segments collapsed, the data-cn names you chose, and exception type names — never a query string, a hash fragment, a request or response body, a header, or an exception message. Interactions are opt-in per element, so the tag sees a text field's label only if you named it, and its contents never.
9 · Where it shows up
Calls, outcomes and latency roll up — in the cloud — into per-department KPIs, a debt-that-hurts heatmap, underuse detection, the ranked static-scan report, and a single North-Star Metric, all on your project dashboard. Functions appear as they're actually called.