Skip to content

Commit f3cd01f

Browse files
Merge pull request #18 from StepenkoAnatoli/fix/live-predicate-is-integrity-not-sufficiency
Run the live predicate for real, and fix what it could never have passed
2 parents a49c793 + 28bb866 commit f3cd01f

4 files changed

Lines changed: 143 additions & 13 deletions

File tree

‎.github/workflows/live-collection.yml‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,21 @@ name: live-collection
1818
# 1. the vendor CLI is a major this adapter supports
1919
# 2. the collection command exited 0
2020
# 3. a ledger was written, and it parses
21-
# 4. handoff exits 0 - the corpus arrived whole
22-
# 5. preflight exits 0 - the corpus passes the same checks a synthetic one does
21+
# 4. handoff exits 0 - the corpus arrived whole
22+
# 5. the INTEGRITY checks pass - provenance, corpus-shape, citations,
23+
# transport-provenance, hygiene
2324
# 6. the repository is unchanged
2425
#
26+
# NOT the whole preflight verdict, and this was found by running the predicate for real
27+
# rather than by reading it. `preflight` answers "is the RESEARCH sufficient to build",
28+
# which is a human-authored question: a scratch project has no unknowns in its contract,
29+
# so it fails `discovery-contract/no-unknowns` and exits 1 no matter how perfectly
30+
# collection worked. Requiring it would have made this workflow fail on every run, always,
31+
# for a reason having nothing to do with the transport it exists to test.
32+
#
33+
# The checks above are the ones collection can actually answer: the chain verifies, every
34+
# cited capture was fetched and is on disk, the corpus parses, rows carry URLs and claims.
35+
#
2536
# Diagnostics are still captured when a step fails, but they are captured in a way that
2637
# preserves the exit status rather than discarding it.
2738
#
@@ -191,7 +202,7 @@ jobs:
191202
console.log(`${parsed.length} ledger entries, ${fetched.length} successful`);
192203
' "$ledger"
193204
194-
- name: the corpus arrived whole, and passes its own checks
205+
- name: the corpus arrived whole, and passes its integrity checks
195206
run: |
196207
set -e
197208
cd "$RUNNER_TEMP/live-project"
@@ -205,15 +216,19 @@ jobs:
205216
echo "::error::handoff failed: the collected corpus did not arrive whole"
206217
exit 1
207218
fi
208-
if ! node "$kit/bin/preflight.mjs" --json > preflight.json; then
209-
echo "::error::preflight failed on the collected corpus"
210-
cat preflight.json || true
211-
exit 1
212-
fi
213-
node -e '
214-
const r = JSON.parse(require("fs").readFileSync("preflight.json", "utf8"));
215-
console.log(`preflight: ${r.status ?? r.verdict}`);
216-
'
219+
# The INTEGRITY checks, one at a time, each required.
220+
#
221+
# Not the whole verdict: preflight also judges whether the research is
222+
# sufficient to build, and a scratch project has no unknowns in its contract -
223+
# so it fails and exits 1 however well collection worked. Verified by running
224+
# it: `discovery-contract/no-unknowns` blocks a freshly scaffolded project.
225+
for check in provenance corpus-shape citations transport-provenance hygiene; do
226+
if ! node "$kit/bin/preflight.mjs" --check "$check"; then
227+
echo "::error::integrity check '$check' failed on the collected corpus"
228+
exit 1
229+
fi
230+
done
231+
echo "all integrity checks passed on the collected corpus"
217232
218233
- name: the repository is unchanged
219234
run: |
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Live predicate, verified over a real network — 2026-09-21
2+
3+
`live-collection` was written to verify a real collection and had never been run. This
4+
records what happened when its success predicate was executed for real, keyless, against
5+
live sites — and the defect that found.
6+
7+
**Status: local keyless live predicate verified.** Not "live-collection verified". The
8+
distinction is the point of this file.
9+
10+
## Method
11+
12+
A scratch project outside the repository, `--transport http-keyless`, two direct URLs and
13+
no search provider, so nothing was spent. Each condition of the workflow's predicate was
14+
run and its exit code read.
15+
16+
## The defect: the predicate asked a question collection cannot answer
17+
18+
The workflow required **`preflight` to exit 0** on the scratch project it collects into.
19+
Preflight exits **1** there, and correctly:
20+
21+
```
22+
fail discovery-contract/no-unknowns the contract enumerates no blocking unknowns -
23+
a contract with no unknowns makes no claim
24+
FAIL 1 blocking, 4 warning(s), 8 passing
25+
```
26+
27+
Preflight answers *"is the research sufficient to build?"* — a **human-authored**
28+
question. A freshly scaffolded project has no unknowns in its contract, so it fails that
29+
check however perfectly collection worked. **The workflow would have failed on every run,
30+
always, for a reason having nothing to do with the transport it exists to test.**
31+
32+
Reading the workflow would not have found this. Running it did, on the first attempt, and
33+
it cost nothing because the keyless route is free.
34+
35+
### The correction
36+
37+
The predicate now requires the checks collection *can* answer — the integrity ones, each
38+
required individually:
39+
40+
| check | what it asserts |
41+
|---|---|
42+
| `provenance` | the chain verifies and every cited capture was actually fetched |
43+
| `corpus-shape` | the corpus parses and the shape is there |
44+
| `citations` | every evidence row has a URL, a claim, and a cached page behind it |
45+
| `transport-provenance` | which adapter fetched each cited capture |
46+
| `hygiene` | duplicate rows, uncited captures, unparseable dates |
47+
48+
All five pass on a freshly collected scratch corpus. A test now refuses any executable
49+
line invoking `preflight.mjs` without `--check`.
50+
51+
## What ran, and what it returned
52+
53+
| condition | result |
54+
|---|---|
55+
| scaffold a scratch project | ok |
56+
| `research.mjs --dry-run` | exit 0 |
57+
| `research.mjs` (2 pages, keyless, real network) | exit 0, 2 collected, 0 failed |
58+
| ledger exists, parses, records a successful fetch | 2 entries, 2 successful |
59+
| `handoff.mjs` | exit 0 — "every cited capture on disk, chain verifies" |
60+
| the five integrity checks | all exit 0 |
61+
| repository unchanged | no change under `research/` |
62+
63+
Both captures graded `partial`, which the keyless adapter reports honestly rather than
64+
claiming `full` — itself a small confirmation that the completeness grading survives a
65+
real fetch.
66+
67+
## What this does and does not establish
68+
69+
**Established, over a real network:**
70+
71+
- the predicate logic is satisfiable by a real corpus, not only by fixtures
72+
- real network behaviour reaches the collector, and the ledger records it
73+
- ledger → handoff → integrity-check integration holds outside synthetic tests
74+
- the "repository unchanged" guard is meaningful: collection wrote only to the scratch path
75+
76+
**Still unproven, and not claimed:**
77+
78+
- the Firecrawl CLI install path in the workflow
79+
- `cliCompatibility` against a real installed CLI in CI
80+
- metered credential handling
81+
- the GitHub environment secret wiring
82+
- the Actions runner path itself — `workflow_dispatch` has never fired
83+
84+
The gap is now "the CI wiring and the metered route are untested" rather than "the whole
85+
thing is untested", which is smaller and specific. Closing the rest needs the
86+
`live-collection` environment and one real dispatch.

