stilltrue 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,9 +9,14 @@ no server, no dashboard, no SaaS.
9
9
 
10
10
  | Command | Watchdog | Status |
11
11
  |---|---|---|
12
- | `npx stilltrue drift` | Curated facts vs their live authoritative sources — fails the build on rot | **v0.1 focus** |
13
- | `npx stilltrue golden` | Deterministic regression evals for answers | later (may wrap promptfoo) |
14
- | `verify` (library) | Judge answers against their sources in the request path | later (P9-shaped staged pipeline) |
12
+ | `npx stilltrue drift` | Curated facts vs their live authoritative sources — fails the build on rot | shipped |
13
+ | `npx stilltrue report` | Self-contained HTML drift timeline from recorded runs | shipped |
14
+ | `verify` (library) | Staged acceptance pipeline for AI output — judge answers against sources in the request path | shipped |
15
+
16
+ For golden-style regression evals (did a code change break an answer?), use
17
+ [promptfoo](https://promptfoo.dev) — **promptfoo tests your prompts;
18
+ stilltrue tests your facts.** Our eval doctrine and how the tools pair:
19
+ [docs/GOLDEN.md](docs/GOLDEN.md).
15
20
 
16
21
  **Why drift first:** "drift detection" in the market means statistical ML
17
22
  feature drift; website-change monitors alert humans, not CI. Nobody ships
@@ -69,10 +74,38 @@ jobs:
69
74
  deterministic either way). `expect` is your curated file. Rot fails the run
70
75
  with one message per vanished fact; unreachable sources only warn.
71
76
 
77
+ ## Verify — staged acceptance for AI output
78
+
79
+ An ordered, short-circuiting pipeline: each stage is a pluggable judge — a
80
+ Zod parse, a deterministic validator, or an LLM. Rejection is structured
81
+ (`{ stage, messages[] }`, one issue per entry) and designed to be fed back to
82
+ the generator for a retry. Stages that *throw* are infrastructure failures,
83
+ not verdicts: `failPolicy: 'open'` (default) warns and continues — a judge
84
+ outage must never block your app.
85
+
86
+ ```ts
87
+ import { runVerify, zodStage, generateVerified, formatRejection } from 'stilltrue';
88
+
89
+ const stages = [
90
+ zodStage('parse_schema', AnswerSchema),
91
+ { name: 'grounding', timeoutMs: 10_000, check: llmJudge }, // may return a revision
92
+ { name: 'chase-sim', tier: 'ci', check: expensiveSimulation }, // skipped inline
93
+ ];
94
+
95
+ // Request path:
96
+ const result = await runVerify(stages, draft);
97
+ publish(result.value); // draft, parsed draft, or the judge's rewrite
98
+
99
+ // Generator loop (one automatic retry, then fall back — don't loop forever):
100
+ const gen = await generateVerified({ generate: (feedback) => ask(model, prompt, feedback && formatRejection(feedback)), stages });
101
+ ```
102
+
72
103
  ## Status
73
104
 
