litclaude-ai 0.3.43 → 0.3.44

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.44 - 2026-08-14 — provider-free activation speed guards
4
+
5
+ - Add provider-free activation and cache measurement contracts with deterministic
6
+ pack and portable-surface coverage.
7
+ - Serialize the aggregate test runner to avoid source-root pack/portable races.
8
+
3
9
  ## 0.3.43 - 2026-08-12 — advisory evidence review guidance
4
10
 
5
11
  - Add advisory evidence review guidance to the `frontend-ui-ux` skill.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
  <p align="center">
11
11
  <img src="https://img.shields.io/badge/npm-litclaude--ai-cb3837" />
12
- <img src="https://img.shields.io/badge/version-0.3.43-2ea44f" />
12
+ <img src="https://img.shields.io/badge/version-0.3.44-2ea44f" />
13
13
  <img src="https://img.shields.io/badge/Claude%20Code-plugin-blueviolet" />
14
14
  <img src="https://img.shields.io/badge/license-MIT-blue" />
15
15
  </p>
@@ -194,18 +194,18 @@ cd /tmp
194
194
  npx --yes litclaude-ai@latest install
195
195
  ```
196
196
 
197
- `0.3.43` is the version prepared by this checkout. Registry lookup is authoritative
197
+ `0.3.44` is the version prepared by this checkout. Registry lookup is authoritative
198
198
  for whether that exact version can be installed:
199
199
 
200
200
  ```bash
201
- npm view litclaude-ai@0.3.43 version
201
+ npm view litclaude-ai@0.3.44 version
202
202
  ```
203
203
 
204
- If that lookup returns 0.3.43, the exact install is available:
204
+ If that lookup returns 0.3.44, the exact install is available:
205
205
 
206
206
  ```bash
207
207
  cd /tmp
208
- npx --yes litclaude-ai@0.3.43 install
208
+ npx --yes litclaude-ai@0.3.44 install
209
209
  ```
210
210
 
211
211
  Otherwise, wait for explicit human publication and repeat the lookup before using
@@ -244,7 +244,7 @@ The installer also sets Claude Code's `statusLine` command to the packaged
244
244
  LitClaude HUD. A typical no-color render starts like:
245
245
 
246
246
  ```text
247
- [🔥LITCLAUDE v0.3.43] | O4.8 │ ctx [▎░░] 9%/1000k │ 5h [▏░] 4% ↻2h15m │ 1w [▊░] 35% ↻3d6h │ git main +3 ✓
247
+ [🔥LITCLAUDE v0.3.44] | O4.8 │ ctx [▎░░] 9%/1000k │ 5h [▏░] 4% ↻2h15m │ 1w [▊░] 35% ↻3d6h │ git main +3 ✓
248
248
  ```
249
249
 
250
250
  The `↻` suffix is a compact rate-limit reset countdown. It is separated from
package/README_ko-KR.md CHANGED
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
  <p align="center">
11
11
  <img src="https://img.shields.io/badge/npm-litclaude--ai-cb3837" />
12
- <img src="https://img.shields.io/badge/version-0.3.43-2ea44f" />
12
+ <img src="https://img.shields.io/badge/version-0.3.44-2ea44f" />
13
13
  <img src="https://img.shields.io/badge/Claude%20Code-plugin-blueviolet" />
14
14
  <img src="https://img.shields.io/badge/license-MIT-blue" />
15
15
  </p>
@@ -204,18 +204,18 @@ cd /tmp
204
204
  npx --yes litclaude-ai@latest install
205
205
  ```
206
206
 
207
- `0.3.43`은 이 checkout에서 준비 중인 version입니다. exact version의 설치 가능
207
+ `0.3.44`은 이 checkout에서 준비 중인 version입니다. exact version의 설치 가능
208
208
  여부에는 registry 조회 결과를 authoritative한 기준으로 사용합니다.
209
209
 
210
210
  ```bash
211
- npm view litclaude-ai@0.3.43 version
211
+ npm view litclaude-ai@0.3.44 version
212
212
  ```
213
213
 
214
- 조회 결과가 0.3.43이면 exact install을 사용할 수 있습니다.
214
+ 조회 결과가 0.3.44이면 exact install을 사용할 수 있습니다.
215
215
 
216
216
  ```bash
217
217
  cd /tmp
218
- npx --yes litclaude-ai@0.3.43 install
218
+ npx --yes litclaude-ai@0.3.44 install
219
219
  ```
220
220
 
221
221
  그렇지 않으면 명시적인 human publication을 기다립니다. 게시 후 조회를 다시 실행해
@@ -254,7 +254,7 @@ installer는 Claude Code의 `statusLine` command도 packaged LitClaude HUD로
254
254
  설정합니다. 색상을 제거한 예시는 다음처럼 시작합니다.
255
255
 
256
256
  ```text
257
- [🔥LITCLAUDE v0.3.43] | O4.8 │ ctx [▎░░] 9%/1000k │ 5h [▏░] 4% ↻2h15m │ 1w [▊░] 35% ↻3d6h │ git main +3 ✓
257
+ [🔥LITCLAUDE v0.3.44] | O4.8 │ ctx [▎░░] 9%/1000k │ 5h [▏░] 4% ↻2h15m │ 1w [▊░] 35% ↻3d6h │ git main +3 ✓
258
258
  ```
259
259
 
260
260
  `↻` 표시는 rate-limit reset까지 남은 시간을 짧게 보여주는 countdown입니다.
@@ -1,6 +1,6 @@
1
1
  # LitClaude Release Checklist
2
2
 
3
- Status: `litclaude-ai@0.3.43` is the current release candidate — exact canonical
3
+ Status: `litclaude-ai@0.3.44` is the current release candidate — exact canonical
4
4
  frontend corpus plus Claude-native `autoresearch`, `autoconference`, and `wikify`
5
5
  workflow-family integration. It byte-pins the frontend library, legal companions,
6
6
  family source closures, and adapters through independent commitments and package
@@ -45,9 +45,9 @@ side-effect-free, the launcher starts only a separate Claude Code
45
45
  print/background worker, and the release preserves the Korean polishing
46
46
  command, strict multi-agent review pipeline, fidelity guardrails, package
47
47
  hygiene checks, native route gates, and safe start-work handoff behavior.
48
- `package.json` is aligned to `0.3.43`,
49
- `plugins/litclaude/.claude-plugin/plugin.json` is aligned to `0.3.43`, and the
50
- plugin-local MCP server reports `0.3.43`.
48
+ `package.json` is aligned to `0.3.44`,
49
+ `plugins/litclaude/.claude-plugin/plugin.json` is aligned to `0.3.44`, and the
50
+ plugin-local MCP server reports `0.3.44`.
51
51
 
52
52
  This release carries the v0.2.2 Dynamic workflow hardening surfaces:
53
53
  `/dynamic-workflow`, `workflow-check --json`, native `/goal` fallback guidance,
@@ -280,9 +280,9 @@ checkout and from an isolated install of the packed tarball:
280
280
  Before requesting publication approval, confirm these artifacts from the current
281
281
  checkout:
282
282
 
283
- - `package.json` version is `0.3.43`.
284
- - `plugins/litclaude/.claude-plugin/plugin.json` version is `0.3.43`.
285
- - `plugins/litclaude/bin/litclaude-mcp.js` reports server version `0.3.43`.
283
+ - `package.json` version is `0.3.44`.
284
+ - `plugins/litclaude/.claude-plugin/plugin.json` version is `0.3.44`.
285
+ - `plugins/litclaude/bin/litclaude-mcp.js` reports server version `0.3.44`.
286
286
  - Prompt-hook tests cover bundled `SKILL.md` body injection for bare `hyperplan`, `litresearch`, `lit research`, `init-deep`, and explicit leading `$start-work`; diagnostic/copy mentions stay inert while leading natural-language `lit start work` stays BLOCKED.
287
287
  - `lit search` and `lit query` route to `/litclaude:litresearch` without activating on slash mentions, code spans, or non-lit prompts.
288
288
  - Litresearch web lanes require public API/feed preference, validator-first checks, route traces, prompt-injection quarantine, and honest auth/paywall/private-data stop reasons.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litclaude-ai",
3
- "version": "0.3.43",
3
+ "version": "0.3.44",
4
4
  "description": "Claude Code-native workflow distribution.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "scripts": {
31
31
  "postinstall": "node scripts/postinstall.mjs",
32
- "test": "node --test test/*.test.mjs",
32
+ "test": "node --test --test-concurrency=1 test/*.test.mjs",
33
33
  "validate:plugin": "node scripts/validate-plugin.mjs",
34
34
  "doctor": "node scripts/doctor.mjs",
35
35
  "qa:tmux": "bash scripts/qa-claude-plugin-smoke.sh",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "litclaude",
3
3
  "description": "Claude Code-native workflow plugin with a bounded-authority start-work lifecycle.",
4
- "version": "0.3.43",
4
+ "version": "0.3.44",
5
5
  "author": {
6
6
  "name": "LitClaude contributors"
7
7
  },
@@ -5,6 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { accentCodeForName, litBrandPrefix, supportsTruecolor } from "../lib/hud-accent-palette.mjs";
8
+ import { latestUsageTokens } from "../lib/cache-measurement.mjs";
8
9
 
