sparda-mcp 0.70.1 → 0.71.1
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/demo-app/.sparda/immunity.json +1 -1
- package/package.json +2 -2
- package/src/commands/apocalypse.js +2 -1
- package/src/commands/badge.js +2 -1
- package/src/commands/dossier.js +5 -2
- package/src/commands/enforce.js +34 -4
- package/src/commands/gate.js +5 -1
- package/src/commands/genome.js +24 -4
- package/src/commands/heal.js +26 -3
- package/src/commands/immunize.js +27 -10
- package/src/commands/prove.js +23 -3
- package/src/commands/review.js +1 -0
- package/src/commands/speculate.js +3 -1
- package/src/commands/stitch.js +28 -3
- package/src/commands/timeless.js +17 -3
- package/src/server/stdio.js +2 -1
- package/src/ubg/apocalypse.js +39 -1
- package/src/ubg/falsify.js +6 -2
- package/src/ubg/immunity.js +22 -2
- package/src/ubg/premise.js +36 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"v":"imm1","proven":
|
|
1
|
+
{"v":"imm1","proven":null,"premiseBasis":"unmeasured","surfaceOnly":true,"coverage":null,"blindHigh":0,"routes":[{"behaviorHash":"bh1_13969839d8a18d54aa0342618e56db6a","pol":121,"exposed":[]},{"behaviorHash":"bh1_2daf8a2b1b0ed393d443991a2b0700f6","pol":121,"exposed":[]},{"behaviorHash":"bh1_58fbf5ac38b6d71abc7204921de6b957","pol":121,"exposed":[]},{"behaviorHash":"bh1_58fbf5ac38b6d71abc7204921de6b957","pol":121,"exposed":[]},{"behaviorHash":"bh1_cc6788289c612af85b8d215b5bdf9b28","pol":121,"exposed":[]}],"posture":{"auth":{"protected":0,"exposed":0,"na":5},"atomicity":{"protected":0,"exposed":0,"na":5},"reversibility":{"protected":0,"exposed":0,"na":5},"validation":{"protected":0,"exposed":0,"na":5},"aggregate":{"protected":0,"exposed":0,"na":5}},"bytes":5}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sparda-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.1",
|
|
4
4
|
"mcpName": "io.github.zyx77550/sparda-mcp",
|
|
5
5
|
"description": "AI writes. SPARDA proves. A deterministic, offline gate that catches when an AI edit removes a guard, exposes a route, or breaks an invariant \u2014 no API key, right in the agent edit loop.",
|
|
6
6
|
"type": "module",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"mutation": "node tests/mutation/run.mjs",
|
|
32
32
|
"wedge": "node bench/wedge.mjs",
|
|
33
33
|
"release:check": "node scripts/release-gate.mjs",
|
|
34
|
-
"publish:vscode": "cd extensions/vscode && vsce publish"
|
|
34
|
+
"publish:vscode": "cd extensions/vscode && npx --yes @vscode/vsce@3.9.2 publish"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"src",
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
buildProofObjects,
|
|
20
20
|
} from '../ubg/apocalypse.js';
|
|
21
21
|
import { surveyBlindspots, coveragePct } from '../ubg/blindspots.js';
|
|
22
|
-
import { premiseFor, withPremiseGaps } from '../ubg/premise.js';
|
|
22
|
+
import { premiseFor, withPremiseGaps, basisFrom } from '../ubg/premise.js';
|
|
23
23
|
import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
|
|
24
24
|
|
|
25
25
|
// version travels with the proof so an audit knows which prover produced it
|
|
@@ -86,6 +86,7 @@ export async function runApocalypse(opts) {
|
|
|
86
86
|
coverage: blind.coverage.ratio,
|
|
87
87
|
blindHigh: blind.byRisk.critical + blind.byRisk.high,
|
|
88
88
|
premiseGaps: premise.available ? premise.gaps.length : 0,
|
|
89
|
+
premiseBasis: basisFrom(premise),
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
if (opts.sarif) {
|
package/src/commands/badge.js
CHANGED
|
@@ -10,7 +10,7 @@ import { compileUBG } from '../ubg/compile.js';
|
|
|
10
10
|
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
11
11
|
import { checkGraph, verdictOf, badgeFor } from '../ubg/apocalypse.js';
|
|
12
12
|
import { surveyBlindspots, coveragePct } from '../ubg/blindspots.js';
|
|
13
|
-
import { premiseFor, withPremiseGaps } from '../ubg/premise.js';
|
|
13
|
+
import { premiseFor, withPremiseGaps, basisFrom } from '../ubg/premise.js';
|
|
14
14
|
|
|
15
15
|
export async function runBadge(opts) {
|
|
16
16
|
const { graph, report } = compileUBG(opts.cwd, { write: false });
|
|
@@ -28,6 +28,7 @@ export async function runBadge(opts) {
|
|
|
28
28
|
coverage: blind.coverage.ratio,
|
|
29
29
|
blindHigh: blind.byRisk.critical + blind.byRisk.high,
|
|
30
30
|
premiseGaps: premise.available ? premise.gaps.length : 0,
|
|
31
|
+
premiseBasis: basisFrom(premise),
|
|
31
32
|
});
|
|
32
33
|
// null = measured-but-unknown (0/0): the JSON keeps the null, the console says the word
|
|
33
34
|
const cov =
|
package/src/commands/dossier.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from 'node:path';
|
|
|
10
10
|
import { compileUBG } from '../ubg/compile.js';
|
|
11
11
|
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
12
12
|
import { checkGraph, verdictOf, verdictState } from '../ubg/apocalypse.js';
|
|
13
|
-
import { premiseFor, withPremiseGaps } from '../ubg/premise.js';
|
|
13
|
+
import { premiseFor, withPremiseGaps, basisFrom } from '../ubg/premise.js';
|
|
14
14
|
import { surveyBlindspots, coveragePct } from '../ubg/blindspots.js';
|
|
15
15
|
import { buildCapsule } from '../ubg/immunity.js';
|
|
16
16
|
import { AXES, POLARITY_SYMBOL, exposedAxes } from '../ubg/polarity.js';
|
|
@@ -20,7 +20,6 @@ export async function runDossier(opts) {
|
|
|
20
20
|
const compiled = compileUBG(opts.cwd, { write: false, openapi: opts.openapi });
|
|
21
21
|
const canonical = canonicalizeGraph(compiled.graph);
|
|
22
22
|
const { findings, polarity } = checkGraph(canonical);
|
|
23
|
-
const capsule = buildCapsule(canonical);
|
|
24
23
|
// the public report — same rule as the badge: it must not describe an app SPARDA
|
|
25
24
|
// never fully had
|
|
26
25
|
const premise = await premiseFor(canonical, compiled.report, {
|
|
@@ -35,7 +34,11 @@ export async function runDossier(opts) {
|
|
|
35
34
|
coverage: blindspots.coverage.ratio,
|
|
36
35
|
blindHigh: blindspots.byRisk.critical + blindspots.byRisk.high,
|
|
37
36
|
premiseGaps: premise.available ? premise.gaps.length : 0,
|
|
37
|
+
premiseBasis: basisFrom(premise),
|
|
38
38
|
});
|
|
39
|
+
// built AFTER the premise, deliberately: the old ordering put `buildCapsule` three lines
|
|
40
|
+
// above the only value that licenses it, which is how the capsule stayed ungraded (E-106).
|
|
41
|
+
const capsule = buildCapsule(canonical, { premiseBasis: basisFrom(premise) });
|
|
39
42
|
|
|
40
43
|
const data = {
|
|
41
44
|
app: path.basename(path.resolve(opts.cwd)) || 'app',
|
package/src/commands/enforce.js
CHANGED
|
@@ -29,6 +29,7 @@ import fs from 'node:fs';
|
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { parse } from '@babel/parser';
|
|
31
31
|
import { compileUBG } from '../ubg/compile.js';
|
|
32
|
+
import { premiseFor } from '../ubg/premise.js';
|
|
32
33
|
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
33
34
|
import {
|
|
34
35
|
checkGraph,
|
|
@@ -141,11 +142,35 @@ function applyEdits(src, plan, principal, shimBody) {
|
|
|
141
142
|
return out;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
// The DELTA verdict: same app, before and after the synthesized check. Deliberately
|
|
146
|
+
// premise-blind — the premise is identical on both sides, so it cannot discriminate, and
|
|
147
|
+
// enforce V1 is Express-only where no boot-free oracle exists. Gating the rollback decision
|
|
148
|
+
// on it would not make enforce safer, it would make enforce impossible.
|
|
149
|
+
//
|
|
150
|
+
// What the premise DOES license is the WORD. Those are two different questions and E-104 is
|
|
151
|
+
// what happens when one answer is used for both: the edit is justified by the delta, the
|
|
152
|
+
// announcement by the oracle. See `announcedState` below.
|
|
144
153
|
function compileVerdict(cwd) {
|
|
145
|
-
const
|
|
154
|
+
const { graph, report } = compileUBG(cwd, { write: false });
|
|
155
|
+
const canonical = canonicalizeGraph(graph);
|
|
146
156
|
const { findings } = checkGraph(canonical);
|
|
147
157
|
const verdict = verdictOf(findings, canonical, {});
|
|
148
|
-
return { canonical, findings, verdict, state: verdictState(verdict) };
|
|
158
|
+
return { canonical, report, findings, verdict, state: verdictState(verdict) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The word this command is allowed to print, once the edit has proven itself.
|
|
162
|
+
//
|
|
163
|
+
// Before E-104 this was the literal string 'PROVEN (ENFORCED)', announced unconditionally on
|
|
164
|
+
// every successful run — on Express, the only framework enforce V1 supports, and one with no
|
|
165
|
+
// boot-free oracle. So the strongest word SPARDA has was printed on an app whose route table
|
|
166
|
+
// nobody had checked, every single time.
|
|
167
|
+
//
|
|
168
|
+
// The synthesized check really was verified on the recompiled graph; that part was never a
|
|
169
|
+
// lie. What was missing is that a verified guard over an UNVERIFIED ROUTE SET does not add up
|
|
170
|
+
// to PROVEN — Direction 3 is a claim about the subject, not the proof.
|
|
171
|
+
async function announcedState(cwd, canonical, report) {
|
|
172
|
+
const premise = await premiseFor(canonical, report, { cwd });
|
|
173
|
+
return premise.available ? 'PROVEN (ENFORCED)' : 'PARTIAL (ENFORCED)';
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
export function readEnforceManifest(cwd) {
|
|
@@ -272,13 +297,14 @@ export async function runEnforce(opts) {
|
|
|
272
297
|
JSON.stringify(manifest, null, 2) + '\n',
|
|
273
298
|
);
|
|
274
299
|
|
|
300
|
+
const announced = await announcedState(cwd, after.canonical, after.report);
|
|
275
301
|
if (opts.json)
|
|
276
302
|
console.log(
|
|
277
303
|
JSON.stringify(
|
|
278
304
|
{
|
|
279
305
|
enforced: manifest.routes,
|
|
280
306
|
files: Object.keys(manifest.files),
|
|
281
|
-
verdict:
|
|
307
|
+
verdict: announced,
|
|
282
308
|
},
|
|
283
309
|
null,
|
|
284
310
|
2,
|
|
@@ -287,7 +313,11 @@ export async function runEnforce(opts) {
|
|
|
287
313
|
else {
|
|
288
314
|
for (const t of targets) log(` ✓ enforced ${t.label}`);
|
|
289
315
|
log(
|
|
290
|
-
`\n✓
|
|
316
|
+
`\n✓ ${announced} — ${targets.length} route(s) now carry a boundary check SPARDA verified on the recompiled graph.` +
|
|
317
|
+
(announced.startsWith('PARTIAL')
|
|
318
|
+
? `\n Not PROVEN: no oracle checked this app's route table, so Direction 3 is unverified. Run with --probe to measure it.`
|
|
319
|
+
: '') +
|
|
320
|
+
`\n Revert any time: sparda enforce --revert (byte-for-byte).`,
|
|
291
321
|
);
|
|
292
322
|
}
|
|
293
323
|
}
|
package/src/commands/gate.js
CHANGED
|
@@ -125,8 +125,12 @@ export async function runGate(opts) {
|
|
|
125
125
|
try {
|
|
126
126
|
({ graph, report } = compileUBG(opts.cwd, { write: false }));
|
|
127
127
|
} catch (err) {
|
|
128
|
+
// `ok` is DELIBERATELY absent, not `true`. Abstaining is right — the agent is mid-edit and
|
|
129
|
+
// a false alarm here is worse than silence — but a consumer reading `ok` must not be able
|
|
130
|
+
// to read a pass out of a check that never ran. The gate still exits 0; it just refuses to
|
|
131
|
+
// say the word (ADR-092).
|
|
128
132
|
if (opts.json)
|
|
129
|
-
console.log(JSON.stringify({ ok:
|
|
133
|
+
console.log(JSON.stringify({ ok: null, abstained: err.message }, null, 2));
|
|
130
134
|
else if (!hook)
|
|
131
135
|
say(
|
|
132
136
|
`⏳ GATE ABSTAINED — the tree does not compile yet (${err.message}). Fix it, then re-check.`,
|
package/src/commands/genome.js
CHANGED
|
@@ -21,16 +21,23 @@ import {
|
|
|
21
21
|
emptyGenome,
|
|
22
22
|
recall,
|
|
23
23
|
} from '../ubg/genome.js';
|
|
24
|
+
import { premiseFor, basisFrom } from '../ubg/premise.js';
|
|
24
25
|
import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
|
|
25
26
|
|
|
26
27
|
const GENOME_FILE = 'sparda-genome.jsonl';
|
|
27
28
|
const KEY_FILE = 'genome.key';
|
|
28
29
|
|
|
29
30
|
export async function runGenome(opts) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const compiled = compileUBG(opts.cwd, { write: false, openapi: opts.openapi });
|
|
32
|
+
const canonical = canonicalizeGraph(compiled.graph);
|
|
33
|
+
// The premise matters MORE here than anywhere, and it was the one place that never asked
|
|
34
|
+
// (E-106). An antibody is a per-behavior claim, so a route the compiler never saw does not
|
|
35
|
+
// falsify the antibodies that were minted — it means no antibody exists for it. But this
|
|
36
|
+
// contribution is signed, content-addressed and merged into a file other people pull, and
|
|
37
|
+
// a genome that silently under-represents an app teaches the world that the surface it
|
|
38
|
+
// covers IS the app. The gaps are named below rather than counted.
|
|
39
|
+
const premise = await premiseFor(canonical, compiled.report, { cwd: opts.cwd });
|
|
40
|
+
const capsule = buildCapsule(canonical, { premiseBasis: basisFrom(premise) });
|
|
34
41
|
const identity = loadOrCreateIdentity(opts.cwd);
|
|
35
42
|
const minted = mintGenome(capsule, identity);
|
|
36
43
|
|
|
@@ -62,6 +69,19 @@ export async function runGenome(opts) {
|
|
|
62
69
|
console.log(
|
|
63
70
|
` merged: +${added} new, ${corroborated} corroborated → ${genome.antibodies.length} antibody(ies) in ${GENOME_FILE}`,
|
|
64
71
|
);
|
|
72
|
+
// What this contribution is NOT. A count of antibodies reads as a measure of the app; it is
|
|
73
|
+
// a measure of the surface SPARDA had. Gaps are NAMED, because "3 routes missing" is a
|
|
74
|
+
// number a reader discounts and "GET /api/legacy/purge" is one they go and look at.
|
|
75
|
+
if (premise.available && premise.gaps.length)
|
|
76
|
+
for (const g of premise.gaps)
|
|
77
|
+
console.log(
|
|
78
|
+
` ⚠ no antibody for ${g.method} ${g.path} — the ${premise.oracle} oracle serves it, the compiler never saw it`,
|
|
79
|
+
);
|
|
80
|
+
else if (capsule.premiseBasis === 'unmeasured')
|
|
81
|
+
console.log(
|
|
82
|
+
` ◐ UNMEASURED PREMISE — no oracle checked this app's route table, so this contribution` +
|
|
83
|
+
` covers the surface SPARDA saw, which may not be the app (${premise.reason}).`,
|
|
84
|
+
);
|
|
65
85
|
// surface any behavior the world now disagrees about — load-bearing signal
|
|
66
86
|
const conflicts = distinctHashes(genome).filter((h) => recall(genome, h).conflict);
|
|
67
87
|
if (conflicts.length)
|
package/src/commands/heal.js
CHANGED
|
@@ -102,7 +102,13 @@ async function gate(opts, id, flight, healDir) {
|
|
|
102
102
|
const staticFindings = checkGraph(fixedGraph).findings;
|
|
103
103
|
let regressions;
|
|
104
104
|
const baselinePath = path.join(healDir, 'baseline.json');
|
|
105
|
-
|
|
105
|
+
// "nothing protected got removed" is a DELTA, and a delta needs two sides. `--check` can be
|
|
106
|
+
// run without the diagnose phase ever having frozen `baseline.json` — and then `diffGraphs`
|
|
107
|
+
// never runs, so no removed guard can possibly be detected. The old verdict line said
|
|
108
|
+
// "zero protection lost" either way: a property nothing had measured, asserted as an
|
|
109
|
+
// outcome. Tracked, so the headline can be qualified rather than the check silently skipped.
|
|
110
|
+
const deltaMeasured = fs.existsSync(baselinePath);
|
|
111
|
+
if (deltaMeasured) {
|
|
106
112
|
const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
107
113
|
regressions = diffGraphs(baseline, fixedGraph).findings;
|
|
108
114
|
// pre-existing static findings are not the fix's fault — only NEW ones gate
|
|
@@ -131,14 +137,31 @@ async function gate(opts, id, flight, healDir) {
|
|
|
131
137
|
console.log(
|
|
132
138
|
` ${apocalypse.safe ? '✓' : '✗'} apocalypse: ${regressions.length === 0 ? 'no new findings' : regressions.map((f) => f.rule).join(', ')}`,
|
|
133
139
|
);
|
|
140
|
+
if (!deltaMeasured)
|
|
141
|
+
console.log(
|
|
142
|
+
` ◐ guard delta UNMEASURED — no baseline.json in .sparda/heal/${id}/;` +
|
|
143
|
+
` run \`sparda heal ${id}\` first so the pre-fix graph is frozen`,
|
|
144
|
+
);
|
|
134
145
|
|
|
135
146
|
const ok = healing.healed && laws.ok && apocalypse.safe;
|
|
136
|
-
const report = {
|
|
147
|
+
const report = {
|
|
148
|
+
id,
|
|
149
|
+
healed: healing,
|
|
150
|
+
laws: laws.checks,
|
|
151
|
+
regressions,
|
|
152
|
+
deltaMeasured,
|
|
153
|
+
ok,
|
|
154
|
+
};
|
|
137
155
|
atomicWrite(path.join(healDir, 'report.json'), JSON.stringify(report, null, 2) + '\n');
|
|
138
156
|
|
|
139
157
|
if (ok) {
|
|
158
|
+
// The claim is trimmed to what was actually measured. Without a baseline the fixed tree
|
|
159
|
+
// was still graded — every static finding gated, which is STRICTER — but "zero protection
|
|
160
|
+
// lost" specifically requires the before/after diff, so it is not said.
|
|
140
161
|
console.log(
|
|
141
|
-
|
|
162
|
+
deltaMeasured
|
|
163
|
+
? `✓ HEALED & PROVEN — same recorded inputs, correct output, zero law broken, zero protection lost. Ship it.`
|
|
164
|
+
: `◐ HEALED, PARTIALLY PROVEN — same recorded inputs, correct output, zero law broken, no static finding. Whether a guard was REMOVED is unmeasured: no pre-fix graph was frozen.`,
|
|
142
165
|
);
|
|
143
166
|
} else {
|
|
144
167
|
console.log(
|
package/src/commands/immunize.js
CHANGED
|
@@ -11,13 +11,18 @@ import { compileUBG } from '../ubg/compile.js';
|
|
|
11
11
|
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
12
12
|
import { buildCapsule } from '../ubg/immunity.js';
|
|
13
13
|
import { AXES } from '../ubg/polarity.js';
|
|
14
|
+
import { premiseFor, basisFrom } from '../ubg/premise.js';
|
|
14
15
|
import { atomicWriteFileSync as atomicWrite } from '../server/persistence.js';
|
|
15
16
|
|
|
16
17
|
export async function runImmunize(opts) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
|
|
18
|
+
const compiled = compileUBG(opts.cwd, { write: false, openapi: opts.openapi });
|
|
19
|
+
const canonical = canonicalizeGraph(compiled.graph);
|
|
20
|
+
// THE PREMISE, asked here too (E-106). `immunize` grades a compiled graph and freezes the
|
|
21
|
+
// result into a portable artifact — the strongest claim in the product, because it is the
|
|
22
|
+
// one that leaves the repo. It printed an `UNMEASURED PREMISE` branch that no call path
|
|
23
|
+
// could reach, because nothing ever told the capsule on what basis it was built.
|
|
24
|
+
const premise = await premiseFor(canonical, compiled.report, { cwd: opts.cwd });
|
|
25
|
+
const capsule = buildCapsule(canonical, { premiseBasis: basisFrom(premise) });
|
|
21
26
|
|
|
22
27
|
if (opts.json) {
|
|
23
28
|
console.log(JSON.stringify(capsule, null, 2));
|
|
@@ -44,11 +49,18 @@ export async function runImmunize(opts) {
|
|
|
44
49
|
const exposed = AXES.filter((a) => capsule.posture[a].exposed > 0)
|
|
45
50
|
.map((a) => `${a}×${capsule.posture[a].exposed}`)
|
|
46
51
|
.join(', ');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
// `proven` is three-state since ADR-092: null means the premise behind this capsule was
|
|
53
|
+
// never measured, so the capsule carries no licence for the strongest word. Checked FIRST —
|
|
54
|
+
// a falsy `null` read as "not proven" would be a different lie in the safe direction, and
|
|
55
|
+
// still a lie.
|
|
56
|
+
const verdictText =
|
|
57
|
+
capsule.proven === null
|
|
58
|
+
? "◑ UNMEASURED PREMISE — no oracle checked this app's route table, so this capsule cannot claim PROVEN"
|
|
59
|
+
: capsule.surfaceOnly
|
|
60
|
+
? '● SURFACE ONLY — routes seen, no behavior resolved (nothing to prove)'
|
|
61
|
+
: capsule.proven
|
|
62
|
+
? '✓ PROVEN'
|
|
63
|
+
: '✗ NOT PROVEN';
|
|
52
64
|
console.log(` verdict: ${verdictText}` + (exposed ? ` — exposed: ${exposed}` : ''));
|
|
53
65
|
console.log(
|
|
54
66
|
` coverage: ${(capsule.coverage * 100).toFixed(0)}% of observed behavior resolved` +
|
|
@@ -60,6 +72,11 @@ export async function runImmunize(opts) {
|
|
|
60
72
|
);
|
|
61
73
|
// surface-only is not a risk (nothing to fault) → don't fail CI; only a real
|
|
62
74
|
// unproven capsule (an exposed axis) gates.
|
|
63
|
-
|
|
75
|
+
//
|
|
76
|
+
// `=== false`, not `!capsule.proven`: `null` is falsy, and gating on it would make SPARDA
|
|
77
|
+
// fail a build because no oracle was AVAILABLE — demanding what it could not measure, which
|
|
78
|
+
// premise.js forbids in exactly these words. Withholding the word and finding a fault are
|
|
79
|
+
// different outcomes, and only the second one closes a gate.
|
|
80
|
+
if (capsule.proven === false && !capsule.surfaceOnly) process.exitCode = 1;
|
|
64
81
|
return { capsule, outPath };
|
|
65
82
|
}
|
package/src/commands/prove.js
CHANGED
|
@@ -9,7 +9,7 @@ import { compileUBG } from '../ubg/compile.js';
|
|
|
9
9
|
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
10
10
|
import { checkGraph, verdictOf, verdictState, badgeFor } from '../ubg/apocalypse.js';
|
|
11
11
|
import { surveyBlindspots, coveragePct } from '../ubg/blindspots.js';
|
|
12
|
-
import { premiseFor, withPremiseGaps } from '../ubg/premise.js';
|
|
12
|
+
import { premiseFor, withPremiseGaps, basisFrom } from '../ubg/premise.js';
|
|
13
13
|
import { buildCapsule } from '../ubg/immunity.js';
|
|
14
14
|
import { fingerprintGraph } from '../ubg/fingerprint.js';
|
|
15
15
|
import { suggestAppDirs } from '../detect.js';
|
|
@@ -39,8 +39,12 @@ export async function runProve(opts) {
|
|
|
39
39
|
coverage: blind.coverage.ratio,
|
|
40
40
|
blindHigh: blind.byRisk.critical + blind.byRisk.high,
|
|
41
41
|
premiseGaps: premise.available ? premise.gaps.length : 0,
|
|
42
|
+
premiseBasis: basisFrom(premise),
|
|
42
43
|
});
|
|
43
|
-
|
|
44
|
+
// The capsule is graded on the SAME basis as the verdict. It shipped without one (E-106):
|
|
45
|
+
// `prove` computed the premise two lines above, used it for the verdict word, and then
|
|
46
|
+
// built a portable capsule that re-asserted `proven: true` with no licence at all.
|
|
47
|
+
const capsule = buildCapsule(canonical, { premiseBasis: basisFrom(premise) });
|
|
44
48
|
const prints = fingerprintGraph(canonical);
|
|
45
49
|
// one app-level seal: a content address over every route's behavior hash — the same
|
|
46
50
|
// behavior, in any repo, yields the same seal (the genome's join key, made visible).
|
|
@@ -77,7 +81,14 @@ export async function runProve(opts) {
|
|
|
77
81
|
probed: premise.probed,
|
|
78
82
|
gaps: premise.gaps.length,
|
|
79
83
|
}
|
|
80
|
-
: {
|
|
84
|
+
: {
|
|
85
|
+
verified: false,
|
|
86
|
+
// 'unmeasured' (no oracle ran) vs 'declared' (OpenAPI — the document analysed IS
|
|
87
|
+
// the route table). A machine consumer that cannot tell those apart is in exactly
|
|
88
|
+
// the position E-104 put the verdict in.
|
|
89
|
+
basis: premise.basis ?? 'unmeasured',
|
|
90
|
+
reason: premise.reason,
|
|
91
|
+
},
|
|
81
92
|
findings: findings.map((f) => ({
|
|
82
93
|
rule: f.rule,
|
|
83
94
|
severity: f.severity,
|
|
@@ -147,6 +158,15 @@ export async function runProve(opts) {
|
|
|
147
158
|
);
|
|
148
159
|
if (summary.blindHigh > 0)
|
|
149
160
|
reasons.push(`${summary.blindHigh} high-risk blind spot(s)`);
|
|
161
|
+
// FIRST, because it is the only rung that is about the SUBJECT rather than the proof.
|
|
162
|
+
// A reader told "PARTIAL: 100% of the surface resolved" and nothing else concludes the
|
|
163
|
+
// analysis was thorough and the word merely cautious. The truth was that no oracle ever
|
|
164
|
+
// checked whether the surface analysed was the app's — and that is a different sentence,
|
|
165
|
+
// with a different remedy (E-104).
|
|
166
|
+
if (verdict.premiseUnmeasured)
|
|
167
|
+
reasons.unshift(
|
|
168
|
+
`the route table was never checked by an oracle — no boot-free oracle for this framework, and --probe was not used, so Direction 3 is UNVERIFIED (not a finding: an unmeasured premise, which PROVEN would assert)`,
|
|
169
|
+
);
|
|
150
170
|
if (verdict.coverageUnknown)
|
|
151
171
|
reasons.push(
|
|
152
172
|
`coverage is UNKNOWN (0 behaviors resolved out of 0 seen — no measurement, not a pass)`,
|
package/src/commands/review.js
CHANGED
|
@@ -86,6 +86,7 @@ export function reviewGraphs(
|
|
|
86
86
|
coverage: candCov,
|
|
87
87
|
blindHigh: candBlind.byRisk.critical + candBlind.byRisk.high,
|
|
88
88
|
premiseGaps: premise?.available ? premise.gaps.length : 0,
|
|
89
|
+
premiseBasis: premise?.available ? 'measured' : (premise?.basis ?? 'unmeasured'),
|
|
89
90
|
}),
|
|
90
91
|
obligations,
|
|
91
92
|
findings,
|
|
@@ -65,7 +65,9 @@ export async function runSpeculate(opts) {
|
|
|
65
65
|
(result.novel.length ? ` · ${result.novel.length} novel need full proof` : '')
|
|
66
66
|
: result.novel.length
|
|
67
67
|
? `· ${result.novel.length} novel shape(s) need the full prover — run \`sparda apocalypse\``
|
|
68
|
-
:
|
|
68
|
+
: capsule?.proven === null
|
|
69
|
+
? `◑ UNMEASURED PREMISE (by lookup) — every route settled from the frozen capsule, but nothing ever checked that capsule's route table`
|
|
70
|
+
: `✓ PROVEN (by lookup) — every route settled from the frozen capsule, zero prover work`,
|
|
69
71
|
);
|
|
70
72
|
if (result.rejected.length) process.exitCode = 1;
|
|
71
73
|
return { result };
|
package/src/commands/stitch.js
CHANGED
|
@@ -17,7 +17,14 @@ export async function runStitch(opts, dirs) {
|
|
|
17
17
|
process.exitCode = 1;
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
// A service that does not compile is UNMEASURED, and the join is only as complete as the
|
|
21
|
+
// set it ran over. The failure used to go to stderr and nowhere else: the headline still
|
|
22
|
+
// said "no cross-service calls resolved (targets may be dynamic or unrelated)" — a
|
|
23
|
+
// reassuring parenthetical for what was really "we compiled one of your two services".
|
|
24
|
+
// Cross-service BOLA is found by JOINING; half a join finds nothing and says nothing,
|
|
25
|
+
// which is the most confident silence in the product.
|
|
20
26
|
const services = [];
|
|
27
|
+
const unread = [];
|
|
21
28
|
for (const d of dirs) {
|
|
22
29
|
const abs = path.resolve(opts.cwd, d);
|
|
23
30
|
try {
|
|
@@ -26,7 +33,7 @@ export async function runStitch(opts, dirs) {
|
|
|
26
33
|
const { findings } = checkGraph(g);
|
|
27
34
|
services.push({ name: path.basename(abs), graph: g, findings });
|
|
28
35
|
} catch (e) {
|
|
29
|
-
|
|
36
|
+
unread.push({ dir: d, reason: e.message.slice(0, 140) });
|
|
30
37
|
}
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -34,16 +41,32 @@ export async function runStitch(opts, dirs) {
|
|
|
34
41
|
|
|
35
42
|
if (opts.json) {
|
|
36
43
|
console.log(
|
|
37
|
-
JSON.stringify(
|
|
44
|
+
JSON.stringify(
|
|
45
|
+
{ services: services.map((s) => s.name), unread, edges, findings },
|
|
46
|
+
null,
|
|
47
|
+
2,
|
|
48
|
+
),
|
|
38
49
|
);
|
|
50
|
+
// the JSON consumer is a CI job, and it must not read an empty `findings` over a partial
|
|
51
|
+
// join as a clean bill of health
|
|
52
|
+
if (unread.length) process.exitCode = 1;
|
|
39
53
|
return;
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
console.log(`\nSPARDA · stitch — ${services.map((s) => s.name).join(' · ')}`);
|
|
43
57
|
console.log('─'.repeat(52));
|
|
58
|
+
// stated BEFORE the result, because it is the qualifier on everything that follows
|
|
59
|
+
for (const u of unread) console.log(` ✗ ${u.dir} did not compile — ${u.reason}`);
|
|
60
|
+
if (unread.length)
|
|
61
|
+
console.log(
|
|
62
|
+
` ◐ PARTIAL JOIN — ${services.length} of ${dirs.length} service(s) read.` +
|
|
63
|
+
` Any call to or from the missing one(s) is UNMEASURED, not absent.`,
|
|
64
|
+
);
|
|
44
65
|
if (!edges.length) {
|
|
45
66
|
console.log(
|
|
46
|
-
|
|
67
|
+
unread.length
|
|
68
|
+
? ' no cross-service calls resolved among the services that DID compile'
|
|
69
|
+
: ' no cross-service calls resolved (targets may be dynamic or unrelated)',
|
|
47
70
|
);
|
|
48
71
|
} else {
|
|
49
72
|
console.log(` ${edges.length} cross-service call(s):`);
|
|
@@ -55,4 +78,6 @@ export async function runStitch(opts, dirs) {
|
|
|
55
78
|
for (const f of findings) console.log(` ${f.message}`);
|
|
56
79
|
}
|
|
57
80
|
console.log('');
|
|
81
|
+
if (unread.length) process.exitCode = 1;
|
|
82
|
+
return { services: services.map((s) => s.name), unread, edges, findings };
|
|
58
83
|
}
|
package/src/commands/timeless.js
CHANGED
|
@@ -77,9 +77,23 @@ async function replay(opts, id) {
|
|
|
77
77
|
`TIMELESS — replaying ${flight.request.method} ${flight.request.url} (${flight.taps.length} taps virtualized)`,
|
|
78
78
|
);
|
|
79
79
|
if (result.match) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// A flight with ZERO taps passes `match` on the response alone: `leftover` and
|
|
81
|
+
// `divergences` are empty because there was nothing to consume and nothing to diverge
|
|
82
|
+
// from. "every tap consumed, zero divergence" over an empty tap set is the falsify bug's
|
|
83
|
+
// shape — a perfect score for a control set of size 0. It matters practically: a flight
|
|
84
|
+
// recorded before `box.wrapClient(db)` was installed captures no taps, so the replay hit
|
|
85
|
+
// the LIVE dependency and the match says the environment answered the same way today,
|
|
86
|
+
// not that the code is unchanged.
|
|
87
|
+
if (!flight.taps.length)
|
|
88
|
+
console.log(
|
|
89
|
+
`◐ response identical (status ${result.actual.status}) — but 0 taps were recorded, so` +
|
|
90
|
+
` NOTHING was virtualized: determinism is UNMEASURED. Wrap your clients` +
|
|
91
|
+
` (\`box.wrapClient(db)\`) and re-record to make this a proof.`,
|
|
92
|
+
);
|
|
93
|
+
else
|
|
94
|
+
console.log(
|
|
95
|
+
`✓ byte-identical replay — status ${result.actual.status}, all ${flight.taps.length} tap(s) consumed, zero divergence`,
|
|
96
|
+
);
|
|
83
97
|
} else {
|
|
84
98
|
if (!result.statusMatch)
|
|
85
99
|
console.log(
|
package/src/server/stdio.js
CHANGED
|
@@ -30,7 +30,7 @@ import { compileUBG } from '../ubg/compile.js';
|
|
|
30
30
|
import { canonicalizeGraph } from '../ubg/schema.js';
|
|
31
31
|
import { checkGraph, diffGraphs, verdictOf, verdictState } from '../ubg/apocalypse.js';
|
|
32
32
|
import { surveyBlindspots } from '../ubg/blindspots.js';
|
|
33
|
-
import { premiseFor, withPremiseGaps } from '../ubg/premise.js';
|
|
33
|
+
import { premiseFor, withPremiseGaps, basisFrom } from '../ubg/premise.js';
|
|
34
34
|
import { witnessTargets, admitWitnesses } from '../ubg/witness.js';
|
|
35
35
|
|
|
36
36
|
const EVENT_POLL_MS = Number(process.env.SPARDA_EVENT_POLL_MS ?? 5000);
|
|
@@ -1266,6 +1266,7 @@ export async function proveApp(cwd, { route } = {}) {
|
|
|
1266
1266
|
coverage: blind.coverage.ratio,
|
|
1267
1267
|
blindHigh: blind.byRisk.critical + blind.byRisk.high,
|
|
1268
1268
|
premiseGaps: premise.available ? premise.gaps.length : 0,
|
|
1269
|
+
premiseBasis: basisFrom(premise),
|
|
1269
1270
|
});
|
|
1270
1271
|
return {
|
|
1271
1272
|
verdict: verdictState(verdict),
|
package/src/ubg/apocalypse.js
CHANGED
|
@@ -290,8 +290,21 @@ export function checkGraph(graph) {
|
|
|
290
290
|
// "confirm intent" — never hidden, never marked safe, never touches PROVEN. The list is the
|
|
291
291
|
// HEAD of the distribution (deliberately precise, not `**/auth/**` blanket: change-password /
|
|
292
292
|
// 2fa / session management are NOT public, and re-labeling those would hide a real hole).
|
|
293
|
+
//
|
|
294
|
+
// The list is precise, but its TOKENS are not: `register` also names authenticated management
|
|
295
|
+
// (`/account/2fa/register`, `/devices/register`), `webhook`/`hook` a subscription CREATE, and
|
|
296
|
+
// any signature can sit under an authenticated namespace (`/account/…`, `/admin/…`). Because
|
|
297
|
+
// this re-label carries NO evidence of a gate, the asymmetric error model forbids it from
|
|
298
|
+
// firing on those: it must ABSTAIN when the path sits in an authenticated area, or it hides
|
|
299
|
+
// the same unguarded mutation it swears off for change-password, one alternative over (the
|
|
300
|
+
// 2FA-register / webhook-management class). Abstaining only ADDS criticals — the safe direction.
|
|
301
|
+
const authenticatedArea =
|
|
302
|
+
/(^|[:/\- ])(account|accounts|settings|admin|me|profile|2fa|mfa|totp|webhooks?|hooks?|devices?|tokens?|api[_-]?keys?|applications?|integrations?)([/:\- ]|$)/i.test(
|
|
303
|
+
ep.label,
|
|
304
|
+
);
|
|
293
305
|
const expectedPublic =
|
|
294
306
|
!credentialGated &&
|
|
307
|
+
!authenticatedArea &&
|
|
295
308
|
(/(^|[:/\- ])(log-?in|sign-?in|sign-?up|register|log-?out|sign-?out|forgot-?password|reset-?password|forgot|verify-?email|email-?verification|confirm-?email|magic-?link|oauth\d?|openid|sso|saml|health(z|check|-?check)?|livez|readyz|metrics|well-known)([/:\- ]|$)/i.test(
|
|
296
309
|
ep.label,
|
|
297
310
|
) ||
|
|
@@ -764,7 +777,7 @@ const COVERAGE_COMPLETE = 0.6;
|
|
|
764
777
|
export function verdictOf(
|
|
765
778
|
findings,
|
|
766
779
|
graph,
|
|
767
|
-
{ coverage, blindHigh = 0, premiseGaps = 0 } = {},
|
|
780
|
+
{ coverage, blindHigh = 0, premiseGaps = 0, premiseBasis = null } = {},
|
|
768
781
|
) {
|
|
769
782
|
const counts = { critical: 0, high: 0, medium: 0, info: 0 };
|
|
770
783
|
for (const f of findings) counts[f.severity]++;
|
|
@@ -824,12 +837,32 @@ export function verdictOf(
|
|
|
824
837
|
// from undefined (not measured at all, e.g. heal's partial-graph delta, old semantics
|
|
825
838
|
// kept). An unknown coverage can never carry a complete PROVEN: unknown ≠ 100%.
|
|
826
839
|
const coverageUnknown = coverage === null;
|
|
840
|
+
// E-104. The premise is the claim that the route set analysed IS the route set the app
|
|
841
|
+
// serves — Direction 3 — and the ONLY instrument that checks it is an oracle that is not
|
|
842
|
+
// the analyser. When no oracle ran, that claim is UNVERIFIED, and `PROVEN` asserts it.
|
|
843
|
+
//
|
|
844
|
+
// The bug this closes: `premiseFor` reported `{ available: false, gaps: [] }` honestly, and
|
|
845
|
+
// every consumer then read it through `premiseGaps === 0` — byte-identical to "an oracle ran
|
|
846
|
+
// and agreed". Measured on our own fixtures: 7 of the 8 that read PROVEN had never had an
|
|
847
|
+
// oracle run at all, every one of them Express or FastAPI. An honest LABEL is not an honest
|
|
848
|
+
// SYSTEM; the place a distinction gets flattened is where the lie is told.
|
|
849
|
+
//
|
|
850
|
+
// Deliberately a PARTIAL rung and not a PREMISE_GAP: a gap is measured evidence that the
|
|
851
|
+
// subject was incomplete (blocking), while silence is only the absence of a witness. PARTIAL
|
|
852
|
+
// already means exactly that — "proved what was seen" — so this neither blocks a deploy nor
|
|
853
|
+
// invents a fault. It withholds the strongest word, which is the one thing it must do.
|
|
854
|
+
//
|
|
855
|
+
// `null` keeps the old semantics for callers with no premise to speak of (heal's regression
|
|
856
|
+
// delta is a partial graph, not a whole-app proof). `'declared'` is OpenAPI, where the
|
|
857
|
+
// document analysed IS the route table and a second witness would be the same map twice.
|
|
858
|
+
const premiseUnmeasured = premiseBasis === 'unmeasured';
|
|
827
859
|
const partial =
|
|
828
860
|
clean &&
|
|
829
861
|
entrypoints > 0 &&
|
|
830
862
|
((coverage != null && coverage < COVERAGE_COMPLETE) ||
|
|
831
863
|
coverageUnknown ||
|
|
832
864
|
blindHigh > 0 ||
|
|
865
|
+
premiseUnmeasured ||
|
|
833
866
|
assertedMutations > 0);
|
|
834
867
|
// THE PREMISE GATE. Every other rung above softens a claim about what SPARDA
|
|
835
868
|
// ANALYSED. This one answers a prior question — was the analysis about the whole
|
|
@@ -860,6 +893,11 @@ export function verdictOf(
|
|
|
860
893
|
coverageUnknown,
|
|
861
894
|
premiseGaps,
|
|
862
895
|
premiseUnverified,
|
|
896
|
+
// Carried so every surface can say WHY the word was withheld. A PARTIAL whose reason is
|
|
897
|
+
// "nobody checked the route table" reads very differently from one whose reason is "77 %
|
|
898
|
+
// coverage", and a user who cannot tell them apart cannot act on either.
|
|
899
|
+
premiseBasis,
|
|
900
|
+
premiseUnmeasured,
|
|
863
901
|
// The CI gate. A premise gap belongs here and not only in the verdict word: an
|
|
864
902
|
// app whose route surface we demonstrably did not have cannot be called safe, and
|
|
865
903
|
// a green CI over it would reproduce the exact failure this whole audit removed —
|
package/src/ubg/falsify.js
CHANGED
|
@@ -82,8 +82,12 @@ export function falsifyGraph(graph, { check = checkGraph } = {}) {
|
|
|
82
82
|
controls: [],
|
|
83
83
|
flipped: 0,
|
|
84
84
|
holes: [],
|
|
85
|
-
score
|
|
86
|
-
|
|
85
|
+
// NOT 1. A score of 1 means "every green depends on its guard, and we checked". Here
|
|
86
|
+
// nothing was checked, and 1 is the number a reader acts on while the `note` sits
|
|
87
|
+
// beside it, unread. `null` cannot be misread — it is the only value that carries its
|
|
88
|
+
// own caveat (ADR-092: the admission goes INSIDE the headline, never next to it).
|
|
89
|
+
score: null,
|
|
90
|
+
note: 'no protected mutation routes — nothing to falsify (score UNMEASURED, not 1)',
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
// Per-route attribution, flood-aware: ablating EVERY guard at once makes UNGUARDED_MUTATION
|
package/src/ubg/immunity.js
CHANGED
|
@@ -19,7 +19,7 @@ import { AXES, posture, provenByPolarity, packVector, exposedAxes } from './pola
|
|
|
19
19
|
export const CAPSULE_VERSION = 'imm1';
|
|
20
20
|
|
|
21
21
|
// canonical graph → a compact, portable, deterministic safety capsule.
|
|
22
|
-
export function buildCapsule(graph) {
|
|
22
|
+
export function buildCapsule(graph, { premiseBasis = null } = {}) {
|
|
23
23
|
const prints = new Map(fingerprintGraph(graph).map((p) => [p.entrypoint, p]));
|
|
24
24
|
const { polarity } = checkGraph(graph);
|
|
25
25
|
|
|
@@ -38,9 +38,29 @@ export function buildCapsule(graph) {
|
|
|
38
38
|
// only if it says so. The genome inherits this, so the world memory carries not just
|
|
39
39
|
// "proven" but "proven over how much" — a verdict's confidence, made portable.
|
|
40
40
|
const survey = surveyBlindspots(graph);
|
|
41
|
+
// THE CAPSULE CARRIES ITS OWN BASIS OF MEASUREMENT (ADR-092). A capsule is PORTABLE — it
|
|
42
|
+
// travels between repos and is replayed by `speculate`/`immunize` far from the analysis that
|
|
43
|
+
// produced it. A frozen `proven: true` whose premise nobody ever measured would be a claim
|
|
44
|
+
// re-asserted in a context where its licence cannot even be checked, which is worse than the
|
|
45
|
+
// original E-104: there, at least, the oracle was one call away.
|
|
46
|
+
//
|
|
47
|
+
// So `proven` is a THREE-state field, not a boolean. `null` = the premise was never measured,
|
|
48
|
+
// and no consumer can read a pass out of it. Absent basis (an older capsule, or a caller
|
|
49
|
+
// with no premise to speak of) keeps the previous semantics rather than retroactively
|
|
50
|
+
// invalidating capsules already in the wild.
|
|
51
|
+
//
|
|
52
|
+
// ONLY THE POSITIVE IS WITHHELD, and getting this wrong is a regression in the unsafe
|
|
53
|
+
// direction. The premise bounds the route SET; a route that is missing from the graph cannot
|
|
54
|
+
// rescue one that IS in it and is exposed. So `proven: false` needs no premise and survives
|
|
55
|
+
// untouched — the direction premise.js states plainly: gaps WITHHOLD a verdict, never grant
|
|
56
|
+
// one. Blanking `false` to `null` would have turned "this app has an unguarded mutation"
|
|
57
|
+
// into "we don't know", which is the same lie pointed the other way.
|
|
58
|
+
const premiseUnmeasured = premiseBasis === 'unmeasured';
|
|
59
|
+
const provenByPol = !surfaceOnly && provenByPolarity(polarity);
|
|
41
60
|
return {
|
|
42
61
|
v: CAPSULE_VERSION,
|
|
43
|
-
proven:
|
|
62
|
+
proven: premiseUnmeasured && provenByPol ? null : provenByPol,
|
|
63
|
+
premiseBasis,
|
|
44
64
|
surfaceOnly,
|
|
45
65
|
coverage: survey.coverage.ratio,
|
|
46
66
|
blindHigh: survey.byRisk.critical + survey.byRisk.high,
|
package/src/ubg/premise.js
CHANGED
|
@@ -181,18 +181,53 @@ export async function premiseFor(graph, report, { cwd, probe = false } = {}) {
|
|
|
181
181
|
if (!wanted)
|
|
182
182
|
return {
|
|
183
183
|
available: false,
|
|
184
|
+
basis: basisOf(report.framework),
|
|
184
185
|
gaps: [],
|
|
185
186
|
probed: 0,
|
|
186
187
|
reason: PROBEABLE.has(report.framework)
|
|
187
188
|
? 'runtime oracle not requested (--probe)'
|
|
188
189
|
: `no oracle for ${report.framework}`,
|
|
189
190
|
};
|
|
190
|
-
|
|
191
|
+
const verified = await verifyPremise(graph, {
|
|
191
192
|
framework: report.framework,
|
|
192
193
|
entryFile: report.entry,
|
|
193
194
|
cwd,
|
|
194
195
|
unknownHandlers: report.unknownHandlers,
|
|
195
196
|
});
|
|
197
|
+
return {
|
|
198
|
+
...verified,
|
|
199
|
+
basis: verified.available ? 'measured' : basisOf(report.framework),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// On what BASIS do we hold the premise — that the route set analysed is the route set the
|
|
204
|
+
// app serves? Three answers, and collapsing the last two is E-104:
|
|
205
|
+
//
|
|
206
|
+
// 'measured' — an oracle that is not the analyser enumerated the surface and agreed.
|
|
207
|
+
// 'declared' — there is nothing to cross-check against because the artefact analysed IS
|
|
208
|
+
// the route table. An OpenAPI document does not have a premise behind it;
|
|
209
|
+
// it is the premise. Demanding a second witness for it would be demanding
|
|
210
|
+
// that a map be verified against itself.
|
|
211
|
+
// 'unmeasured' — no oracle ran. NOT the same as "an oracle ran and found nothing", and the
|
|
212
|
+
// whole point of naming this state is that the verdict must be able to tell
|
|
213
|
+
// the difference (E-104: `PROVEN` was reachable on 7 of our 8 proving
|
|
214
|
+
// fixtures with no oracle ever having run).
|
|
215
|
+
export function basisOf(framework) {
|
|
216
|
+
if (framework === 'openapi') return 'declared';
|
|
217
|
+
return 'unmeasured';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The basis of a premise RESULT, as every consumer needs it. `premiseFor` already computes
|
|
221
|
+
// it; NINE call sites re-derived it by hand anyway — the same ternary, copied, which is nine
|
|
222
|
+
// chances to get it subtly wrong and one more shape to keep in sync.
|
|
223
|
+
//
|
|
224
|
+
// The default is the load-bearing part: a caller that has NO premise object at all — because
|
|
225
|
+
// it never asked — gets 'unmeasured', not 'measured' and not `null`. Forgetting to measure
|
|
226
|
+
// must fail toward the weaker word, never toward silence. That is the whole of E-106 in one
|
|
227
|
+
// default value.
|
|
228
|
+
export function basisFrom(premise) {
|
|
229
|
+
if (premise?.available) return 'measured';
|
|
230
|
+
return premise?.basis ?? 'unmeasured';
|
|
196
231
|
}
|
|
197
232
|
|
|
198
233
|
// Fold the gaps into the report the blind-spot ledger reads. Every caller did these two
|