74
- v0.1 — `drift` is real and running in production (AskGwinnett). `golden` and
75
- `verify` are designed but not yet shipped; see the brief.
105
+ v0.4 — feature-complete for the 1.0 shape: `drift` and `report` run in
106
+ production CI (AskGwinnett), `verify` runs in the production request path
107
+ (AskGwinnett's pre-publish check sweep). Golden is deliberately out of
108
+ scope — settled in [docs/GOLDEN.md](docs/GOLDEN.md).
76
109
 
77
110
  ## License
78
111
 
package/dist/cli.js CHANGED
@@ -95,7 +95,8 @@ try {
95
95
  process.exit(await report(rest));
96
96
  break;
97
97
  case 'golden':
98
- console.error('stilltrue golden: not implemented yet — drift shipped first (docs/BRIEF.md)');
98
+ console.error('stilltrue deliberately has no golden runner — promptfoo tests your prompts; stilltrue tests your facts.\n' +
99
+ 'Use https://promptfoo.dev for prompt/answer regression evals. Doctrine + pairing guide: docs/GOLDEN.md');
99
100
  process.exit(1);
100
101
  break;
101
102
  default:
package/dist/config.d.ts CHANGED
@@ -56,9 +56,8 @@ export interface DriftCheck<E = unknown, A = unknown> {
56
56
  }
57
57
  export interface StilltrueConfig {
58
58
  drift?: DriftCheck<any, any>[];
59
- /** golden and verify are later phases — see docs/BRIEF.md build plan. */
60
- golden?: unknown;
61
- verify?: unknown;
59
+ /** Named verify pipelines (see src/verify.ts) — usable from library code. */
60
+ verify?: Record<string, import('./verify.ts').VerifyStage<any>[]>;
62
61
  }
63
62
  /** Identity function for typed configs: `export default defineStilltrue({...})`. */
64
63
  export declare function defineStilltrue(config: StilltrueConfig): StilltrueConfig;
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { defineStilltrue, type StilltrueConfig, type DriftCheck, type DriftResul
2
2
  export { corpus, json, surname, htmlToText, type CorpusOptions } from './helpers.ts';
3
3
  export { runDrift, runDriftCheck, type RunDriftOptions } from './runner.ts';
4
4
  export { resolveCompare } from './compare.ts';
5
+ export { runVerify, zodStage, formatRejection, generateVerified, type VerifyStage, type VerifyResult, type StageOutcome, type StageContext, type RunVerifyOptions, type GenerateVerifiedOptions, } from './verify.ts';
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { defineStilltrue, } from "./config.js";
2
2
  export { corpus, json, surname, htmlToText } from "./helpers.js";
3
3
  export { runDrift, runDriftCheck } from "./runner.js";
4
4
  export { resolveCompare } from "./compare.js";
5
+ export { runVerify, zodStage, formatRejection, generateVerified, } from "./verify.js";
@@ -0,0 +1,100 @@
1
+ /**
2
+ * `verify` — an ordered, short-circuiting acceptance pipeline for AI (or any
3
+ * generator) output. Modeled on the P9 level-rejection contract, generalized
4
+ * with the review.js production doctrine:
5
+ *
6
+ * - Stages run in a fixed order and stop at the first rejection; later
7
+ * stages may assume earlier ones passed. (P9)
8
+ * - Rejection is structured and machine-readable — `{ stage, messages[] }`,
9
+ * one issue per entry — designed to be fed back to the generator. (P9)
10
+ * - The judge is pluggable: a Zod parse, a deterministic validator, or an
11
+ * LLM. The pipeline neither knows nor cares. (P9 + review.js)
12
+ * - A stage that THROWS is an infrastructure failure, not a verdict.
13
+ * `failPolicy: 'open'` (default) warns and continues — a judge outage must
14
+ * never block the app; `'closed'` treats it as rejection. (review.js)
15
+ * - A rejecting stage may supply a `revision` — a corrected value ready to
16
+ * use (review.js's judge rewrite). The pipeline stops there; the result
17
+ * carries the revision and is marked `revised`.
18
+ * - Tiers: `inline` stages are cheap and run everywhere; `ci` stages are
19
+ * expensive (simulations, live fetches) and run only when asked. (P9)
20
+ */
21
+ import type { ZodType } from 'zod';
22
+ export type StageOutcome<T> =
23
+ /** Pass. `value` optionally replaces the input for later stages (e.g. a parse). */
24
+ {
25
+ ok: true;
26
+ value?: T;
27
+ }
28
+ /** Reject: one issue per message. `revision` is an optional corrected value. */
29
+ | {
30
+ ok: false;
31
+ messages: string[];
32
+ revision?: T;
33
+ };
34
+ export interface StageContext {
35
+ warn: (message: string) => void;
36
+ /** Which tier this run was invoked with. */
37
+ tier: 'inline' | 'ci';
38
+ }
39
+ export interface VerifyStage<T = unknown> {
40
+ name: string;
41
+ check: (input: T, ctx: StageContext) => StageOutcome<T> | Promise<StageOutcome<T>>;
42
+ /**
43
+ * What a THROWN stage means. 'open' (default): infrastructure failure —
44
+ * warn and continue with the input unchanged. 'closed': reject.
45
+ */
46
+ failPolicy?: 'open' | 'closed';
47
+ /** Abort the stage after this long; the timeout follows failPolicy. */
48
+ timeoutMs?: number;
49
+ /** 'inline' (default) runs always; 'ci' runs only with `{ tier: 'ci' }`. */
50
+ tier?: 'inline' | 'ci';
51
+ }
52
+ export interface VerifyResult<T> {
53
+ ok: boolean;
54
+ /** First failing stage; absent on success. */
55
+ stage?: string;
56
+ /** The failing stage's messages only — not cumulative (P9 §2). */
57
+ messages: string[];
58
+ /** Final value: the (possibly transformed) input, or the revision. */
59
+ value: T;
60
+ /** True when a rejecting stage supplied a corrected value. */
61
+ revised: boolean;
62
+ /** Fail-open infrastructure failures and stage warnings. */
63
+ warnings: string[];
64
+ stagesRun: string[];
65
+ }
66
+ export interface RunVerifyOptions {
67
+ /** 'inline' (default) skips `tier: 'ci'` stages; 'ci' runs everything. */
68
+ tier?: 'inline' | 'ci';
69
+ }
70
+ export declare function runVerify<T>(stages: VerifyStage<T>[], input: T, options?: RunVerifyOptions): Promise<VerifyResult<T>>;
71
+ /**
72
+ * Stage helper: a P9-style `parse_schema` gate from a Zod schema. On success
73
+ * the parsed (typed, defaulted, stripped) value replaces the input for later
74
+ * stages; on failure, one message per Zod issue with its path.
75
+ */
76
+ export declare function zodStage<T>(name: string, schema: ZodType<T>): VerifyStage<any>;
77
+ /**
78
+ * Format a rejection as generator feedback — stage id + messages verbatim as
79
+ * bullets (P9 §3: preserve strings so distances and ids stay fixable).
80
+ */
81
+ export declare function formatRejection(result: VerifyResult<any>): string;
82
+ export interface GenerateVerifiedOptions<T> {
83
+ /** Produce a candidate. On retries, `feedback` is the previous rejection. */
84
+ generate: (feedback?: VerifyResult<T>) => T | Promise<T>;
85
+ stages: VerifyStage<T>[];
86
+ /**
87
+ * Total attempts, default 2 (P9 §3: one automatic retry; more wastes
88
+ * tokens — after the cap, fall back or ask a human, don't loop).
89
+ */
90
+ attempts?: number;
91
+ tier?: 'inline' | 'ci';
92
+ }
93
+ /**
94
+ * The generate → verify → feed-back-and-retry loop as a first-class helper.
95
+ * Returns the first passing result, or the LAST rejection after the cap —
96
+ * never silently discards errors (P9 §3.4).
97
+ */
98
+ export declare function generateVerified<T>(options: GenerateVerifiedOptions<T>): Promise<VerifyResult<T> & {
99
+ attempts: number;
100
+ }>;
package/dist/verify.js ADDED
@@ -0,0 +1,96 @@
1
+ async function runStage(stage, input, ctx) {
2
+ const run = Promise.resolve(stage.check(input, ctx));
3
+ if (!stage.timeoutMs)
4
+ return run;
5
+ let timer;
6
+ const timeout = new Promise((_, reject) => {
7
+ timer = setTimeout(() => reject(new Error(`stage timeout after ${stage.timeoutMs}ms`)), stage.timeoutMs);
8
+ });
9
+ try {
10
+ return await Promise.race([run, timeout]);
11
+ }
12
+ finally {
13
+ clearTimeout(timer);
14
+ }
15
+ }
16
+ export async function runVerify(stages, input, options = {}) {
17
+ const tier = options.tier ?? 'inline';
18
+ const warnings = [];
19
+ const stagesRun = [];
20
+ const ctx = { warn: (m) => warnings.push(m), tier };
21
+ let value = input;
22
+ for (const stage of stages) {
23
+ if ((stage.tier ?? 'inline') === 'ci' && tier !== 'ci')
24
+ continue;
25
+ stagesRun.push(stage.name);
26
+ let outcome;
27
+ try {
28
+ outcome = await runStage(stage, value, ctx);
29
+ }
30
+ catch (err) {
31
+ const message = `${stage.name}: ${err instanceof Error ? err.message : String(err)}`;
32
+ if ((stage.failPolicy ?? 'open') === 'open') {
33
+ warnings.push(`fail-open: ${message}`);
34
+ continue;
35
+ }
36
+ return { ok: false, stage: stage.name, messages: [message], value, revised: false, warnings, stagesRun };
37
+ }
38
+ if (!outcome.ok) {
39
+ const revised = outcome.revision !== undefined;
40
+ return {
41
+ ok: false,
42
+ stage: stage.name,
43
+ messages: outcome.messages,
44
+ value: revised ? outcome.revision : value,
45
+ revised,
46
+ warnings,
47
+ stagesRun,
48
+ };
49
+ }
50
+ if (outcome.value !== undefined)
51
+ value = outcome.value;
52
+ }
53
+ return { ok: true, messages: [], value, revised: false, warnings, stagesRun };
54
+ }
55
+ /**
56
+ * Stage helper: a P9-style `parse_schema` gate from a Zod schema. On success
57
+ * the parsed (typed, defaulted, stripped) value replaces the input for later
58
+ * stages; on failure, one message per Zod issue with its path.
59
+ */
60
+ export function zodStage(name, schema) {
61
+ return {
62
+ name,
63
+ check: (input) => {
64
+ const parsed = schema.safeParse(input);
65
+ if (parsed.success)
66
+ return { ok: true, value: parsed.data };
67
+ return {
68
+ ok: false,
69
+ messages: parsed.error.issues.map((i) => `${i.path.length ? i.path.join('.') : '(root)'}: ${i.message}`),
70
+ };
71
+ },
72
+ };
73
+ }
74
+ /**
75
+ * Format a rejection as generator feedback — stage id + messages verbatim as
76
+ * bullets (P9 §3: preserve strings so distances and ids stay fixable).
77
+ */
78
+ export function formatRejection(result) {
79
+ return `Rejected at stage "${result.stage}":\n${result.messages.map((m) => `- ${m}`).join('\n')}`;
80
+ }
81
+ /**
82
+ * The generate → verify → feed-back-and-retry loop as a first-class helper.
83
+ * Returns the first passing result, or the LAST rejection after the cap —
84
+ * never silently discards errors (P9 §3.4).
85
+ */
86
+ export async function generateVerified(options) {
87
+ const attempts = Math.max(1, options.attempts ?? 2);
88
+ let last;
89
+ for (let i = 0; i < attempts; i++) {
90
+ const candidate = await options.generate(last);
91
+ last = await runVerify(options.stages, candidate, { tier: options.tier });
92
+ if (last.ok)
93
+ return { ...last, attempts: i + 1 };
94
+ }
95
+ return { ...last, attempts };
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stilltrue",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Your tests check your code. stilltrue checks your facts. CI checks for AI-in-the-loop apps: drift (curated facts vs live sources), golden (regression evals), verify (judge answers against sources).",
5
5
  "keywords": [
6
6
  "drift",