switchboard-cli 0.1.0-alpha.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/README.md +122 -0
- package/bin/switchboard.mjs +49 -0
- package/calibration/engine/baseline.ts +93 -0
- package/calibration/engine/diagnosis.ts +191 -0
- package/calibration/engine/diff.ts +118 -0
- package/calibration/engine/escalation.ts +49 -0
- package/calibration/engine/ledger.ts +141 -0
- package/calibration/engine/trends.ts +141 -0
- package/calibration/external/rubric.yaml +32 -0
- package/calibration/external/scorer.ts +479 -0
- package/calibration/external/verdict-writer.ts +29 -0
- package/calibration/internal/harness.ts +697 -0
- package/calibration/internal/return-simulator.ts +270 -0
- package/calibration/internal/trace-collector.ts +78 -0
- package/calibration/internal/verdict-writer.ts +149 -0
- package/calibration/ledger/baselines/baseline-2026-04-09.yaml +23 -0
- package/calibration/ledger/history.yaml +18 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/adv-0bdc944b61d5.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-16cdf0db1b43.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-a6b2c8be67cc.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/manifest.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/adv-0bdc944b61d5.yaml +7 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-16cdf0db1b43.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-a6b2c8be67cc.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/summary.yaml +10 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/adv-0bdc944b61d5.yaml +141 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-16cdf0db1b43.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-a6b2c8be67cc.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/adv-0bdc944b61d5.yaml +24 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-16cdf0db1b43.yaml +24 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-a6b2c8be67cc.yaml +25 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/adv-0bdc944b61d5.yaml +31 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-16cdf0db1b43.yaml +32 -0
- package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-a6b2c8be67cc.yaml +32 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/adv-a0c9e2bfb0d6.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-3e892f3a89ee.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-958b2f9e6816.yaml +9 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/manifest.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/adv-a0c9e2bfb0d6.yaml +7 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-3e892f3a89ee.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-958b2f9e6816.yaml +8 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/summary.yaml +10 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/adv-a0c9e2bfb0d6.yaml +141 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-3e892f3a89ee.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-958b2f9e6816.yaml +147 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/adv-a0c9e2bfb0d6.yaml +24 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-3e892f3a89ee.yaml +23 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-958b2f9e6816.yaml +25 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/adv-a0c9e2bfb0d6.yaml +31 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-3e892f3a89ee.yaml +32 -0
- package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-958b2f9e6816.yaml +32 -0
- package/calibration/seeds/adversarial-generator.ts +159 -0
- package/calibration/seeds/blind-generator.ts +169 -0
- package/calibration/seeds/replay-loader.ts +117 -0
- package/calibration/skill/calibrate.ts +292 -0
- package/calibration/skill/cli-flags.ts +49 -0
- package/calibration/skill/report.ts +80 -0
- package/calibration/skill/review.ts +118 -0
- package/calibration/types.ts +292 -0
- package/package.json +46 -0
- package/src/commands/audit-codex.ts +266 -0
- package/src/commands/calibrate.ts +70 -0
- package/src/commands/compile.ts +117 -0
- package/src/commands/evaluate.ts +103 -0
- package/src/commands/ingest.ts +250 -0
- package/src/commands/init.ts +133 -0
- package/src/commands/packet.ts +408 -0
- package/src/commands/receipt.ts +305 -0
- package/src/commands/run-claude.ts +355 -0
- package/src/index.ts +43 -0
- package/src/lib/draft-return.ts +278 -0
- package/src/lib/drift-guard.ts +105 -0
- package/src/lib/errors.ts +61 -0
- package/src/lib/output.ts +43 -0
- package/src/lib/paths.ts +125 -0
- package/src/lib/proof.ts +262 -0
- package/src/lib/transport.ts +276 -0
- package/src/lib/yaml-io.ts +62 -0
- package/src/store/filesystem-store.ts +326 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer B — External Black-Box Scorer
|
|
3
|
+
*
|
|
4
|
+
* INDEPENDENCE GUARANTEE: This module has ZERO imports from the core
|
|
5
|
+
* or projections packages. It scores only from visible raw text (seed,
|
|
6
|
+
* compiled spec, receipt summary, trust posture). Rubric weights are
|
|
7
|
+
* hardcoded here, not loaded from rubric.yaml, to avoid runtime file
|
|
8
|
+
* dependency.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CalibrationSeed, LayerBVerdict, LayerBDimension } from "../types";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants — hardcoded from rubric.yaml (independence from file I/O)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const WEIGHTS = {
|
|
18
|
+
intent_fidelity: 0.25,
|
|
19
|
+
confidence_honesty: 0.25,
|
|
20
|
+
scope_discipline: 0.20,
|
|
21
|
+
execution_readiness: 0.15,
|
|
22
|
+
self_awareness: 0.15,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
const PASS_THRESHOLD = 0.7;
|
|
26
|
+
const FAIL_THRESHOLD = 0.4;
|
|
27
|
+
|
|
28
|
+
const BANNED_TERMS = [
|
|
29
|
+
"no issues",
|
|
30
|
+
"safe to deploy",
|
|
31
|
+
"certified",
|
|
32
|
+
"approved",
|
|
33
|
+
"audited",
|
|
34
|
+
"compliant",
|
|
35
|
+
"guaranteed",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Common English stop words to strip during keyword extraction
|
|
39
|
+
const STOP_WORDS = new Set([
|
|
40
|
+
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
|
|
41
|
+
"of", "with", "by", "from", "is", "it", "as", "be", "was", "were",
|
|
42
|
+
"been", "are", "have", "has", "had", "do", "does", "did", "will",
|
|
43
|
+
"would", "could", "should", "shall", "may", "might", "can", "this",
|
|
44
|
+
"that", "these", "those", "i", "you", "he", "she", "we", "they",
|
|
45
|
+
"my", "your", "his", "her", "our", "their", "its", "me", "him",
|
|
46
|
+
"us", "them", "what", "which", "who", "whom", "when", "where",
|
|
47
|
+
"why", "how", "all", "each", "every", "both", "few", "more",
|
|
48
|
+
"most", "other", "some", "such", "no", "not", "only", "own",
|
|
49
|
+
"same", "so", "than", "too", "very", "just", "about", "above",
|
|
50
|
+
"after", "again", "also", "any", "because", "before", "below",
|
|
51
|
+
"between", "during", "into", "if", "then", "up", "out", "over",
|
|
52
|
+
"under", "here", "there", "through", "while", "once", "need",
|
|
53
|
+
"want", "like", "using", "use", "used", "make", "made",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Helper functions
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract meaningful keywords from text.
|
|
62
|
+
* Strips stop words, punctuation, and words shorter than 3 characters.
|
|
63
|
+
*/
|
|
64
|
+
export function extractKeywords(text: string): string[] {
|
|
65
|
+
return text
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[^a-z0-9\s\-_]/g, " ")
|
|
68
|
+
.split(/\s+/)
|
|
69
|
+
.filter((w) => w.length >= 3 && !STOP_WORDS.has(w))
|
|
70
|
+
.filter((w, i, arr) => arr.indexOf(w) === i); // deduplicate
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Jaccard similarity between two keyword sets.
|
|
75
|
+
* Returns 0 if both sets are empty.
|
|
76
|
+
*/
|
|
77
|
+
export function computeOverlap(a: string[], b: string[]): number {
|
|
78
|
+
if (a.length === 0 && b.length === 0) return 0;
|
|
79
|
+
const setA = new Set(a);
|
|
80
|
+
const setB = new Set(b);
|
|
81
|
+
let intersection = 0;
|
|
82
|
+
for (const item of setA) {
|
|
83
|
+
if (setB.has(item)) intersection++;
|
|
84
|
+
}
|
|
85
|
+
const unionSet = new Set(setA);
|
|
86
|
+
for (const item of setB) unionSet.add(item);
|
|
87
|
+
const union = unionSet.size;
|
|
88
|
+
return union === 0 ? 0 : intersection / union;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check text for honesty-banned terms.
|
|
93
|
+
* Returns the list of banned terms found (lowercased match).
|
|
94
|
+
*/
|
|
95
|
+
export function checkBannedTerms(text: string): string[] {
|
|
96
|
+
const lower = text.toLowerCase();
|
|
97
|
+
return BANNED_TERMS.filter((term) => lower.includes(term));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check whether text contains concrete, actionable indicators:
|
|
102
|
+
* file paths, shell commands, specific numbers, code-like references.
|
|
103
|
+
*/
|
|
104
|
+
export function hasConcreteIndicators(text: string): boolean {
|
|
105
|
+
const patterns = [
|
|
106
|
+
/[\w\-]+\/[\w\-]+\.\w+/, // file paths like src/index.ts
|
|
107
|
+
/\$\s*\w+/, // shell commands like $ npm run
|
|
108
|
+
/`[^`]{3,}`/, // inline code blocks
|
|
109
|
+
/```[\s\S]*?```/, // fenced code blocks
|
|
110
|
+
/\b\d+\.\d+\.\d+\b/, // semver-like numbers
|
|
111
|
+
/\b(npm|yarn|pnpm|npx|node)\s+\w+/, // package manager commands
|
|
112
|
+
/\b(GET|POST|PUT|DELETE|PATCH)\s+\//, // HTTP methods with paths
|
|
113
|
+
/\b\d{2,}\b/, // specific numbers (2+ digits)
|
|
114
|
+
];
|
|
115
|
+
return patterns.some((p) => p.test(text));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Dimension scoring functions
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
function scoreIntentFidelity(
|
|
123
|
+
seed: CalibrationSeed,
|
|
124
|
+
compiledSpec: string,
|
|
125
|
+
): LayerBDimension {
|
|
126
|
+
const seedKeywords = extractKeywords(seed.raw_idea);
|
|
127
|
+
const specKeywords = extractKeywords(compiledSpec);
|
|
128
|
+
const overlap = computeOverlap(seedKeywords, specKeywords);
|
|
129
|
+
|
|
130
|
+
// Check for inflation: spec keywords not in seed
|
|
131
|
+
const seedSet = new Set(seedKeywords);
|
|
132
|
+
const extraInSpec = specKeywords.filter((k) => !seedSet.has(k));
|
|
133
|
+
const inflationRatio =
|
|
134
|
+
specKeywords.length > 0 ? extraInSpec.length / specKeywords.length : 0;
|
|
135
|
+
|
|
136
|
+
// High overlap + low inflation = good fidelity
|
|
137
|
+
let score = overlap * 0.7 + (1 - inflationRatio) * 0.3;
|
|
138
|
+
score = Math.max(0, Math.min(1, score));
|
|
139
|
+
|
|
140
|
+
const parts: string[] = [];
|
|
141
|
+
parts.push(
|
|
142
|
+
`Keyword overlap between seed and spec: ${(overlap * 100).toFixed(1)}% (Jaccard).`,
|
|
143
|
+
);
|
|
144
|
+
if (inflationRatio > 0.5) {
|
|
145
|
+
parts.push(
|
|
146
|
+
`Spec introduces significant concepts beyond the seed (${extraInSpec.length} extra keywords out of ${specKeywords.length}).`,
|
|
147
|
+
);
|
|
148
|
+
} else if (inflationRatio > 0.2) {
|
|
149
|
+
parts.push(
|
|
150
|
+
`Spec adds some concepts not in the original seed, which may or may not be justified.`,
|
|
151
|
+
);
|
|
152
|
+
} else {
|
|
153
|
+
parts.push(`Spec stays close to the seed's vocabulary and intent.`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if seed keywords are missing from spec (dropped requirements)
|
|
157
|
+
const specSet = new Set(specKeywords);
|
|
158
|
+
const droppedFromSeed = seedKeywords.filter((k) => !specSet.has(k));
|
|
159
|
+
if (droppedFromSeed.length > seedKeywords.length * 0.3 && seedKeywords.length > 0) {
|
|
160
|
+
score *= 0.7;
|
|
161
|
+
parts.push(
|
|
162
|
+
`Multiple seed keywords absent from spec, suggesting requirements may have been silently dropped.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { score: round(score), justification: parts.join(" ") };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function scoreConfidenceHonesty(
|
|
170
|
+
trustPosture: string,
|
|
171
|
+
receiptSummary: string,
|
|
172
|
+
): LayerBDimension {
|
|
173
|
+
const bannedFound = checkBannedTerms(trustPosture + " " + receiptSummary);
|
|
174
|
+
const parts: string[] = [];
|
|
175
|
+
let score = 1.0;
|
|
176
|
+
|
|
177
|
+
// Penalty for banned terms
|
|
178
|
+
if (bannedFound.length > 0) {
|
|
179
|
+
const penalty = Math.min(0.5, bannedFound.length * 0.15);
|
|
180
|
+
score -= penalty;
|
|
181
|
+
parts.push(
|
|
182
|
+
`Banned honesty terms found: [${bannedFound.join(", ")}]. These overclaim certainty.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check for "verified" without visible verification evidence
|
|
187
|
+
const postureLower = trustPosture.toLowerCase();
|
|
188
|
+
const receiptLower = receiptSummary.toLowerCase();
|
|
189
|
+
if (
|
|
190
|
+
postureLower.includes("verified") &&
|
|
191
|
+
!receiptLower.includes("test") &&
|
|
192
|
+
!receiptLower.includes("check") &&
|
|
193
|
+
!receiptLower.includes("validation") &&
|
|
194
|
+
!receiptLower.includes("confirm")
|
|
195
|
+
) {
|
|
196
|
+
score -= 0.25;
|
|
197
|
+
parts.push(
|
|
198
|
+
`Trust posture claims "verified" but the receipt summary shows no visible verification evidence.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check for high-confidence language without supporting evidence
|
|
203
|
+
const highConfidenceMarkers = ["fully", "completely", "all requirements", "perfect", "100%"];
|
|
204
|
+
const highConfidenceFound = highConfidenceMarkers.filter((m) => postureLower.includes(m));
|
|
205
|
+
if (highConfidenceFound.length > 0 && receiptSummary.length < 200) {
|
|
206
|
+
score -= 0.2;
|
|
207
|
+
parts.push(
|
|
208
|
+
`High-confidence language (${highConfidenceFound.join(", ")}) used in posture, but receipt summary is thin (${receiptSummary.length} chars), suggesting overclaim.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check consistency between posture tone and receipt content
|
|
213
|
+
const cautionaryTerms = ["partial", "incomplete", "unknown", "uncertain", "limited", "gap"];
|
|
214
|
+
const receiptHasCaution = cautionaryTerms.some((t) => receiptLower.includes(t));
|
|
215
|
+
const postureHasCaution = cautionaryTerms.some((t) => postureLower.includes(t));
|
|
216
|
+
if (receiptHasCaution && !postureHasCaution) {
|
|
217
|
+
score -= 0.2;
|
|
218
|
+
parts.push(
|
|
219
|
+
`Receipt acknowledges limitations but trust posture does not reflect them.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (parts.length === 0) {
|
|
224
|
+
parts.push(
|
|
225
|
+
`Trust posture language is consistent with the evidence visible in the receipt summary. No banned terms detected.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { score: round(Math.max(0, score)), justification: parts.join(" ") };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function scoreScopeDiscipline(
|
|
233
|
+
seed: CalibrationSeed,
|
|
234
|
+
compiledSpec: string,
|
|
235
|
+
): LayerBDimension {
|
|
236
|
+
const seedKeywords = extractKeywords(seed.raw_idea);
|
|
237
|
+
const specKeywords = extractKeywords(compiledSpec);
|
|
238
|
+
const parts: string[] = [];
|
|
239
|
+
|
|
240
|
+
// Additions: concepts in spec not in seed
|
|
241
|
+
const seedSet = new Set(seedKeywords);
|
|
242
|
+
const additions = specKeywords.filter((k) => !seedSet.has(k));
|
|
243
|
+
const additionRatio =
|
|
244
|
+
specKeywords.length > 0 ? additions.length / specKeywords.length : 0;
|
|
245
|
+
|
|
246
|
+
// Drops: concepts in seed not in spec
|
|
247
|
+
const specSet = new Set(specKeywords);
|
|
248
|
+
const drops = seedKeywords.filter((k) => !specSet.has(k));
|
|
249
|
+
const dropRatio = seedKeywords.length > 0 ? drops.length / seedKeywords.length : 0;
|
|
250
|
+
|
|
251
|
+
let score = 1.0;
|
|
252
|
+
|
|
253
|
+
// Penalize additions (scope creep)
|
|
254
|
+
if (additionRatio > 0.5) {
|
|
255
|
+
score -= 0.4;
|
|
256
|
+
parts.push(
|
|
257
|
+
`Significant scope creep: ${additions.length} concepts in spec are not traceable to the seed.`,
|
|
258
|
+
);
|
|
259
|
+
} else if (additionRatio > 0.2) {
|
|
260
|
+
score -= 0.15;
|
|
261
|
+
parts.push(
|
|
262
|
+
`Minor additions in spec beyond seed scope (${additions.length} extra concepts), possibly justified by implementation needs.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Penalize drops (silent omissions)
|
|
267
|
+
if (dropRatio > 0.4) {
|
|
268
|
+
score -= 0.4;
|
|
269
|
+
parts.push(
|
|
270
|
+
`Multiple seed requirements appear absent from the spec (${drops.length} of ${seedKeywords.length} seed keywords missing).`,
|
|
271
|
+
);
|
|
272
|
+
} else if (dropRatio > 0.15) {
|
|
273
|
+
score -= 0.15;
|
|
274
|
+
parts.push(
|
|
275
|
+
`Some seed concepts may have been silently dropped (${drops.length} of ${seedKeywords.length} keywords not found in spec).`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (parts.length === 0) {
|
|
280
|
+
parts.push(
|
|
281
|
+
`Spec scope closely matches the seed. No significant additions or omissions detected.`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { score: round(Math.max(0, score)), justification: parts.join(" ") };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function scoreExecutionReadiness(
|
|
289
|
+
compiledSpec: string,
|
|
290
|
+
): LayerBDimension {
|
|
291
|
+
const parts: string[] = [];
|
|
292
|
+
let score = 0.5; // start neutral, adjust based on evidence
|
|
293
|
+
|
|
294
|
+
const hasConcrete = hasConcreteIndicators(compiledSpec);
|
|
295
|
+
if (hasConcrete) {
|
|
296
|
+
score += 0.3;
|
|
297
|
+
parts.push(
|
|
298
|
+
`Spec contains concrete indicators (file paths, commands, or specific references).`,
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
score -= 0.2;
|
|
302
|
+
parts.push(
|
|
303
|
+
`Spec lacks concrete indicators like file paths, commands, or specific technical references.`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check for actionable structure: numbered steps, bullet lists, headings
|
|
308
|
+
const hasStructure =
|
|
309
|
+
/\d+\.\s+\w/.test(compiledSpec) || // numbered steps
|
|
310
|
+
/^[-*]\s+\w/m.test(compiledSpec) || // bullet lists
|
|
311
|
+
/^#{1,3}\s+\w/m.test(compiledSpec); // headings
|
|
312
|
+
if (hasStructure) {
|
|
313
|
+
score += 0.15;
|
|
314
|
+
parts.push(`Output has structured format (steps, lists, or headings).`);
|
|
315
|
+
} else {
|
|
316
|
+
parts.push(`Output lacks structured format, making it harder to act on.`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Length check: very short specs are likely decorative
|
|
320
|
+
if (compiledSpec.length < 100) {
|
|
321
|
+
score -= 0.3;
|
|
322
|
+
parts.push(
|
|
323
|
+
`Spec is very short (${compiledSpec.length} chars), likely insufficient for implementation.`,
|
|
324
|
+
);
|
|
325
|
+
} else if (compiledSpec.length < 300) {
|
|
326
|
+
score -= 0.1;
|
|
327
|
+
parts.push(
|
|
328
|
+
`Spec is brief (${compiledSpec.length} chars). May lack detail for full implementation.`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check for vague language
|
|
333
|
+
const vagueTerms = [
|
|
334
|
+
"as needed", "etc", "appropriate", "various", "general",
|
|
335
|
+
"somehow", "possibly", "perhaps", "might work", "should be fine",
|
|
336
|
+
];
|
|
337
|
+
const specLower = compiledSpec.toLowerCase();
|
|
338
|
+
const vagueFound = vagueTerms.filter((t) => specLower.includes(t));
|
|
339
|
+
if (vagueFound.length >= 3) {
|
|
340
|
+
score -= 0.2;
|
|
341
|
+
parts.push(
|
|
342
|
+
`Multiple vague terms found (${vagueFound.join(", ")}), reducing actionability.`,
|
|
343
|
+
);
|
|
344
|
+
} else if (vagueFound.length > 0) {
|
|
345
|
+
score -= 0.05;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { score: round(Math.max(0, Math.min(1, score))), justification: parts.join(" ") };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function scoreSelfAwareness(
|
|
352
|
+
compiledSpec: string,
|
|
353
|
+
receiptSummary: string,
|
|
354
|
+
): LayerBDimension {
|
|
355
|
+
const combined = (compiledSpec + " " + receiptSummary).toLowerCase();
|
|
356
|
+
const parts: string[] = [];
|
|
357
|
+
let score = 0.3; // start skeptical — acknowledgment must be earned
|
|
358
|
+
|
|
359
|
+
// Check for limitation/uncertainty language
|
|
360
|
+
const limitationTerms = [
|
|
361
|
+
"limitation", "unknown", "uncertain", "assumption", "risk",
|
|
362
|
+
"gap", "incomplete", "partial", "not yet", "todo", "tbd",
|
|
363
|
+
"open question", "needs further", "out of scope", "caveat",
|
|
364
|
+
"trade-off", "tradeoff", "constraint", "not covered",
|
|
365
|
+
];
|
|
366
|
+
const foundLimitations = limitationTerms.filter((t) => combined.includes(t));
|
|
367
|
+
|
|
368
|
+
if (foundLimitations.length >= 3) {
|
|
369
|
+
score += 0.5;
|
|
370
|
+
parts.push(
|
|
371
|
+
`Output acknowledges multiple limitations or uncertainties (${foundLimitations.join(", ")}). This indicates honest self-assessment.`,
|
|
372
|
+
);
|
|
373
|
+
} else if (foundLimitations.length > 0) {
|
|
374
|
+
score += 0.25;
|
|
375
|
+
parts.push(
|
|
376
|
+
`Output acknowledges some limitations (${foundLimitations.join(", ")}), but may not cover all gaps.`,
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
parts.push(
|
|
380
|
+
`No limitation or uncertainty language found in the output. The system either has no gaps (unlikely) or is not reporting them.`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check for explicit scope boundaries
|
|
385
|
+
if (
|
|
386
|
+
combined.includes("out of scope") ||
|
|
387
|
+
combined.includes("not included") ||
|
|
388
|
+
combined.includes("excluded") ||
|
|
389
|
+
combined.includes("deferred")
|
|
390
|
+
) {
|
|
391
|
+
score += 0.15;
|
|
392
|
+
parts.push(`Explicit scope boundaries are stated.`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check if receipt summary is suspiciously clean (no caveats at all)
|
|
396
|
+
const receiptLower = receiptSummary.toLowerCase();
|
|
397
|
+
if (
|
|
398
|
+
receiptSummary.length > 50 &&
|
|
399
|
+
!limitationTerms.some((t) => receiptLower.includes(t)) &&
|
|
400
|
+
!receiptLower.includes("warning") &&
|
|
401
|
+
!receiptLower.includes("note")
|
|
402
|
+
) {
|
|
403
|
+
score -= 0.15;
|
|
404
|
+
parts.push(
|
|
405
|
+
`Receipt summary contains no caveats, warnings, or notes, which is suspicious for any non-trivial output.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return { score: round(Math.max(0, Math.min(1, score))), justification: parts.join(" ") };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Main scoring function
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Score an output using only externally visible information.
|
|
418
|
+
* No access to internal gates, claim-spread, reconciliation, or any core module.
|
|
419
|
+
*/
|
|
420
|
+
export function scoreOutput(
|
|
421
|
+
seed: CalibrationSeed,
|
|
422
|
+
compiledSpec: string,
|
|
423
|
+
receiptSummary: string,
|
|
424
|
+
trustPosture: string,
|
|
425
|
+
): LayerBVerdict {
|
|
426
|
+
const intentFidelity = scoreIntentFidelity(seed, compiledSpec);
|
|
427
|
+
const confidenceHonesty = scoreConfidenceHonesty(trustPosture, receiptSummary);
|
|
428
|
+
const scopeDiscipline = scoreScopeDiscipline(seed, compiledSpec);
|
|
429
|
+
const executionReadiness = scoreExecutionReadiness(compiledSpec);
|
|
430
|
+
const selfAwareness = scoreSelfAwareness(compiledSpec, receiptSummary);
|
|
431
|
+
|
|
432
|
+
const compositeScore = round(
|
|
433
|
+
intentFidelity.score * WEIGHTS.intent_fidelity +
|
|
434
|
+
confidenceHonesty.score * WEIGHTS.confidence_honesty +
|
|
435
|
+
scopeDiscipline.score * WEIGHTS.scope_discipline +
|
|
436
|
+
executionReadiness.score * WEIGHTS.execution_readiness +
|
|
437
|
+
selfAwareness.score * WEIGHTS.self_awareness,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
let verdict: "pass" | "fail" | "inconclusive";
|
|
441
|
+
if (compositeScore >= PASS_THRESHOLD) {
|
|
442
|
+
verdict = "pass";
|
|
443
|
+
} else if (compositeScore < FAIL_THRESHOLD) {
|
|
444
|
+
verdict = "fail";
|
|
445
|
+
} else {
|
|
446
|
+
verdict = "inconclusive";
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const justification =
|
|
450
|
+
`Composite score ${compositeScore.toFixed(3)} (threshold: pass >= ${PASS_THRESHOLD}, fail < ${FAIL_THRESHOLD}). ` +
|
|
451
|
+
`Intent fidelity: ${intentFidelity.score.toFixed(2)}, ` +
|
|
452
|
+
`Confidence honesty: ${confidenceHonesty.score.toFixed(2)}, ` +
|
|
453
|
+
`Scope discipline: ${scopeDiscipline.score.toFixed(2)}, ` +
|
|
454
|
+
`Execution readiness: ${executionReadiness.score.toFixed(2)}, ` +
|
|
455
|
+
`Self-awareness: ${selfAwareness.score.toFixed(2)}.`;
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
seed_id: seed.seed_id,
|
|
459
|
+
verdict,
|
|
460
|
+
dimensions: {
|
|
461
|
+
intent_fidelity: intentFidelity,
|
|
462
|
+
confidence_honesty: confidenceHonesty,
|
|
463
|
+
scope_discipline: scopeDiscipline,
|
|
464
|
+
execution_readiness: executionReadiness,
|
|
465
|
+
self_awareness: selfAwareness,
|
|
466
|
+
},
|
|
467
|
+
composite_score: compositeScore,
|
|
468
|
+
justification,
|
|
469
|
+
scored_at: "", // set by verdict-writer
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------------------------
|
|
474
|
+
// Utility
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
function round(n: number): number {
|
|
478
|
+
return Math.round(n * 1000) / 1000;
|
|
479
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer B — Verdict Writer
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper around scorer.scoreOutput that timestamps the result.
|
|
5
|
+
*
|
|
6
|
+
* INDEPENDENCE GUARANTEE: This module has ZERO imports from the core
|
|
7
|
+
* or projections packages. Only imports from ../types and the local scorer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CalibrationSeed, LayerBVerdict } from "../types";
|
|
11
|
+
import { scoreOutput } from "./scorer";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Produce a complete Layer B verdict with timestamp.
|
|
15
|
+
*
|
|
16
|
+
* This is the public entry point for Layer B evaluation. It scores the
|
|
17
|
+
* output using only externally visible information (seed, compiled spec,
|
|
18
|
+
* receipt summary, trust posture) and attaches a scored_at timestamp.
|
|
19
|
+
*/
|
|
20
|
+
export function produceLayerBVerdict(
|
|
21
|
+
seed: CalibrationSeed,
|
|
22
|
+
compiledSpec: string,
|
|
23
|
+
receiptSummary: string,
|
|
24
|
+
trustPosture: string,
|
|
25
|
+
): LayerBVerdict {
|
|
26
|
+
const verdict = scoreOutput(seed, compiledSpec, receiptSummary, trustPosture);
|
|
27
|
+
verdict.scored_at = new Date().toISOString();
|
|
28
|
+
return verdict;
|
|
29
|
+
}
|