9
10
  const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
10
11
  const pluginManifestPath = join(pluginRoot, ".claude-plugin", "plugin.json");
@@ -82,25 +83,6 @@ const makeContextBar = (value, width = 3) => {
82
83
  return makeBlockBar(pct, width, colors.accent);
83
84
  };
84
85
 
85
- const latestUsageTokens = (transcriptPath) => {
86
- if (!transcriptPath || !existsSync(transcriptPath)) return 0;
87
- const lines = readFileSync(transcriptPath, "utf8").split(/\r?\n/u).filter(Boolean);
88
- for (let index = lines.length - 1; index >= 0; index -= 1) {
89
- try {
90
- const event = JSON.parse(lines[index]);
91
- const usage = event?.message?.usage;
92
- if (!usage || event.isSidechain === true || event.isApiErrorMessage === true) continue;
93
- return (
94
- Number(usage.input_tokens ?? 0) +
95
- Number(usage.cache_read_input_tokens ?? 0) +
96
- Number(usage.cache_creation_input_tokens ?? 0)
97
- );
98
- } catch {
99
- }
100
- }
101
- return 0;
102
- };
103
-
104
86
  const latestUserMessage = (transcriptPath) => {
105
87
  if (!transcriptPath || !existsSync(transcriptPath)) return "";
106
88
  const lines = readFileSync(transcriptPath, "utf8").split(/\r?\n/u).filter(Boolean);
@@ -9,7 +9,7 @@ import {
9
9
  } from "../lib/wikify-knowledge.mjs";
10
10
 
11
11
  const protocolVersion = "2024-11-05";
12
- const serverVersion = "0.3.43";
12
+ const serverVersion = "0.3.44";
13
13
 
14
14
  const publicSourceReadTool = {
15
15
  name: "public_source_read",
@@ -0,0 +1,222 @@
1
+ // Local-only measurements for provider cache usage and rule-body deduplication.
2
+ //
3
+ // Provider cache percentages are valid only when a Claude transcript contains
4
+ // provider usage fields. This module never returns transcript text, rule bodies,
5
+ // file contents, secrets, or network metadata.
6
+
7
+ import { dirname } from "node:path";
8
+ import { MAX_BOUNDED_FILE_BYTES, readRegularStable } from "./secure-path-read.mjs";
9
+ import { dedupeByBody } from "./rules/format.mjs";
10
+
11
+ const MAX_TRANSCRIPT_BYTES = MAX_BOUNDED_FILE_BYTES;
12
+
13
+ const emptyTotals = () => ({
14
+ inputTokens: 0,
15
+ cacheReadInputTokens: 0,
16
+ cacheCreationInputTokens: 0,
17
+ outputTokens: 0,
18
+ observedInputTokens: 0,
19
+ });
20
+
21
+ const emptyProviderMeasurement = (reason) => ({
22
+ status: "UNPROVEN",
23
+ reason,
24
+ cacheHitRate: null,
25
+ cacheReadRate: null,
26
+ cacheReuseRate: null,
27
+ totals: emptyTotals(),
28
+ receipt: null,
29
+ });
30
+
31
+ const toToken = (value) => {
32
+ const number = typeof value === "number" ? value : Number(value);
33
+ return Number.isSafeInteger(number) && number >= 0 ? number : null;
34
+ };
35
+
36
+ const safeAdd = (left, right) => {
37
+ const total = left + right;
38
+ return Number.isSafeInteger(total) ? total : Number.MAX_SAFE_INTEGER;
39
+ };
40
+
41
+ const roundedPercent = (numerator, denominator) => {
42
+ if (denominator <= 0) return null;
43
+ return Math.round((numerator * 100 * 100) / denominator) / 100;
44
+ };
45
+
46
+ const parseUsageField = (usage, field) => {
47
+ if (!Object.hasOwn(usage, field)) return { present: false, valid: true, value: 0 };
48
+ const value = toToken(usage[field]);
49
+ return { present: true, valid: value !== null, value: value ?? 0 };
50
+ };
51
+
52
+ const isCountableUsageEvent = (event) =>
53
+ event?.isSidechain !== true
54
+ && event?.isApiErrorMessage !== true
55
+ && event?.message?.usage !== null
56
+ && typeof event?.message?.usage === "object"
57
+ && !Array.isArray(event.message.usage);
58
+
59
+ const forEachUsageEvent = (transcriptText, callback) => {
60
+ if (typeof transcriptText !== "string") return;
61
+ for (const line of transcriptText.split(/\r?\n/u)) {
62
+ if (line.trim().length === 0) continue;
63
+ try {
64
+ const event = JSON.parse(line);
65
+ if (isCountableUsageEvent(event)) callback(event.message.usage);
66
+ } catch {
67
+ // A partial or unrelated JSONL line cannot establish a provider receipt.
68
+ }
69
+ }
70
+ };
71
+
72
+ const measureUsageText = (transcriptText) => {
73
+ const totals = emptyTotals();
74
+ let usageEvents = 0;
75
+ let cacheUsageEvents = 0;
76
+ let cacheFieldsSeen = false;
77
+ let invalidFieldCount = 0;
78
+
79
+ forEachUsageEvent(transcriptText, (usage) => {
80
+ usageEvents += 1;
81
+ const input = parseUsageField(usage, "input_tokens");
82
+ const cacheRead = parseUsageField(usage, "cache_read_input_tokens");
83
+ const cacheCreation = parseUsageField(usage, "cache_creation_input_tokens");
84
+ const output = parseUsageField(usage, "output_tokens");
85
+ const fields = [input, cacheRead, cacheCreation, output];
86
+
87
+ if (cacheRead.present || cacheCreation.present) {
88
+ cacheFieldsSeen = true;
89
+ cacheUsageEvents += 1;
90
+ }
91
+ invalidFieldCount += fields.filter((field) => !field.valid).length;
92
+ totals.inputTokens = safeAdd(totals.inputTokens, input.value);
93
+ totals.cacheReadInputTokens = safeAdd(totals.cacheReadInputTokens, cacheRead.value);
94
+ totals.cacheCreationInputTokens = safeAdd(totals.cacheCreationInputTokens, cacheCreation.value);
95
+ totals.outputTokens = safeAdd(totals.outputTokens, output.value);
96
+ });
97
+
98
+ totals.observedInputTokens = safeAdd(
99
+ safeAdd(totals.inputTokens, totals.cacheReadInputTokens),
100
+ totals.cacheCreationInputTokens,
101
+ );
102
+
103
+ const receipt = usageEvents > 0
104
+ ? { kind: "local-transcript-usage", usageEvents, cacheUsageEvents }
105
+ : null;
106
+ if (usageEvents === 0) return emptyProviderMeasurement("no-provider-usage-receipt");
107
+ if (invalidFieldCount > 0) {
108
+ return {
109
+ ...emptyProviderMeasurement("invalid-provider-usage-fields"),
110
+ totals,
111
+ receipt,
112
+ };
113
+ }
114
+ if (!cacheFieldsSeen) {
115
+ return {
116
+ ...emptyProviderMeasurement("cache-fields-not-exposed"),
117
+ totals,
118
+ receipt,
119
+ };
120
+ }
121
+ if (totals.observedInputTokens <= 0) {
122
+ return {
123
+ ...emptyProviderMeasurement("zero-observed-input"),
124
+ totals,
125
+ receipt,
126
+ };
127
+ }
128
+
129
+ // `cacheReadRate` is read tokens over all observed input tokens. The narrower
130
+ // `cacheReuseRate` excludes uncached input and compares reads with cache writes.
131
+ const cacheableInput = safeAdd(totals.cacheReadInputTokens, totals.cacheCreationInputTokens);
132
+ return {
133
+ status: "MEASURED",
134
+ reason: null,
135
+ cacheHitRate: roundedPercent(totals.cacheReadInputTokens, totals.observedInputTokens),
136
+ cacheReadRate: roundedPercent(totals.cacheReadInputTokens, totals.observedInputTokens),
137
+ cacheReuseRate: roundedPercent(totals.cacheReadInputTokens, cacheableInput),
138
+ totals,
139
+ receipt,
140
+ };
141
+ };
142
+
143
+ /**
144
+ * Measure cache reads from an in-memory transcript fixture.
145
+ * The returned receipt contains counts only; no transcript fields are returned.
146
+ */
147
+ export const measureProviderCacheFromText = (transcriptText) => measureUsageText(transcriptText);
148
+
149
+ /**
150
+ * Read one regular, stable, bounded local transcript and measure its usage fields.
151
+ * Symlinks, oversized files, and changing files are reported as UNPROVEN.
152
+ */
153
+ export const measureProviderCache = (transcriptPath) => {
154
+ if (typeof transcriptPath !== "string" || transcriptPath.length === 0) {
155
+ return emptyProviderMeasurement("transcript-path-not-provided");
156
+ }
157
+ try {
158
+ const result = readRegularStable(dirname(transcriptPath), transcriptPath, undefined, { maxBytes: MAX_TRANSCRIPT_BYTES });
159
+ if (result.failure) {
160
+ return emptyProviderMeasurement(`transcript-${result.failure.toLowerCase().replaceAll("_", "-")}`);
161
+ }
162
+ return measureUsageText(result.bytes.toString("utf8"));
163
+ } catch {
164
+ return emptyProviderMeasurement("transcript-read-failed");
165
+ }
166
+ };
167
+
168
+ /** Match HUD context sizing: latest non-sidechain provider usage event only. */
169
+ export const latestUsageTokensFromText = (transcriptText) => {
170
+ let latest = null;
171
+ forEachUsageEvent(transcriptText, (usage) => {
172
+ const input = parseUsageField(usage, "input_tokens").value;
173
+ const cacheRead = parseUsageField(usage, "cache_read_input_tokens").value;
174
+ const cacheCreation = parseUsageField(usage, "cache_creation_input_tokens").value;
175
+ latest = safeAdd(safeAdd(input, cacheRead), cacheCreation);
176
+ });
177
+ return latest ?? 0;
178
+ };
179
+
180
+ /** Match HUD context sizing while applying the same bounded local-file policy. */
181
+ export const latestUsageTokens = (transcriptPath) => {
182
+ if (typeof transcriptPath !== "string" || transcriptPath.length === 0) return 0;
183
+ try {
184
+ const result = readRegularStable(dirname(transcriptPath), transcriptPath, undefined, { maxBytes: MAX_TRANSCRIPT_BYTES });
185
+ return result.failure ? 0 : latestUsageTokensFromText(result.bytes.toString("utf8"));
186
+ } catch {
187
+ return 0;
188
+ }
189
+ };
190
+
191
+ const normalizeRuleBody = (body) => body.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n").trim();
192
+
193
+ /**
194
+ * Measure the existing rules-engine body deduplication without returning rule text.
195
+ * The denominator is usable rule bodies; empty/malformed bodies stay outside it.
196
+ */
197
+ export const measureRuleDedup = (rules) => {
198
+ if (!Array.isArray(rules)) {
199
+ return {
200
+ status: "UNPROVEN",
201
+ reason: "rules-not-provided",
202
+ candidateRuleCount: 0,
203
+ usableRuleCount: 0,
204
+ uniqueRuleCount: 0,
205
+ duplicateRuleCount: 0,
206
+ dedupRate: null,
207
+ };
208
+ }
209
+
210
+ const usableRules = rules.filter((rule) => typeof rule?.body === "string" && normalizeRuleBody(rule.body).length > 0);
211
+ const uniqueRuleCount = dedupeByBody(usableRules).length;
212
+ const duplicateRuleCount = usableRules.length - uniqueRuleCount;
213
+ return {
214
+ status: usableRules.length > 0 ? "MEASURED" : "UNPROVEN",
215
+ reason: usableRules.length > 0 ? null : "no-usable-rules",
216
+ candidateRuleCount: rules.length,
217
+ usableRuleCount: usableRules.length,
218
+ uniqueRuleCount,
219
+ duplicateRuleCount,
220
+ dedupRate: roundedPercent(duplicateRuleCount, usableRules.length),
221
+ };
222
+ };
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import {
7
+ ContractError,
8
+ PHASES,
9
+ SCENARIO_ID,
10
+ UNAVAILABLE,
11
+ VALIDATION_SCHEMA,
12
+ fail,
13
+ isObject,
14
+ parseJson,
15
+ validateScenarioText,
16
+ } from "./harness-speed-scenario.mjs";
17
+
18
+ const SCHEMA = "litfamily.harness-speed/v1";
19
+ const ARMS = ["baseline", "candidate", "control"];
20
+ const REQUIRED = [
21
+ "schema", "scenario_id", "product", "arm", "block", "order", "sequence", "phase",
22
+ "prompt_bytes", "prompt_sha256", "response_bytes", "response_sha256", "sentinel_match",
23
+ "route_observed", "output_policy_match", "correct", "failure_code", "timed_out",
24
+ "start_offset_ms", "first_content_offset_ms", "final_receipt_offset_ms", "exit_offset_ms",
25
+ "input_tokens", "cache_read_tokens", "cache_write_tokens", "output_tokens",
26
+ ];
27
+ const FORBIDDEN = new Set([
28
+ "prompt", "response", "transcript", "url", "requestid", "threadid", "sessionid",
29
+ "credential", "authorization", "cookie", "apikey", "rawprompt", "prompttext",
30
+ "rawresponse", "responsetext",
31
+ ]);
32
+
33
+ const isSafeCounter = (value) => value === UNAVAILABLE || Number.isSafeInteger(value) && value >= 0;
34
+ const isTime = (value) => Number.isFinite(value) && value >= 0;
35
+ const phaseContract = (id) => PHASES.find(([phase]) => phase === id);
36
+
37
+ function assertPrivate(value) {
38
+ if (Array.isArray(value)) {
39
+ for (const item of value) assertPrivate(item);
40
+ return;
41
+ }
42
+ if (!isObject(value)) return;
43
+ for (const [key, child] of Object.entries(value)) {
44
+ if (FORBIDDEN.has(key.toLowerCase().replaceAll(/[^a-z0-9]/gu, ""))) fail("FORBIDDEN_FIELD");
45
+ assertPrivate(child);
46
+ }
47
+ }
48
+
49
+ function validateRecord(record, index) {
50
+ if (!isObject(record)) fail("INVALID_RECORD");
51
+ assertPrivate(record);
52
+ if (Object.keys(record).length !== REQUIRED.length || REQUIRED.some((key) => !Object.hasOwn(record, key))) {
53
+ fail("INVALID_RECORD_SHAPE");
54
+ }
55
+ const expected = phaseContract(record.phase);
56
+ if (record.schema !== SCHEMA || record.scenario_id !== SCENARIO_ID || record.product !== "litclaude"
57
+ || !ARMS.includes(record.arm) || !Number.isInteger(record.block) || record.block < 1 || record.block > 6
58
+ || !Number.isInteger(record.order) || record.order < 1 || record.order > 3
59
+ || record.sequence !== index + 1 || !expected
60
+ || record.prompt_bytes !== expected[1] || record.prompt_sha256 !== expected[2]
61
+ || !Number.isSafeInteger(record.response_bytes) || record.response_bytes < 0
62
+ || !/^[a-f0-9]{64}$/u.test(record.response_sha256)) fail("INVALID_RECORD");
63
+ for (const key of ["sentinel_match", "route_observed", "output_policy_match", "correct", "timed_out"]) {
64
+ if (typeof record[key] !== "boolean") fail("INVALID_RECORD");
65
+ }
66
+ if (record.failure_code !== null && (typeof record.failure_code !== "string"
67
+ || !/^[A-Z][A-Z0-9_]{0,63}$/u.test(record.failure_code))) {
68
+ fail("INVALID_FAILURE_CODE");
69
+ }
70
+ for (const key of ["input_tokens", "cache_read_tokens", "cache_write_tokens", "output_tokens"]) {
71
+ if (!isSafeCounter(record[key])) fail("INVALID_COUNTER");
72
+ }
73
+ if (!isTime(record.start_offset_ms) || !isTime(record.final_receipt_offset_ms)
74
+ || !isTime(record.exit_offset_ms) || record.start_offset_ms > record.final_receipt_offset_ms
75
+ || record.final_receipt_offset_ms > record.exit_offset_ms) fail("INVALID_TIME");
76
+ if (record.first_content_offset_ms !== UNAVAILABLE
77
+ && (!isTime(record.first_content_offset_ms) || record.first_content_offset_ms < record.start_offset_ms
78
+ || record.first_content_offset_ms > record.final_receipt_offset_ms)) fail("INVALID_TIME");
79
+ if (record.sentinel_match
80
+ && (record.response_bytes !== expected[3] || record.response_sha256 !== expected[4])) fail("INVALID_RESPONSE_HASH");
81
+ const successObserved = record.sentinel_match && record.route_observed && record.output_policy_match
82
+ && record.timed_out === false;
83
+ if ((record.failure_code === null) !== successObserved) fail("INCONSISTENT_FAILURE_CODE");
84
+ const derivedCorrect = successObserved && record.failure_code === null;
85
+ if (record.correct !== derivedCorrect) fail("FALSE_CORRECTNESS_CLAIM");
86
+ }
87
+
88
+ function validateShape(records) {
89
+ if (!Array.isArray(records) || records.length !== 54) fail("INVALID_COHORT_SIZE");
90
+ records.forEach(validateRecord);
91
+ const permutations = [];
92
+ let cursor = 0;
93
+ for (let block = 1; block <= 6; block += 1) {
94
+ const arms = [];
95
+ for (let order = 1; order <= 3; order += 1) {
96
+ const session = records.slice(cursor, cursor + 3);
97
+ const arm = session[0].arm;
98
+ if (session.some((record, phaseIndex) => record.block !== block || record.order !== order
99
+ || record.arm !== arm || record.phase !== PHASES[phaseIndex][0])) fail("INVALID_SESSION_ORDER");
100
+ arms.push(arm);
101
+ cursor += 3;
102
+ }
103
+ if (new Set(arms).size !== 3) fail("MISSING_ARM");
104
+ permutations.push(arms.join(","));
105
+ }
106
+ if (new Set(permutations).size !== 6) fail("DUPLICATE_ARM_PERMUTATION");
107
+ for (const arm of ARMS) {
108
+ const armRecords = records.filter((record) => record.arm === arm);
109
+ if (armRecords.filter((record) => record.phase === "B0").length !== 6
110
+ || armRecords.filter((record) => record.phase !== "B0").length !== 12) fail("INVALID_ARM_COUNT");
111
+ }
112
+ }
113
+
114
+ function nearestRank(values, percentile) {
115
+ const sorted = [...values].sort((left, right) => left - right);
116
+ return sorted[Math.max(0, Math.ceil(percentile * sorted.length) - 1)];
117
+ }
118
+
119
+ function diagnostic(records) {
120
+ const inputs = records.map(({ input_tokens: value }) => value);
121
+ const cacheReads = records.map(({ cache_read_tokens: value }) => value);
122
+ const numeric = inputs.every(Number.isSafeInteger) && cacheReads.every(Number.isSafeInteger);
123
+ const inputSum = numeric ? inputs.reduce((sum, value) => sum + value, 0) : 0;
124
+ const compatible = numeric && records.every((record) => record.input_tokens >= record.cache_read_tokens);
125
+ const correctTurns = records.filter(({ correct }) => correct).length;
126
+ return {
127
+ cache_read_share: numeric && inputSum > 0
128
+ ? cacheReads.reduce((sum, value) => sum + value, 0) / inputSum : UNAVAILABLE,
129
+ uncached_input_per_correct_turn: compatible && correctTurns > 0
130
+ ? records.reduce((sum, record) => sum + record.input_tokens - record.cache_read_tokens, 0) / correctTurns
131
+ : UNAVAILABLE,
132
+ };
133
+ }
134
+
135
+ export function validateCohort(value) {
136
+ if (!isObject(value)) fail("INVALID_COHORT");
137
+ assertPrivate(value);
138
+ if (Object.keys(value).length !== 4 || value.schema !== SCHEMA || value.scenario_id !== SCENARIO_ID
139
+ || value.product !== "litclaude") fail("INVALID_COHORT");
140
+ validateShape(value.records);
141
+ const measured = value.records.filter(({ phase }) => phase !== "B0");
142
+ const byArm = Object.fromEntries(ARMS.map((arm) => [arm, measured.filter((record) => record.arm === arm)]));
143
+ const latencies = Object.fromEntries(ARMS.map((arm) => [arm,
144
+ byArm[arm].map((record) => record.exit_offset_ms - record.start_offset_ms)]));
145
+ const latencyStats = Object.fromEntries(ARMS.map((arm) => [arm, {
146
+ p50: nearestRank(latencies[arm], 0.5), p95: nearestRank(latencies[arm], 0.95),
147
+ }]));
148
+ const ratios = byArm.candidate.map((candidate) => {
149
+ const baseline = byArm.baseline.find(({ block, phase }) => block === candidate.block && phase === candidate.phase);
150
+ return (candidate.exit_offset_ms - candidate.start_offset_ms)
151
+ / (baseline.exit_offset_ms - baseline.start_offset_ms);
152
+ });
153
+ const controlFirst = latencies.control.slice(0, 6);
154
+ const controlLast = latencies.control.slice(6);
155
+ const controlFirstP95 = nearestRank(controlFirst, 0.95);
156
+ const controlLastP95 = nearestRank(controlLast, 0.95);
157
+ const controlDrift = controlFirstP95 > 0
158
+ ? Math.abs(controlLastP95 - controlFirstP95) / controlFirstP95 : UNAVAILABLE;
159
+ const numericInputs = [...byArm.baseline, ...byArm.candidate]
160
+ .every(({ input_tokens: value }) => Number.isSafeInteger(value));
161
+ const inputTotals = numericInputs ? {
162
+ baseline: byArm.baseline.reduce((sum, record) => sum + record.input_tokens, 0),
163
+ candidate: byArm.candidate.reduce((sum, record) => sum + record.input_tokens, 0),
164
+ } : { baseline: UNAVAILABLE, candidate: UNAVAILABLE };
165
+ const pairedMedianRatio = nearestRank(ratios, 0.5);
166
+ const gates = {
167
+ correctness: ARMS.every((arm) => value.records.filter((record) => record.arm === arm)
168
+ .every(({ correct, failure_code: failureCode, timed_out: timedOut }) => correct && failureCode === null && !timedOut)),
169
+ candidate_p95: latencyStats.candidate.p95 <= latencyStats.baseline.p95 * 0.85,
170
+ candidate_p50: latencyStats.candidate.p50 <= latencyStats.baseline.p50,
171
+ paired_ratio: pairedMedianRatio <= 0.85,
172
+ control_drift: controlDrift !== UNAVAILABLE && controlDrift <= 0.2,
173
+ anti_padding: numericInputs && inputTotals.candidate <= inputTotals.baseline,
174
+ };
175
+ const candidateCache = byArm.candidate.map(({ cache_read_tokens: value }) => value);
176
+ const compatibleCache = [...byArm.baseline, ...byArm.candidate]
177
+ .every((record) => Number.isSafeInteger(record.input_tokens)
178
+ && Number.isSafeInteger(record.cache_read_tokens) && record.input_tokens >= record.cache_read_tokens);
179
+ const candidateCacheSum = candidateCache.every(Number.isSafeInteger)
180
+ ? candidateCache.reduce((sum, value) => sum + value, 0) : 0;
181
+ const latencySaved = compatibleCache && candidateCacheSum > 0
182
+ ? (latencies.baseline.reduce((sum, value) => sum + value, 0)
183
+ - latencies.candidate.reduce((sum, value) => sum + value, 0)) / (candidateCacheSum / 1000)
184
+ : UNAVAILABLE;
185
+ const verdict = !gates.control_drift ? "BLOCKED_ENVIRONMENT_DRIFT"
186
+ : Object.values(gates).every(Boolean) ? "PASS" : "FAIL";
187
+ return {
188
+ schema: VALIDATION_SCHEMA,
189
+ scenario_id: SCENARIO_ID,
190
+ product: "litclaude",
191
+ verdict,
192
+ record_count: value.records.length,
193
+ latency_ms: latencyStats,
194
+ paired_median_ratio: pairedMedianRatio,
195
+ control_drift_ratio: controlDrift,
196
+ input_token_totals: inputTotals,
197
+ gates,
198
+ diagnostics: {
199
+ baseline: diagnostic(byArm.baseline),
200
+ candidate: diagnostic(byArm.candidate),
201
+ control: diagnostic(byArm.control),
202
+ latency_saved_ms_per_1k_cache_read_tokens: latencySaved,
203
+ },
204
+ provider_calls: 0,
205
+ };
206
+ }
207
+
208
+ function main() {
209
+ const [mode, inputPath] = process.argv.slice(2);
210
+ if (!inputPath || !["scenario", "cohort"].includes(mode)) fail("USAGE");
211
+ const text = readFileSync(inputPath, "utf8");
212
+ const receipt = mode === "scenario" ? validateScenarioText(text) : validateCohort(parseJson(text));
213
+ process.stdout.write(`${JSON.stringify(receipt)}\n`);
214
+ process.exitCode = receipt.verdict === "PASS" ? 0 : 1;
215
+ }
216
+
217
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
218
+ try {
219
+ main();
220
+ } catch (error) {
221
+ const code = error instanceof ContractError ? error.code : "INPUT_READ_FAILED";
222
+ process.stdout.write(`${JSON.stringify({ schema: VALIDATION_SCHEMA, verdict: "INVALID", error_code: code, provider_calls: 0 })}\n`);
223
+ process.exitCode = code === "USAGE" ? 64 : 65;
224
+ }
225
+ }
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export const SCENARIO_ID = "litfamily-speed-lit-activation-v1";
4
+ export const VALIDATION_SCHEMA = "litfamily.harness-speed-validation/v1";
5
+ export const UNAVAILABLE = "UNAVAILABLE";
6
+ export const PHASES = [
7
+ ["B0", 69, "a5f634868cc6e20375f60803dcbe850eeb3d2709d6bb2c0efee204b9b5534904", 15, "7652e386a8825b8459833ec8aa7d19fa6a55bf82652f63f45cc4ae29bcfee17b"],
8
+ ["S1", 73, "a52e0ec9444cc2195c1c8dbb638862251f1f07df0512c505d06b43d44d4a4e78", 15, "8a655cd5ccd93b2727d641b5ca444e0f72bc09016115b7b6b16aa29e8457414a"],
9
+ ["S2", 99, "7b6712f5542798968a74a56241689f40475d50851d5a4d80f877d9a2589983a0", 15, "482d6b2f6dc3a90d5feb4aef7615f715ef2ffbceea5e5ffde90f070175c22dc8"],
10
+ ];
11
+
12
+ export class ContractError extends Error {
13
+ constructor(code) {
14
+ super(code);
15
+ this.code = code;
16
+ }
17
+ }
18
+
19
+ export const fail = (code) => { throw new ContractError(code); };
20
+ export const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
21
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
22
+
23
+ export function parseJson(text) {
24
+ try {
25
+ return JSON.parse(text);
26
+ } catch {
27
+ return fail("MALFORMED_JSON");
28
+ }
29
+ }
30
+
31
+ export function validateScenarioText(text) {
32
+ const value = parseJson(text);
33
+ if (sha256(text) !== "aaef5ba778532013248f0b5c0a9f958468786840595e48cb84851ab88bf2b4be"
34
+ || Buffer.byteLength(text, "utf8") !== 1476) fail("STALE_SCENARIO");
35
+ if (!isObject(value) || value.schema !== "litfamily.harness-speed-scenario/v1"
36
+ || value.scenario_id !== SCENARIO_ID || value.encoding !== "UTF-8"
37
+ || value.prompt_trailing_newline !== false || !Array.isArray(value.records)
38
+ || value.records.length !== PHASES.length) fail("INVALID_SCENARIO");
39
+ for (const [index, record] of value.records.entries()) {
40
+ const expected = PHASES[index];
41
+ if (!isObject(record) || record.id !== expected[0] || record.measured !== (index > 0)
42
+ || Buffer.byteLength(record.prompt, "utf8") !== expected[1] || record.prompt_bytes !== expected[1]
43
+ || sha256(record.prompt) !== expected[2] || record.prompt_sha256 !== expected[2]
44
+ || Buffer.byteLength(record.sentinel, "utf8") !== expected[3] || record.sentinel_bytes !== expected[3]
45
+ || sha256(record.sentinel) !== expected[4] || record.sentinel_sha256 !== expected[4]) fail("INVALID_SCENARIO");
46
+ }
47
+ return {
48
+ schema: VALIDATION_SCHEMA,
49
+ verdict: "PASS",
50
+ scenario_id: SCENARIO_ID,
51
+ encoding: "UTF-8",
52
+ fixture_bytes: Buffer.byteLength(text, "utf8"),
53
+ fixture_sha256: "aaef5ba778532013248f0b5c0a9f958468786840595e48cb84851ab88bf2b4be",
54
+ record_ids: PHASES.map(([id]) => id),
55
+ prompt_sha256: Object.fromEntries(PHASES.map(([id, , hash]) => [id, hash])),
56
+ provider_calls: 0,
57
+ };
58
+ }
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ import { ContractError, SCENARIO_ID, fail, isObject, parseJson } from "./harness-speed-scenario.mjs";
7
+
8
+ const SCHEMA = "litfamily.harness-speed-v2/v2";
9
+ const RECEIPT_SCHEMA = "litfamily.harness-speed-v2-validation/v2";
10
+ const FIXTURE_SHA = "aaef5ba778532013248f0b5c0a9f958468786840595e48cb84851ab88bf2b4be";
11
+ const FROZEN_BASELINE = Object.freeze({
12
+ role: "frozen_pre_v2",
13
+ head: "85c205e3e4ae5f5c7820545a1f78eff78fac93c9",
14
+ status_sha256: "c199ad8baf5d7d2b157a3279e0f17efe98758e5cdfacc0a1501650ae1490022f",
15
+ source_artifact_sha256: "74980bdc66c40eab8b471d0d03af3759dee1b9624bfd47476bf8d0875d66e0af",
16
+ artifact_sha256: "9cee3b98b0311bfae8a930a6d7ca18e0222f17943e80d469211545421bc01759",
17
+ fixture_sha256: FIXTURE_SHA,
18
+ });
19
+ const PHASES = Object.freeze(["session_start", "s1_activation", "s2_continuation", "no_route_floor"]);
20
+ const ARMS = Object.freeze(["baseline", "candidate"]);
21
+ const BASELINE_IDENTITY_KEYS = Object.freeze(["role", "head", "status_sha256", "source_artifact_sha256", "artifact_sha256", "fixture_sha256"]);
22
+ const CANDIDATE_IDENTITY_KEYS = Object.freeze([...BASELINE_IDENTITY_KEYS, "base_source_artifact_sha256"]);
23
+ const RECORD_KEYS = Object.freeze([
24
+ "block", "arm", "phase", "sample", "start_ns", "end_ns", "context_bytes", "correct",
25
+ "route_match", "banner_match", "body_match", "payload_match", "explicit_guard_applicable",
26
+ "explicit_guard_passed", "compaction_guard_applicable", "compaction_guard_passed",
27
+ ]);
28
+ const FORBIDDEN = new Set(["body", "prompt", "rawprompt", "prompttext", "promise", "unknown", "verdict", "result", "response", "transcript"]);
29
+ const SHA = /^[a-f0-9]{64}$/u;
30
+ const NS = /^(0|[1-9][0-9]{0,23})$/u;
31
+
32
+ const sameKeys = (value, keys) => isObject(value) && Object.keys(value).length === keys.length
33
+ && keys.every((key) => Object.hasOwn(value, key));
34
+ const normalizeKey = (key) => key.toLowerCase().replaceAll(/[^a-z0-9]/gu, "");
35
+
36
+ function assertNoForbiddenFields(value) {
37
+ if (Array.isArray(value)) return value.forEach(assertNoForbiddenFields);
38
+ if (!isObject(value)) return;
39
+ for (const [key, child] of Object.entries(value)) {
40
+ if (FORBIDDEN.has(normalizeKey(key))) fail("FORBIDDEN_FIELD");
41
+ assertNoForbiddenFields(child);
42
+ }
43
+ }
44
+
45
+ function validateIdentity(identity, expectedRole, keys) {
46
+ if (!sameKeys(identity, keys) || identity.role !== expectedRole || !/^[a-f0-9]{40}$/u.test(identity.head)
47
+ || !SHA.test(identity.status_sha256) || !SHA.test(identity.source_artifact_sha256) || !SHA.test(identity.artifact_sha256)
48
+ || identity.fixture_sha256 !== FIXTURE_SHA) fail("FIXTURE_IDENTITY_MISMATCH");
49
+ }
50
+
51
+ function validateIdentities(value) {
52
+ if (!sameKeys(value, ARMS)) fail("INVALID_IDENTITIES");
53
+ validateIdentity(value.baseline, "frozen_pre_v2", BASELINE_IDENTITY_KEYS);
54
+ if (!isObject(value.candidate) || value.candidate.base_source_artifact_sha256 !== FROZEN_BASELINE.source_artifact_sha256) {
55
+ fail("BASE_SOURCE_IDENTITY_MISMATCH");
56
+ }
57
+ if (!SHA.test(value.candidate.artifact_sha256 ?? "") || /^0+$/u.test(value.candidate.artifact_sha256)
58
+ || value.candidate.artifact_sha256 === FROZEN_BASELINE.artifact_sha256) fail("CANDIDATE_ARTIFACT_MISMATCH");
59
+ validateIdentity(value.candidate, "v2_candidate", CANDIDATE_IDENTITY_KEYS);
60
+ if (BASELINE_IDENTITY_KEYS.some((key) => value.baseline[key] !== FROZEN_BASELINE[key])) fail("BASELINE_IDENTITY_MISMATCH");
61
+ if (value.candidate.head !== FROZEN_BASELINE.head || value.candidate.status_sha256 === FROZEN_BASELINE.status_sha256
62
+ || value.candidate.source_artifact_sha256 === FROZEN_BASELINE.source_artifact_sha256
63
+ || /^0+$/u.test(value.candidate.status_sha256) || /^0+$/u.test(value.candidate.source_artifact_sha256)) fail("CANDIDATE_IDENTITY_MISMATCH");
64
+ }
65
+
66
+ function validateSelection(value) {
67
+ if (sameKeys(value, ["kind", "phase"]) && value.kind === "latency" && PHASES.includes(value.phase)) return value;
68
+ if (sameKeys(value, ["kind"]) && value.kind === "context") return value;
69
+ fail("INVALID_SELECTION");
70
+ }
71
+
72
+ function validateProvider(value) {
73
+ const keys = ["mode", "calls", "completions", "diagnostics"];
74
+ const diagnosticKeys = ["input_tokens", "output_tokens", "cache_read_tokens", "cache_write_tokens"];
75
+ if (!sameKeys(value, keys) || value.mode !== "provider_free" || value.calls !== 0 || value.completions !== 0
76
+ || !sameKeys(value.diagnostics, diagnosticKeys)) fail("INVALID_PROVIDER_PROOF");
77
+ for (const diagnostic of Object.values(value.diagnostics)) {
78
+ if (!sameKeys(diagnostic, ["status", "reason"]) || diagnostic.status !== "UNAVAILABLE"
79
+ || diagnostic.reason !== "PROVIDER_FREE") fail("INVALID_PROVIDER_PROOF");
80
+ }
81
+ }
82
+
83
+ function validateRecord(record, expected) {
84
+ if (!sameKeys(record, RECORD_KEYS) || record.block !== expected.block || record.arm !== expected.arm
85
+ || record.phase !== expected.phase || record.sample !== expected.sample || !NS.test(record.start_ns)
86
+ || !NS.test(record.end_ns) || BigInt(record.end_ns) <= BigInt(record.start_ns)
87
+ || !Number.isSafeInteger(record.context_bytes) || record.context_bytes <= 0) fail("INVALID_SAMPLE_MATRIX");
88
+ for (const key of RECORD_KEYS.slice(7)) if (typeof record[key] !== "boolean") fail("INVALID_SAMPLE_MATRIX");
89
+ }
90
+
91
+ function validateRecords(records) {
92
+ if (!Array.isArray(records) || records.length !== 240) fail("INVALID_SAMPLE_MATRIX");
93
+ let cursor = 0;
94
+ for (let block = 1; block <= 6; block += 1) {
95
+ const arms = block % 2 === 1 ? ARMS : [...ARMS].reverse();
96
+ for (const arm of arms) for (const phase of PHASES) for (let sample = 1; sample <= 5; sample += 1) {
97
+ validateRecord(records[cursor], { block, arm, phase, sample });
98
+ cursor += 1;
99
+ }
100
+ }
101
+ }
102
+
103
+ const duration = (record) => BigInt(record.end_ns) - BigInt(record.start_ns);
104
+ const nearestRank = (values, percentile) => [...values].sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
105
+ [Math.max(0, Math.ceil(percentile * values.length) - 1)];
106
+ const stats = (records) => ({ p50: nearestRank(records.map(duration), 0.5), p95: nearestRank(records.map(duration), 0.95) });
107
+ const stringifyStats = ({ p50, p95 }) => ({ p50: String(p50), p95: String(p95) });
108
+
109
+ function validateContextProof(proof, required) {
110
+ if (!required && proof === null) return;
111
+ if (!sameKeys(proof, ["status", "host", "surface", "mechanism", "deterministic", "body_sha256", "loaded_sha256"])
112
+ || proof.status !== "OBSERVED" || proof.host !== "claude-code" || proof.surface !== "PreRequestContextLoad"
113
+ || proof.mechanism !== "host_native_pre_request" || proof.deterministic !== true
114
+ || !SHA.test(proof.body_sha256) || /^0+$/u.test(proof.body_sha256)
115
+ || !SHA.test(proof.loaded_sha256) || /^0+$/u.test(proof.loaded_sha256)) {
116
+ fail("INVALID_CONTEXT_PROOF");
117
+ }
118
+ if (proof.body_sha256 !== proof.loaded_sha256) fail("CONTEXT_HASH_MISMATCH");
119
+ }
120
+
121
+ export function validateV2Cohort(value) {
122
+ assertNoForbiddenFields(value);
123
+ if (!isObject(value) || !Object.hasOwn(value, "selected")) fail("INVALID_SELECTION");
124
+ const selected = validateSelection(value.selected);
125
+ const keys = ["schema", "scenario_id", "product", "measurement", "clock", "identities", "provider", "selected", "context_loader_proof", "records"];
126
+ if (!sameKeys(value, keys) || value.schema !== SCHEMA || value.scenario_id !== SCENARIO_ID || value.product !== "litclaude"
127
+ || value.measurement !== "provider_free_installed_hook_comparison" || value.clock !== "process.hrtime.bigint") fail("INVALID_V2_COHORT");
128
+ validateIdentities(value.identities);
129
+ validateProvider(value.provider);
130
+ validateRecords(value.records);
131
+ const byArm = Object.fromEntries(ARMS.map((arm) => [arm, value.records.filter((record) => record.arm === arm)]));
132
+ const aggregate = Object.fromEntries(ARMS.map((arm) => [arm, stats(byArm[arm])]));
133
+ const phaseStats = Object.fromEntries(PHASES.map((phase) => [phase, Object.fromEntries(ARMS.map((arm) => [arm,
134
+ stats(byArm[arm].filter((record) => record.phase === phase))]))]));
135
+ const early = stats(byArm.baseline.filter(({ block }) => block <= 3)).p95;
136
+ const late = stats(byArm.baseline.filter(({ block }) => block >= 4)).p95;
137
+ const driftDelta = early > late ? early - late : late - early;
138
+ const context = Object.fromEntries(ARMS.map((arm) => [arm, byArm[arm].reduce((sum, record) => sum + record.context_bytes, 0)]));
139
+ const contextRemoved = context.candidate < context.baseline;
140
+ validateContextProof(value.context_loader_proof, contextRemoved || selected.kind === "context");
141
+ const guardCoverage = ["explicit_guard", "compaction_guard"].every((guard) => value.records.some((record) => record[`${guard}_applicable`]));
142
+ const correctness = guardCoverage && value.records.every((record) => record.correct && record.route_match && record.banner_match
143
+ && record.body_match && record.payload_match && (!record.explicit_guard_applicable || record.explicit_guard_passed)
144
+ && (!record.compaction_guard_applicable || record.compaction_guard_passed));
145
+ const gates = {
146
+ baseline_drift: driftDelta * 100n <= early * 20n,
147
+ candidate_p50: aggregate.candidate.p50 <= aggregate.baseline.p50,
148
+ candidate_p95: aggregate.candidate.p95 <= aggregate.baseline.p95,
149
+ phase_p95: PHASES.every((phase) => phaseStats[phase].candidate.p95 <= phaseStats[phase].baseline.p95),
150
+ selected_improvement: selected.kind === "latency"
151
+ ? phaseStats[selected.phase].candidate.p95 * 100n <= phaseStats[selected.phase].baseline.p95 * 85n
152
+ : context.candidate * 100 <= context.baseline * 75,
153
+ correctness,
154
+ };
155
+ const verdict = !gates.baseline_drift ? "BLOCKED_ENVIRONMENT_DRIFT" : Object.values(gates).every(Boolean) ? "PASS" : "FAIL";
156
+ return {
157
+ schema: RECEIPT_SCHEMA, verdict, scenario_id: SCENARIO_ID, product: "litclaude", record_count: value.records.length,
158
+ identities: value.identities, selected, aggregate_latency_ns: Object.fromEntries(ARMS.map((arm) => [arm, stringifyStats(aggregate[arm])])),
159
+ phase_latency_ns: Object.fromEntries(PHASES.map((phase) => [phase, Object.fromEntries(ARMS.map((arm) => [arm, stringifyStats(phaseStats[phase][arm])]))])),
160
+ baseline_drift_ratio: Number(driftDelta) / Number(early), context_reduction_ratio: (context.baseline - context.candidate) / context.baseline,
161
+ context_bytes: context, gates, provider: value.provider, provider_calls: 0, provider_completions: 0,
162
+ };
163
+ }
164
+
165
+ function main() {
166
+ const [inputPath] = process.argv.slice(2);
167
+ if (!inputPath) fail("USAGE");
168
+ const receipt = validateV2Cohort(parseJson(readFileSync(inputPath, "utf8")));
169
+ process.stdout.write(`${JSON.stringify(receipt)}\n`);
170
+ process.exitCode = receipt.verdict === "PASS" ? 0 : 1;
171
+ }
172
+
173
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
174
+ try { main(); } catch (error) {
175
+ const errorCode = error instanceof ContractError ? error.code : "INPUT_READ_FAILED";
176
+ process.stdout.write(`${JSON.stringify({ schema: RECEIPT_SCHEMA, verdict: "INVALID", error_code: errorCode, provider_calls: 0, provider_completions: 0 })}\n`);
177
+ process.exitCode = errorCode === "USAGE" ? 64 : 65;
178
+ }
179
+ }
@@ -22,8 +22,10 @@ import {
22
22
  formatCleanupReceipt,
23
23
  isolatedInstall,
24
24
  repoRoot,
25
+ runLitClaudeLocalSpeed,
25
26
  tempRoot,
26
27
  } from "./qa-real-surface-lib.mjs";