‎research-kit/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ node research-kit/bin/selftest.mjs # all of it
197197
node research-kit/bin/selftest.mjs gate hook # just these files
198198
```
199199

200-
644 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
200+
645 tests, offline, no key and no network. The runner **awaits** every test, so `ok` means
201201
the assertions settled (ADR-0021), and each test is raced against a watchdog
202202
(`RESEARCH_KIT_TEST_TIMEOUT`, default 60s).
203203

‎research-kit/test/architecture-map.test.mjs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,32 @@ test('the live workflow does not swallow the checks that decide its result', ()
286286
'the summary prints a URL prefix, which discloses what was being researched');
287287
assert(/hostname/.test(yaml), 'the summary should reduce URLs to hostnames');
288288
});
289+
290+
test('the live workflow requires integrity, not research sufficiency', () => {
291+
// Found by RUNNING the predicate, not by reading it.
292+
//
293+
// The workflow originally required `preflight` to exit 0 on the scratch project it
294+
// collects into. Preflight answers "is the RESEARCH sufficient to build" - a
295+
// human-authored question - so a freshly scaffolded project fails
296+
// `discovery-contract/no-unknowns` and exits 1 however perfectly collection worked.
297+
// The workflow would have failed on every run, always, for a reason having nothing to
298+
// do with the transport it exists to test.
299+
//
300+
// What collection CAN answer is integrity: the chain verifies, captures are on disk,
301+
// the corpus parses. Those are required per-check instead.
302+
const file = path.join(REPO, '.github', 'workflows', 'live-collection.yml');
303+
if (!fs.existsSync(file)) return;
304+
const yaml = fs.readFileSync(file, 'utf8');
305+
const executable = yaml.split('\n').filter((line) => !line.trim().startsWith('#'));
306+
307+
const wholeVerdict = executable.filter((line) => /preflight\.mjs/.test(line) && !/--check/.test(line));
308+
assertEqual(wholeVerdict.length, 0,
309+
'the workflow requires the whole preflight verdict from a scratch project, which can '
310+
+ `never satisfy it - it has no unknowns to prove:\n ${wholeVerdict.join('\n ')}`);
311+
312+
assert(/--check "\$check"|--check \$check/.test(yaml),
313+
'the workflow should require named integrity checks rather than the overall verdict');
314+
for (const check of ['provenance', 'corpus-shape', 'citations']) {
315+
assert(yaml.includes(check), `the integrity predicate no longer names ${check}`);
316+
}
317+
});

0 commit comments

Comments
 (0)