feat: gate, packs, records, and offline development (v0.1.2) - #6
Conversation
Adds the decision layer the CLI was missing: questions are judged by the model, policy is applied offline to a saved answer. - `jev gate --input <judgment|record> --pack <name>|--policy <file>` evaluates rules offline and exits 0 accept / 2 review / 3 deny / 4 abstain. Fail-closed: a missing or wrong-typed answer abstains, and a choice label outside `accept` never accepts, however confident the model is. - `packs/` ships versioned question sets with embedded policies: verify (claim vs. evidence), screen (injection, harmful content, severity; plus substance and relevance for the caller), route (handler class + complexity). `jev packs` lists them with a content hash; `ask --pack <name>` uses one. - `--record <path>` writes a decision record: model resolved, latency, pack hash, and hashes of the state and questions — the state itself is never stored. `jev replay --record <file>` re-emits stored answers without a call. - `jev doctor [--live]` checks engine, key presence (never the value), base URL, model, and pack validity; `--live` also authenticates, lists models, and times a probe. - `--version` now reads package.json at runtime instead of a hardcoded string. - Fix: SIGINT/SIGTERM cancellation was dropped — the signal travelled in client options, which ignore it, instead of request options. - Fix: `batch` JSON rows bypassed cost enrichment, so they lacked the `cost` block every other JSON response carries. The publish workflow stages instead of publishing: trusted publishers created after 2026-09-03 are staging-only, which is why run 35279646306 received "OIDC permission denied" after signing provenance. It now requires the release tag to match the committed version (rather than rewriting it), installs a staging-capable npm CLI (>= 11.15.0), and runs `npm stage publish` for a maintainer to approve with 2FA. Offline development: `tools/stub-server.mjs` answers deterministically and non-committally so a pipeline can be wired without a key, and `tools/evaluate.mjs` scores a policy against labeled records (accuracy, outcome mix, acceptance rate per probability bucket) without touching the inference path.
…ords
Addresses review findings on the gate. Each one was a path where a malformed or
ambiguous input could still reach `accept`:
- Choice rules no longer fall back to `confidence` when the probability map is
missing or lacks the chosen label. Confidence describes the answer as a whole,
not that label, so substituting it let `{"supports": 2}` accept. The map must
now be numbers in [0, 1] summing to 1 (tolerance 0.05); anything else abstains.
- Score rules check the scale: the score must fall inside the rule's `range`, or
the level indices the answer reports in legend/probabilities. `severity: -100`
sailed under `accept_at: 0.5` before.
- Record envelopes are validated, never reinterpreted: a `record_version` this
CLI does not write, or a non-object `response`, exits 1 instead of falling back
to a conflicting top-level `answers`.
- Gating a record against its pack now checks identity. Changed questions exit 1
(stored answers no longer mean what the rules assume); changed thresholds only
warn and apply, because the answers still mean the same thing. The pack hash and
that comparison print in table output too, not just JSON.
- State hashes are type-tagged and versioned, so a text state no longer collides
with the JSON state that parses to the same bytes.
- `--record` creates missing parent directories before the request, so a bad path
cannot fail after an answer was paid for.
- Lint rejects an inverted choice band (`review_at > accept_at`), a repeated
accepted label, and a malformed `range`; evaluation counts each accepted label
once regardless.
- `tools/evaluate.mjs` buckets by the probability the policy treats as permission
for the primary rule, not by the chosen label's probability. The old metric
mixed confident `supports` with confident `contradicts` and could call good
separation flat. Score rules are excluded from the curve; they are not
probabilities.
- The stub never claims the requested model: `model_resolved` is `stub:<requested>`,
so a stub record is identifiable as one. A contract test asserts, through the
real transport, that all three bundled policies deny stub answers — the claim
the docs make.
- Docs: narrowed the record-hash claim to an input commitment, documented the
fail-closed rules and pack-drift behavior, and fixed examples that did not run
(record path, route pipeline, stub accuracy).
|
Review response — Astra (slow role), every finding triaged. All blocking and important items are fixed in 61343f9; the two unverifiable CI claims are addressed below. BLOCKING1. Invalid choice probabilities could accept. Confirmed, and worse than stated: 2. Out-of-range scores could authorize. Confirmed. A score is now checked against its scale — the rule's optional 3. Ambiguous record envelopes. Confirmed. IMPORTANT4. Pack mismatch did not affect authorization. Fixed, split by what changed, because the two cases mean different things:
5. State hash collisions. Fixed: the hash is now a versioned, type-tagged envelope, so text 6. Choice lint gaps. Both confirmed: 7. Calibration mixed opposite outcomes. Confirmed — bucketing the chosen label's probability against "is the ground-truth label acceptable" mixed correct 8. Stub could claim a real model. Fixed: the stub never echoes the requested model — MINORAll four fixed: Publishing verificationYou are right that the diff alone cannot establish the staging claims. Evidence, from the npm docs rather than the diff:
Local check: the npm CLI here is 11.6.2 and reports Verification91 tests pass (up from 82): new coverage for malformed probability maps, out-of-scale scores, record envelope rejection, state-hash distinctness, pack question drift, threshold drift, and the stub-denial claim. Re-ran the live API afterwards — |
What
Grows
jev-clifrom a transport wrapper into a decision tool. Inference stays exit 0 — policy moves tojev gate, which is offline.denyandabstainare separate codes because remediation differs.Why
Two external reviews of the 0.1.0 surface agreed on the same gap: the CLI returns probabilities, and every caller then hand-rolls thresholds.
gateis that code, once, with documented semantics — fail-closed, and reproducible because it reads a saved answer rather than calling the model.Changes
jev gate— rules overnoul/choice/scoreanswers withaccept_at/review_atbands,mode: all|any,optionalrules, and per-rule reasons on stdout. A choice label outsideacceptnever accepts regardless of confidence; what can soften a denial toreviewis probability mass on an accepted label. A missing or wrong-typed answer abstains. Policies are validated on load, soreview_at > accept_atfails loudly instead of never firing.packs/—verify(claim vs. cited evidence),screen(injection, harmful content, severity, plus substance/relevance for the caller; advisory, not a security boundary),route(deterministic/specialist/human/none + complexity). Each carries a state contract, an embedded policy, and a content hash that records and gate output name.--recordwrites model resolved, latency, pack hash, and hashes of state and questions. The state is never stored, so a record can sit next to a log without carrying confidential text.replayre-emits stored answers marked"replayed": true; it is not a rerun.jev doctor [--live]— engine, key presence (never the value), base URL, model, packs;--liveadds auth, model list, probe latency, and cost.--versionreadspackage.jsoninstead ofconst VERSION = "0.1.0".formatOutput, so they lacked thecostblock the README promises for every JSON response.tools/stub-server.mjsanswers deterministically and non-committally (last choice option, middle score, noul 0.5) so a stub run cannot look like an approval;tools/evaluate.mjsscores a policy against labeled records (accuracy, outcome mix, per-bucket acceptance rate).examples/ships inputs and labels only — no committed model output.Release unblock
Run 35279646306 signed provenance, then failed:
403 Forbidden — OIDC permission denied. Trusted-publisher connections created after 2026-09-03 are staging-only (npm stage publishis always allowed; directnpm publishis opt-in), which matches the error exactly. The workflow now:npm stage publishneeds >= 11.15.0; 11.6.x reports "Unknown command: stage"),npm stage publish --access publicand prints the stage id for 2FA approval.v0.1.1pointed at the same commit asv0.1.0and never reached the registry;v0.1.2supersedes it.Verification
npm test), up from 42:gate.test.tscovers every decision, both precedence modes, optional/missing/mismatched answers, lint rejections, record round-trips, and pack loading;contract.test.tsspawns the real CLI for exit codes 0/2/3/4,--versionagainstpackage.json, batch cost blocks, and a doctor run that proves the key value never reaches stdout.doctor --live(modeljev-1.13.0, 856 ms model list, 923 ms probe) and the verify/screen packs —supports→ exit 0,contradicts→ 3,says_nothing→ 3, and the sample injected page →injection 0.98→ exit 3 with severity inreview.tools/evaluate.mjsover those live records: 3/3 label accuracy, 1 accept / 2 deny, and it skips records with no label.Notes
lintis structural only.