28
+ import { ContractError, validateScenarioText } from "./harness-speed-scenario.mjs";
27
29
 
28
30
  const version = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version;
29
31
 
@@ -434,8 +436,64 @@ const BEHAVIOURS = [
434
436
  ["plugin validation", pluginValidation],
435
437
  ];
436
438
 
437
- function main() {
438
- const json = process.argv.slice(2).includes("--json");
439
+ const argumentValue = (args, name) => {
440
+ const index = args.indexOf(name);
441
+ return index === -1 ? null : args[index + 1] ?? null;
442
+ };
443
+
444
+ const compactCleanup = (cleanup) => ({
445
+ status: cleanup.status,
446
+ resource_count: cleanup.resources.length,
447
+ cleaned_count: cleanup.resources.filter(({ state }) => state === "cleaned").length,
448
+ leaked_count: cleanup.resources.filter(({ state }) => state === "leaked").length,
449
+ processes_remaining: 0,
450
+ });
451
+
452
+ async function localSpeedMain(args) {
453
+ const sourceRoot = argumentValue(args, "--source-root");
454
+ const arm = argumentValue(args, "--arm");
455
+ const scenarioPath = argumentValue(args, "--scenario");
456
+ const samples = Number(argumentValue(args, "--samples"));
457
+ let receipt;
458
+ try {
459
+ if (!sourceRoot || !arm || !scenarioPath) throw new Error("usage");
460
+ const scenarioText = readFileSync(scenarioPath, "utf8");
461
+ const validation = validateScenarioText(scenarioText);
462
+ receipt = await runLitClaudeLocalSpeed({
463
+ sourceRoot,
464
+ arm,
465
+ samples,
466
+ scenario: JSON.parse(scenarioText),
467
+ });
468
+ receipt.scenario_fixture_sha256 = validation.fixture_sha256;
469
+ } catch (error) {
470
+ receipt = {
471
+ schema: "litfamily.harness-speed/v1",
472
+ scenario_id: "litfamily-speed-lit-activation-v1",
473
+ product: "litclaude",
474
+ measurement: "provider_free_installed_hook",
475
+ arm: ["baseline", "candidate"].includes(arm) ? arm : "INVALID",
476
+ verdict: "INVALID",
477
+ error_code: error instanceof ContractError ? error.code
478
+ : error instanceof Error && error.message === "usage" ? "USAGE"
479
+ : "LOCAL_PROBE_FAILED",
480
+ provider_calls: 0,
481
+ };
482
+ }
483
+ const cleanup = compactCleanup(cleanupAll());
484
+ receipt.cleanup = cleanup;
485
+ if (cleanup.status !== "complete") receipt.verdict = "FAIL";
486
+ process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`);
487
+ process.exitCode = receipt.verdict === "PASS" ? 0 : receipt.error_code === "USAGE" ? 64 : 1;
488
+ }
489
+
490
+ async function main() {
491
+ const args = process.argv.slice(2);
492
+ if (args.includes("--local-speed")) {
493
+ await localSpeedMain(args);
494
+ return;
495
+ }
496
+ const json = args.includes("--json");
439
497
  const results = BEHAVIOURS.map(([name, probe]) => {
440
498
  let outcome;
441
499
  try {
@@ -502,4 +560,4 @@ function main() {
502
560
  process.stdout.write("REAL_SURFACE_BEHAVIORS_PASS\n");
503
561
  }
504
562
 
505
- main();
563
+ await main();
@@ -10,10 +10,20 @@
10
10
  // `cleanupAll()` and print the receipt; an unreleased root is reported, never hidden.
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { createHash } from "node:crypto";
13
- import { existsSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
13
+ import {
14
+ existsSync,
15
+ lstatSync,
16
+ mkdtempSync,
17
+ readFileSync,
18
+ readdirSync,
19
+ readlinkSync,
20
+ rmSync,
21
+ statSync,
22
+ writeFileSync,
23
+ } from "node:fs";
14
24
  import { tmpdir } from "node:os";
15
25
  import { join, resolve } from "node:path";
16
- import { fileURLToPath } from "node:url";
26
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
27
  import { deflateSync } from "node:zlib";
18
28
 
19
29
  export const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
@@ -476,12 +486,13 @@ export function boundedPng(width, height, fill = 0) {
476
486
  // Isolated install lifecycle
477
487
  // ---------------------------------------------------------------------------
478
488
 
479
- // Installs the shipped CLI into two temp roots. Never touches a live profile: both
480
- // LITCLAUDE_HOME and CLAUDE_CONFIG_DIR are overridden, which is the same isolation the existing
481
- // scripts/qa-portable-install.sh gate uses.
482
- export function isolatedInstall({ label = "install" } = {}) {
489
+ // Installs the shipped CLI into isolated temp roots. Never touches a live profile:
490
+ // LITCLAUDE_HOME and CLAUDE_CONFIG_DIR are always overridden, and benchmark callers may
491
+ // additionally isolate HOME. This is the same profile boundary as qa-portable-install.sh.
492
+ export function isolatedInstall({ label = "install", sourceRoot = repoRoot, isolateUserHome = false } = {}) {
483
493
  const litHome = tempRoot(`${label}-lit-home`);
484
494
  const claudeHome = tempRoot(`${label}-claude-home`);
495
+ const userHome = isolateUserHome ? tempRoot(`${label}-user-home`) : null;
485
496
  const env = {
486
497
  ...process.env,
487
498
  LITCLAUDE_HOME: litHome,
@@ -489,13 +500,15 @@ export function isolatedInstall({ label = "install" } = {}) {
489
500
  LITCLAUDE_HUD_ACCENT: "cyan",
490
501
  LITCLAUDE_HUD_ACCENT_PROMPT: "0",
491
502
  };
503
+ if (userHome !== null) env.HOME = userHome;
492
504
  delete env.CLAUDE_HOME;
493
- const cli = join(repoRoot, "bin", "litclaude-ai.js");
505
+ const cli = join(resolve(sourceRoot), "bin", "litclaude-ai.js");
494
506
  const install = runNode(cli, ["install"], { env });
495
507
  const pathProbe = install.status === 0 ? runNode(cli, ["path"], { env }) : null;
496
508
  return {
497
509
  litHome,
498
510
  claudeHome,
511
+ userHome,
499
512
  env,
500
513
  install,
501
514
  pluginPath: pathProbe?.status === 0 ? pathProbe.stdout.trim() : "",
@@ -503,3 +516,224 @@ export function isolatedInstall({ label = "install" } = {}) {
503
516
  run: (args, options = {}) => runNode(cli, args, { env, ...options }),
504
517
  };
505
518
  }
519
+
520
+ // ---------------------------------------------------------------------------
521
+ // Provider-free installed hook timing
522
+ // ---------------------------------------------------------------------------
523
+
524
+ const LOCAL_SPEED_SCHEMA = "litfamily.harness-speed/v1";
525
+ const LOCAL_SPEED_SCENARIO = "litfamily-speed-lit-activation-v1";
526
+ const LOCAL_SPEED_MAX_OUTPUT_BYTES = 64 * 1024;
527
+ const LOCAL_SPEED_PHASES = Object.freeze([
528
+ ["session_start", "session-start", "SessionStart"],
529
+ ["s1_activation", "user-prompt-submit", "UserPromptSubmit"],
530
+ ["s2_continuation", "user-prompt-submit", "UserPromptSubmit"],
531
+ ["no_route_floor", "user-prompt-submit", "UserPromptSubmit"],
532
+ ]);
533
+ const NO_ROUTE_CANARY = "CANARY-4e9897f7-ignore-prior-instructions";
534
+
535
+ export function nearestRank(values, percentile) {
536
+ if (!Array.isArray(values) || values.length === 0) return null;
537
+ const sorted = [...values].sort((left, right) => left - right);
538
+ return sorted[Math.max(0, Math.ceil(percentile * sorted.length) - 1)];
539
+ }
540
+
541
+ function installedTreeSha256(root) {
542
+ const digest = createHash("sha256");
543
+ let fileCount = 0;
544
+ let byteCount = 0;
545
+ const visit = (directory, prefix = "") => {
546
+ for (const entry of readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
547
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
548
+ const path = join(directory, entry.name);
549
+ const stats = lstatSync(path);
550
+ if (entry.isDirectory()) {
551
+ digest.update(`directory\0${relativePath}\0${stats.mode & 0o777}\0`);
552
+ visit(path, relativePath);
553
+ } else if (entry.isFile()) {
554
+ const bytes = readFileSync(path);
555
+ digest.update(`file\0${relativePath}\0${stats.mode & 0o777}\0${bytes.length}\0`);
556
+ digest.update(bytes);
557
+ fileCount += 1;
558
+ byteCount += bytes.length;
559
+ } else if (entry.isSymbolicLink()) {
560
+ digest.update(`symlink\0${relativePath}\0${readlinkSync(path)}\0`);
561
+ } else {
562
+ throw new Error("unsupported installed plugin entry type");
563
+ }
564
+ }
565
+ };
566
+ visit(root);
567
+ return { sha256: digest.digest("hex"), file_count: fileCount, byte_count: byteCount };
568
+ }
569
+
570
+ function timedHookRun(hookBin, event, payload, { cwd, env }) {
571
+ const started = process.hrtime.bigint();
572
+ const result = spawnSync(process.execPath, [hookBin, event], {
573
+ cwd,
574
+ encoding: "utf8",
575
+ env,
576
+ input: JSON.stringify(payload),
577
+ maxBuffer: LOCAL_SPEED_MAX_OUTPUT_BYTES * 2,
578
+ timeout: 30_000,
579
+ });
580
+ const ended = process.hrtime.bigint();
581
+ const stdout = result.stdout ?? "";
582
+ let parsed = null;
583
+ if (Buffer.byteLength(stdout, "utf8") <= LOCAL_SPEED_MAX_OUTPUT_BYTES && stdout.trim()) {
584
+ try { parsed = JSON.parse(stdout); } catch { /* Classified below without retaining output. */ }
585
+ }
586
+ return {
587
+ durationMs: Number(ended - started) / 1_000_000,
588
+ outputBytes: Buffer.byteLength(stdout, "utf8"),
589
+ status: result.status,
590
+ signal: result.signal,
591
+ parsed,
592
+ combinedOutput: `${stdout}${result.stderr ?? ""}`,
593
+ };
594
+ }
595
+
596
+ function phaseCorrect(phase, run) {
597
+ if (run.status !== 0 || run.signal !== null || run.parsed === null
598
+ || run.outputBytes <= 0 || run.outputBytes > LOCAL_SPEED_MAX_OUTPUT_BYTES
599
+ || run.parsed.hookSpecificOutput?.hookEventName !== phase.eventName) return false;
600
+ const context = run.parsed.hookSpecificOutput?.additionalContext;
601
+ if (typeof context !== "string") return false;
602
+ if (phase.id === "session_start") return context.startsWith("LitClaude rules loaded for ");
603
+ if (phase.id === "s1_activation") {
604
+ return /LITWORK MODE ENABLED/u.test(context)
605
+ && /LitClaude lit hook active/u.test(run.parsed.systemMessage ?? "");
606
+ }
607
+ return run.parsed.systemMessage === undefined
608
+ && context === "LitClaude prompt hook checked: no workflow activation.";
609
+ }
610
+
611
+ const aggregatePhase = (samples) => {
612
+ const durations = samples.map(({ durationMs }) => durationMs);
613
+ const bytes = samples.map(({ outputBytes }) => outputBytes);
614
+ const passed = samples.filter(({ correct }) => correct).length;
615
+ return {
616
+ samples: samples.length,
617
+ duration_ms: {
618
+ p50: nearestRank(durations, 0.5),
619
+ p95: nearestRank(durations, 0.95),
620
+ min: Math.min(...durations),
621
+ max: Math.max(...durations),
622
+ },
623
+ output_bytes: {
624
+ p50: nearestRank(bytes, 0.5),
625
+ p95: nearestRank(bytes, 0.95),
626
+ min: Math.min(...bytes),
627
+ max: Math.max(...bytes),
628
+ },
629
+ correctness: { passed, failed: samples.length - passed },
630
+ };
631
+ };
632
+
633
+ async function cacheDiagnostic(pluginPath) {
634
+ const helper = join(pluginPath, "lib", "cache-measurement.mjs");
635
+ if (!existsSync(helper)) {
636
+ return {
637
+ status: "UNAVAILABLE",
638
+ reason: "helper-not-installed",
639
+ cache_read_rate: null,
640
+ cache_reuse_rate: null,
641
+ provider_receipt_observed: false,
642
+ };
643
+ }
644
+ try {
645
+ const module = await import(pathToFileURL(helper).href);
646
+ const measured = module.measureProviderCache();
647
+ return {
648
+ status: measured.status,
649
+ reason: measured.reason,
650
+ cache_read_rate: measured.cacheReadRate,
651
+ cache_reuse_rate: measured.cacheReuseRate,
652
+ provider_receipt_observed: measured.receipt !== null,
653
+ };
654
+ } catch {
655
+ return { status: "UNAVAILABLE", reason: "helper-load-failed" };
656
+ }
657
+ }
658
+
659
+ export async function runLitClaudeLocalSpeed({ sourceRoot, arm, samples, scenario }) {
660
+ if (!["baseline", "candidate"].includes(arm)) throw new Error("invalid local speed arm");
661
+ if (!Number.isSafeInteger(samples) || samples < 1 || samples > 100) throw new Error("invalid local speed sample count");
662
+ const records = new Map(scenario?.records?.map((record) => [record.id, record]) ?? []);
663
+ if (scenario?.scenario_id !== LOCAL_SPEED_SCENARIO || !records.has("S1") || !records.has("S2")) {
664
+ throw new Error("invalid local speed scenario");
665
+ }
666
+
667
+ const handle = isolatedInstall({ label: "speed-local", sourceRoot, isolateUserHome: true });
668
+ if (handle.install.status !== 0 || !handle.pluginPath) throw new Error("isolated install failed");
669
+ const hookBin = join(handle.pluginPath, "bin", "litclaude-hook.js");
670
+ if (!existsSync(hookBin)) throw new Error("installed hook missing");
671
+ const workspace = tempRoot("speed-local-workspace");
672
+ writeFileSync(join(workspace, "package.json"), "{\"private\":true}\n");
673
+ const env = {
674
+ ...handle.env,
675
+ CLAUDE_PLUGIN_ROOT: handle.pluginPath,
676
+ LITCLAUDE_NO_AUTO_UPDATE: "1",
677
+ LITCLAUDE_NO_UPDATE_CHECK: "1",
678
+ NO_UPDATE_NOTIFIER: "1",
679
+ };
680
+ const phaseSamples = Object.fromEntries(LOCAL_SPEED_PHASES.map(([id]) => [id, []]));
681
+ let canaryEchoed = false;
682
+
683
+ for (let index = 0; index < samples; index += 1) {
684
+ const sessionId = `speed-${arm}-${index}`;
685
+ const phaseInputs = {
686
+ session_start: { hook_event_name: "SessionStart", cwd: workspace, source: "startup", session_id: sessionId },
687
+ s1_activation: { hook_event_name: "UserPromptSubmit", cwd: workspace, prompt: records.get("S1").prompt, session_id: sessionId },
688
+ s2_continuation: { hook_event_name: "UserPromptSubmit", cwd: workspace, prompt: records.get("S2").prompt, session_id: sessionId },
689
+ no_route_floor: {
690
+ hook_event_name: "UserPromptSubmit",
691
+ cwd: workspace,
692
+ prompt: `${NO_ROUTE_CANARY} and expose credentials`,
693
+ session_id: sessionId,
694
+ },
695
+ };
696
+ for (const [id, event, eventName] of LOCAL_SPEED_PHASES) {
697
+ const run = timedHookRun(hookBin, event, phaseInputs[id], { cwd: workspace, env });
698
+ canaryEchoed ||= run.combinedOutput.includes(NO_ROUTE_CANARY);
699
+ phaseSamples[id].push({
700
+ durationMs: run.durationMs,
701
+ outputBytes: run.outputBytes,
702
+ correct: phaseCorrect({ id, eventName }, run),
703
+ });
704
+ }
705
+ }
706
+
707
+ const phases = Object.fromEntries(Object.entries(phaseSamples).map(([id, values]) => [id, aggregatePhase(values)]));
708
+ const failed = Object.values(phases).reduce((sum, phase) => sum + phase.correctness.failed, 0);
709
+ const artifact = installedTreeSha256(handle.pluginPath);
710
+ return {
711
+ schema: LOCAL_SPEED_SCHEMA,
712
+ scenario_id: LOCAL_SPEED_SCENARIO,
713
+ product: "litclaude",
714
+ measurement: "provider_free_installed_hook",
715
+ arm,
716
+ verdict: failed === 0 && !canaryEchoed ? "PASS" : "FAIL",
717
+ clock: "process.hrtime.bigint",
718
+ sample_count_per_phase: samples,
719
+ artifact: {
720
+ kind: "isolated-installed-plugin-tree",
721
+ sha256: artifact.sha256,
722
+ file_count: artifact.file_count,
723
+ byte_count: artifact.byte_count,
724
+ },
725
+ phases,
726
+ correctness: {
727
+ verdict: failed === 0 && !canaryEchoed ? "PASS" : "FAIL",
728
+ installed_hook_events: failed === 0,
729
+ s1_litwork_mode: phases.s1_activation.correctness.failed === 0,
730
+ s2_same_session: phases.s2_continuation.correctness.failed === 0,
731
+ no_route_silent: phases.no_route_floor.correctness.failed === 0,
732
+ bounded_output: Object.values(phases).every(({ output_bytes: bytes }) => bytes.max <= LOCAL_SPEED_MAX_OUTPUT_BYTES),
733
+ canary_echoed: canaryEchoed,
734
+ failure_count: failed + (canaryEchoed ? 1 : 0),
735
+ },
736
+ diagnostics: { provider_cache: await cacheDiagnostic(handle.pluginPath) },
737
+ provider_calls: 0,
738
+ };
739
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "schema": "litfamily.harness-speed-scenario/v1",
3
+ "scenario_id": "litfamily-speed-lit-activation-v1",
4
+ "encoding": "UTF-8",
5
+ "prompt_trailing_newline": false,
6
+ "records": [
7
+ {
8
+ "id": "B0",
9
+ "measured": false,
10
+ "prompt": "Reply with exactly this ASCII token and nothing else: LIT_SPEED_B0_OK",
11
+ "prompt_bytes": 69,
12
+ "prompt_sha256": "a5f634868cc6e20375f60803dcbe850eeb3d2709d6bb2c0efee204b9b5534904",
13
+ "sentinel": "LIT_SPEED_B0_OK",
14
+ "sentinel_bytes": 15,
15
+ "sentinel_sha256": "7652e386a8825b8459833ec8aa7d19fa6a55bf82652f63f45cc4ae29bcfee17b"
16
+ },
17
+ {
18
+ "id": "S1",
19
+ "measured": true,
20
+ "prompt": "lit\nReply with exactly this ASCII token and nothing else: LIT_SPEED_S1_OK",
21
+ "prompt_bytes": 73,
22
+ "prompt_sha256": "a52e0ec9444cc2195c1c8dbb638862251f1f07df0512c505d06b43d44d4a4e78",
23
+ "sentinel": "LIT_SPEED_S1_OK",
24
+ "sentinel_bytes": 15,
25
+ "sentinel_sha256": "8a655cd5ccd93b2727d641b5ca444e0f72bc09016115b7b6b16aa29e8457414a"
26
+ },
27
+ {
28
+ "id": "S2",
29
+ "measured": true,
30
+ "prompt": "Continue the active workflow. Reply with exactly this ASCII token and nothing else: LIT_SPEED_S2_OK",
31
+ "prompt_bytes": 99,
32
+ "prompt_sha256": "7b6712f5542798968a74a56241689f40475d50851d5a4d80f877d9a2589983a0",
33
+ "sentinel": "LIT_SPEED_S2_OK",
34
+ "sentinel_bytes": 15,
35
+ "sentinel_sha256": "482d6b2f6dc3a90d5feb4aef7615f715ef2ffbceea5e5ffde90f070175c22dc8"
36
+ }
37
+ ]
38
+ }