stilltrue 0.4.0 → 0.5.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 +125 -74
- package/dist/cli.js +19 -1
- package/dist/init.d.ts +6 -0
- package/dist/init.js +78 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -2,111 +2,162 @@
|
|
|
2
2
|
|
|
3
3
|
> Your tests check your code. **stilltrue** checks your facts.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
|
|
5
|
+
## The problem
|
|
6
|
+
|
|
7
|
+
Say you've built an app or chatbot that answers questions using facts you
|
|
8
|
+
collected — a list of school board members, office hours, tax rates,
|
|
9
|
+
whatever. You checked those facts when you wrote them down. But the world
|
|
10
|
+
keeps moving: a board member resigns, a rate changes, a first day of school
|
|
11
|
+
rolls over. **Your app doesn't know.** It keeps confidently repeating
|
|
12
|
+
yesterday's truth, and nothing alerts you.
|
|
13
|
+
|
|
14
|
+
stilltrue is the alarm. Once a week (or whenever you like), it re-reads the
|
|
15
|
+
official web pages your facts came from and checks that every fact you
|
|
16
|
+
recorded still appears there. If something vanished, your build fails and
|
|
17
|
+
tells you exactly which fact went stale — so a human can go verify and
|
|
18
|
+
update it.
|
|
19
|
+
|
|
20
|
+
## What you get
|
|
21
|
+
|
|
22
|
+
| Command | What it does |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `npx stilltrue drift` | Checks your saved facts against their live sources. Fails loudly if a fact has gone stale. |
|
|
25
|
+
| `npx stilltrue report` | Turns past check runs into a simple web page — green/red grid, what broke and when. |
|
|
26
|
+
| `verify` | For developers: a checkpoint that inspects AI-generated answers before they're shown to users. |
|
|
27
|
+
|
|
28
|
+
## Getting started
|
|
29
|
+
|
|
30
|
+
You'll need [Node.js](https://nodejs.org) (version 20 or newer) and a
|
|
31
|
+
project folder with your facts saved in a JSON file.
|
|
32
|
+
|
|
33
|
+
**1. Install it** — in your project folder, run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install --save-dev stilltrue
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**2. Scaffold a check** — this creates a ready-to-edit config file and an
|
|
40
|
+
example facts file (it never overwrites anything you already have):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx stilltrue init
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Open the generated `stilltrue.config.mjs`. Each check answers three
|
|
47
|
+
questions: *where do I fetch the truth from* (`source`), *what do I
|
|
48
|
+
believe* (`expect`), and *how do I compare the two* (`compare`):
|
|
49
|
+
|
|
50
|
+
```js
|
|
44
51
|
import { defineStilltrue, corpus, json, surname } from 'stilltrue';
|
|
45
52
|
|
|
46
53
|
export default defineStilltrue({
|
|
47
54
|
drift: [
|
|
48
55
|
{
|
|
49
56
|
name: 'school-board-roster',
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
// Where the truth lives: the district's own pages.
|
|
58
|
+
source: corpus(['https://www.myschooldistrict.org/board']),
|
|
59
|
+
// What you believe: read your saved file, keep just the last names.
|
|
60
|
+
expect: json('./data/school-board.json', (data) =>
|
|
61
|
+
data.members.map((m) => surname(m.name))
|
|
62
|
+
),
|
|
63
|
+
// How to compare: every name must still appear on those pages.
|
|
52
64
|
compare: 'contains-all',
|
|
53
65
|
},
|
|
54
66
|
],
|
|
55
67
|
});
|
|
56
68
|
```
|
|
57
69
|
|
|
70
|
+
This example says: *fetch the board page, and make sure every last name in
|
|
71
|
+
my `school-board.json` file still appears somewhere on it.*
|
|
72
|
+
|
|
73
|
+
Choosing what to assert is the judgment call — surnames beat full names,
|
|
74
|
+
"143 schools" beats a bare "143". The short guide:
|
|
75
|
+
[docs/MARKERS.md](docs/MARKERS.md).
|
|
76
|
+
|
|
77
|
+
**3. Run it:**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx stilltrue drift
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You'll see one line per check:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
✓ school-board-roster — pass (1204ms)
|
|
87
|
+
|
|
88
|
+
1 pass, 0 rot, 0 error (errors warn, never fail)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## The three results (and why there are three, not two)
|
|
92
|
+
|
|
93
|
+
- **pass** — every saved fact still appears at its source. All good.
|
|
94
|
+
- **rot** — a fact is gone from the source. The world changed; your data is
|
|
95
|
+
stale. This *fails the run* so you notice and fix the data.
|
|
96
|
+
- **error** — the website couldn't be reached (down, timeout, blocked).
|
|
97
|
+
This only *warns*. An unreachable site doesn't mean your facts are wrong,
|
|
98
|
+
and an alarm that cries wolf gets ignored — so outages never fail the build.
|
|
99
|
+
|
|
100
|
+
## Running it automatically every week
|
|
101
|
+
|
|
102
|
+
If your project is on GitHub, add this file as
|
|
103
|
+
`.github/workflows/drift.yml` and GitHub will run the check every Wednesday
|
|
104
|
+
and email you if anything rotted:
|
|
105
|
+
|
|
58
106
|
```yaml
|
|
59
|
-
|
|
107
|
+
name: Fact drift check
|
|
60
108
|
on:
|
|
61
|
-
schedule:
|
|
109
|
+
schedule:
|
|
110
|
+
- cron: '17 14 * * 3' # Wednesdays, ~10am US Eastern
|
|
111
|
+
workflow_dispatch: # adds a manual "Run workflow" button
|
|
62
112
|
jobs:
|
|
63
113
|
drift:
|
|
64
114
|
runs-on: ubuntu-latest
|
|
65
115
|
steps:
|
|
66
|
-
- uses: actions/checkout@
|
|
67
|
-
- uses: actions/setup-node@
|
|
116
|
+
- uses: actions/checkout@v5
|
|
117
|
+
- uses: actions/setup-node@v5
|
|
68
118
|
with: { node-version: '22' }
|
|
69
119
|
- run: npm ci
|
|
70
120
|
- run: npx stilltrue drift
|
|
71
121
|
```
|
|
72
122
|
|
|
73
|
-
|
|
74
|
-
deterministic either way). `expect` is your curated file. Rot fails the run
|
|
75
|
-
with one message per vanished fact; unreachable sources only warn.
|
|
123
|
+
## The report
|
|
76
124
|
|
|
77
|
-
|
|
125
|
+
Add `--record` to save each run, then generate the web page:
|
|
78
126
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
not verdicts: `failPolicy: 'open'` (default) warns and continues — a judge
|
|
84
|
-
outage must never block your app.
|
|
127
|
+
```bash
|
|
128
|
+
npx stilltrue drift --record
|
|
129
|
+
npx stilltrue report
|
|
130
|
+
```
|
|
85
131
|
|
|
86
|
-
|
|
87
|
-
|
|
132
|
+
That writes `stilltrue-report.html` — open it in any browser. It shows a
|
|
133
|
+
timeline grid (one row per check, one column per run, green ✓ / red ✗ /
|
|
134
|
+
amber ⚠) and a log of everything that ever rotted, with details.
|
|
88
135
|
|
|
89
|
-
|
|
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
|
-
];
|
|
136
|
+
## One rule underneath everything
|
|
94
137
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
138
|
+
**AI may extract, but never decides.** The drift check itself uses no AI at
|
|
139
|
+
all — it fetches, reads, and compares text, the same way every time, with
|
|
140
|
+
no API key and no cost. Deterministic alarms are trustworthy alarms. (If
|
|
141
|
+
*your* fact-gathering step wants to use AI to pull structure out of a messy
|
|
142
|
+
page, that's fine — it happens inside your `source` function, and the
|
|
143
|
+
comparison stays mechanical.)
|
|
98
144
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
145
|
+
## For developers: `verify`
|
|
146
|
+
|
|
147
|
+
`verify` is a library for teams shipping AI-generated answers: an ordered
|
|
148
|
+
series of checkpoints (a schema parse, a rules check, an AI judge — your
|
|
149
|
+
choice) that each answer must pass before publishing. Rejections say
|
|
150
|
+
exactly which checkpoint failed and why, in a format you can feed back to
|
|
151
|
+
the model for a retry. A broken checker never blocks your app — it fails
|
|
152
|
+
open. See the [project docs](docs/BRIEF.md) and the typed API in
|
|
153
|
+
`stilltrue`'s exports (`runVerify`, `zodStage`, `generateVerified`).
|
|
102
154
|
|
|
103
|
-
##
|
|
155
|
+
## What about testing prompts?
|
|
104
156
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
scope — settled in [docs/GOLDEN.md](docs/GOLDEN.md).
|
|
157
|
+
That's a different job with a great existing tool:
|
|
158
|
+
[promptfoo](https://promptfoo.dev) tests your prompts; stilltrue tests your
|
|
159
|
+
facts. How they fit together: [docs/GOLDEN.md](docs/GOLDEN.md).
|
|
109
160
|
|
|
110
161
|
## License
|
|
111
162
|
|
|
112
|
-
MIT
|
|
163
|
+
MIT — free to use, modify, and share.
|
package/dist/cli.js
CHANGED
|
@@ -94,13 +94,31 @@ try {
|
|
|
94
94
|
case 'report':
|
|
95
95
|
process.exit(await report(rest));
|
|
96
96
|
break;
|
|
97
|
+
case 'init': {
|
|
98
|
+
const { init } = await import("./init.js");
|
|
99
|
+
const { created, skipped } = await init(process.cwd());
|
|
100
|
+
for (const f of created)
|
|
101
|
+
console.log(`created ${f}`);
|
|
102
|
+
for (const f of skipped)
|
|
103
|
+
console.log(`kept existing ${f} (never overwritten)`);
|
|
104
|
+
if (created.length) {
|
|
105
|
+
console.log('\nNext steps:\n' +
|
|
106
|
+
' 1. Edit stilltrue.config.mjs — point source at the real page(s) your facts came from\n' +
|
|
107
|
+
' 2. Edit data/example-facts.json — or point the config at a facts file you already have\n' +
|
|
108
|
+
' 3. Run: npx stilltrue drift\n' +
|
|
109
|
+
'Picking good markers: docs/MARKERS.md in the stilltrue repo');
|
|
110
|
+
}
|
|
111
|
+
process.exit(0);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
97
114
|
case 'golden':
|
|
98
115
|
console.error('stilltrue deliberately has no golden runner — promptfoo tests your prompts; stilltrue tests your facts.\n' +
|
|
99
116
|
'Use https://promptfoo.dev for prompt/answer regression evals. Doctrine + pairing guide: docs/GOLDEN.md');
|
|
100
117
|
process.exit(1);
|
|
101
118
|
break;
|
|
102
119
|
default:
|
|
103
|
-
console.log('usage: stilltrue
|
|
120
|
+
console.log('usage: stilltrue init\n' +
|
|
121
|
+
' stilltrue drift [--config <path>] [--only <names>] [--json <path>] [--record] [--history <path>]\n' +
|
|
104
122
|
' stilltrue report [--history <path>] [--out <path>] [--title <text>]');
|
|
105
123
|
process.exit(command ? 1 : 0);
|
|
106
124
|
}
|
package/dist/init.d.ts
ADDED
package/dist/init.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const CONFIG_TEMPLATE = `// stilltrue configuration — one entry per fact-set you want watched.
|
|
5
|
+
// Docs: https://github.com/sg-bfoster/stilltrue#readme
|
|
6
|
+
// Picking good markers: https://github.com/sg-bfoster/stilltrue/blob/main/docs/MARKERS.md
|
|
7
|
+
import { defineStilltrue, corpus, json, surname } from 'stilltrue';
|
|
8
|
+
|
|
9
|
+
export default defineStilltrue({
|
|
10
|
+
drift: [
|
|
11
|
+
{
|
|
12
|
+
// A short id for this check — shows up in output and reports.
|
|
13
|
+
name: 'example-board-roster',
|
|
14
|
+
|
|
15
|
+
// WHERE THE TRUTH LIVES: the official page(s) your facts came from.
|
|
16
|
+
// corpus() fetches each page and strips it down to readable text.
|
|
17
|
+
// If one page is unreachable it warns; only if ALL are down does the
|
|
18
|
+
// check report an error (which never fails your build).
|
|
19
|
+
source: corpus([
|
|
20
|
+
'https://example.org/board',
|
|
21
|
+
// 'https://example.org/about', // add more pages if facts span several
|
|
22
|
+
]),
|
|
23
|
+
|
|
24
|
+
// WHAT YOU BELIEVE: read your saved facts file and pick out the
|
|
25
|
+
// "markers" — short, stable strings that must still appear on the
|
|
26
|
+
// page(s) above. Here: each member's last name, plus one number.
|
|
27
|
+
expect: json('./data/example-facts.json', (data) => [
|
|
28
|
+
...data.members.map((m) => surname(m.name)),
|
|
29
|
+
\`\${data.totalSchools} schools\`,
|
|
30
|
+
]),
|
|
31
|
+
|
|
32
|
+
// HOW TO COMPARE: 'contains-all' = every marker must appear in the
|
|
33
|
+
// fetched text (case-insensitive). Any missing marker = rot = the
|
|
34
|
+
// build fails and tells you which fact went stale.
|
|
35
|
+
compare: 'contains-all',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
`;
|
|
40
|
+
const DATA_TEMPLATE = `{
|
|
41
|
+
"_comment": "Example facts file - replace with your own data, or point the config at a file you already have.",
|
|
42
|
+
"members": [
|
|
43
|
+
{ "name": "Dr. Jane Smith" },
|
|
44
|
+
{ "name": "Robert Jones Jr." }
|
|
45
|
+
],
|
|
46
|
+
"totalSchools": 12
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
/** Write starter config + example data file. Never overwrites. */
|
|
50
|
+
export async function init(dir) {
|
|
51
|
+
const created = [];
|
|
52
|
+
const skipped = [];
|
|
53
|
+
const configPath = join(dir, 'stilltrue.config.mjs');
|
|
54
|
+
const existingConfig = [
|
|
55
|
+
'stilltrue.config.mjs',
|
|
56
|
+
'stilltrue.config.ts',
|
|
57
|
+
'stilltrue.config.mts',
|
|
58
|
+
'stilltrue.config.js',
|
|
59
|
+
'stilltrue.config.cjs',
|
|
60
|
+
].find((name) => existsSync(join(dir, name)));
|
|
61
|
+
if (existingConfig) {
|
|
62
|
+
skipped.push(existingConfig);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await writeFile(configPath, CONFIG_TEMPLATE);
|
|
66
|
+
created.push('stilltrue.config.mjs');
|
|
67
|
+
}
|
|
68
|
+
const dataPath = join(dir, 'data', 'example-facts.json');
|
|
69
|
+
if (existsSync(dataPath)) {
|
|
70
|
+
skipped.push('data/example-facts.json');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await mkdir(join(dir, 'data'), { recursive: true });
|
|
74
|
+
await writeFile(dataPath, DATA_TEMPLATE);
|
|
75
|
+
created.push('data/example-facts.json');
|
|
76
|
+
}
|
|
77
|
+
return { created, skipped };
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stilltrue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -37,5 +37,13 @@
|
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/sg-bfoster/stilltrue.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/sg-bfoster/stilltrue#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/sg-bfoster/stilltrue/issues"
|
|
40
48
|
}
|
|
41
49
|
}
|