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.
Files changed (79) hide show
  1. package/README.md +122 -0
  2. package/bin/switchboard.mjs +49 -0
  3. package/calibration/engine/baseline.ts +93 -0
  4. package/calibration/engine/diagnosis.ts +191 -0
  5. package/calibration/engine/diff.ts +118 -0
  6. package/calibration/engine/escalation.ts +49 -0
  7. package/calibration/engine/ledger.ts +141 -0
  8. package/calibration/engine/trends.ts +141 -0
  9. package/calibration/external/rubric.yaml +32 -0
  10. package/calibration/external/scorer.ts +479 -0
  11. package/calibration/external/verdict-writer.ts +29 -0
  12. package/calibration/internal/harness.ts +697 -0
  13. package/calibration/internal/return-simulator.ts +270 -0
  14. package/calibration/internal/trace-collector.ts +78 -0
  15. package/calibration/internal/verdict-writer.ts +149 -0
  16. package/calibration/ledger/baselines/baseline-2026-04-09.yaml +23 -0
  17. package/calibration/ledger/history.yaml +18 -0
  18. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/adv-0bdc944b61d5.yaml +9 -0
  19. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-16cdf0db1b43.yaml +9 -0
  20. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/diffs/blind-a6b2c8be67cc.yaml +9 -0
  21. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/manifest.yaml +8 -0
  22. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/adv-0bdc944b61d5.yaml +7 -0
  23. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-16cdf0db1b43.yaml +8 -0
  24. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/seeds/blind-a6b2c8be67cc.yaml +8 -0
  25. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/summary.yaml +10 -0
  26. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/adv-0bdc944b61d5.yaml +141 -0
  27. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-16cdf0db1b43.yaml +147 -0
  28. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/traces/blind-a6b2c8be67cc.yaml +147 -0
  29. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/adv-0bdc944b61d5.yaml +24 -0
  30. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-16cdf0db1b43.yaml +24 -0
  31. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-a/blind-a6b2c8be67cc.yaml +25 -0
  32. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/adv-0bdc944b61d5.yaml +31 -0
  33. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-16cdf0db1b43.yaml +32 -0
  34. package/calibration/ledger/runs/2026-04-09T09-45-01-838Z/verdicts-b/blind-a6b2c8be67cc.yaml +32 -0
  35. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/adv-a0c9e2bfb0d6.yaml +9 -0
  36. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-3e892f3a89ee.yaml +9 -0
  37. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/diffs/blind-958b2f9e6816.yaml +9 -0
  38. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/manifest.yaml +8 -0
  39. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/adv-a0c9e2bfb0d6.yaml +7 -0
  40. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-3e892f3a89ee.yaml +8 -0
  41. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/seeds/blind-958b2f9e6816.yaml +8 -0
  42. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/summary.yaml +10 -0
  43. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/adv-a0c9e2bfb0d6.yaml +141 -0
  44. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-3e892f3a89ee.yaml +147 -0
  45. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/traces/blind-958b2f9e6816.yaml +147 -0
  46. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/adv-a0c9e2bfb0d6.yaml +24 -0
  47. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-3e892f3a89ee.yaml +23 -0
  48. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-a/blind-958b2f9e6816.yaml +25 -0
  49. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/adv-a0c9e2bfb0d6.yaml +31 -0
  50. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-3e892f3a89ee.yaml +32 -0
  51. package/calibration/ledger/runs/2026-04-09T10-02-57-143Z/verdicts-b/blind-958b2f9e6816.yaml +32 -0
  52. package/calibration/seeds/adversarial-generator.ts +159 -0
  53. package/calibration/seeds/blind-generator.ts +169 -0
  54. package/calibration/seeds/replay-loader.ts +117 -0
  55. package/calibration/skill/calibrate.ts +292 -0
  56. package/calibration/skill/cli-flags.ts +49 -0
  57. package/calibration/skill/report.ts +80 -0
  58. package/calibration/skill/review.ts +118 -0
  59. package/calibration/types.ts +292 -0
  60. package/package.json +46 -0
  61. package/src/commands/audit-codex.ts +266 -0
  62. package/src/commands/calibrate.ts +70 -0
  63. package/src/commands/compile.ts +117 -0
  64. package/src/commands/evaluate.ts +103 -0
  65. package/src/commands/ingest.ts +250 -0
  66. package/src/commands/init.ts +133 -0
  67. package/src/commands/packet.ts +408 -0
  68. package/src/commands/receipt.ts +305 -0
  69. package/src/commands/run-claude.ts +355 -0
  70. package/src/index.ts +43 -0
  71. package/src/lib/draft-return.ts +278 -0
  72. package/src/lib/drift-guard.ts +105 -0
  73. package/src/lib/errors.ts +61 -0
  74. package/src/lib/output.ts +43 -0
  75. package/src/lib/paths.ts +125 -0
  76. package/src/lib/proof.ts +262 -0
  77. package/src/lib/transport.ts +276 -0
  78. package/src/lib/yaml-io.ts +62 -0
  79. 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
+ }