@compaction/cli 0.1.2 → 0.2.0

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 (38) hide show
  1. package/README.md +11 -1
  2. package/dist/cli/commands/compact.js +15 -0
  3. package/dist/cli/commands/context.d.ts +2 -0
  4. package/dist/cli/commands/context.js +130 -0
  5. package/dist/cli/commands/import.js +3 -1
  6. package/dist/cli/commands/init.js +184 -157
  7. package/dist/cli/commands/run.js +13 -1
  8. package/dist/cli/index.js +4 -1
  9. package/dist/cli/onboarding/InitTui.d.ts +52 -0
  10. package/dist/cli/onboarding/InitTui.js +153 -0
  11. package/dist/cli/onboarding/model.d.ts +39 -0
  12. package/dist/cli/onboarding/model.js +77 -0
  13. package/dist/cli/onboarding/should-use-tui.d.ts +25 -0
  14. package/dist/cli/onboarding/should-use-tui.js +35 -0
  15. package/dist/cli/onboarding/wordmark.d.ts +28 -0
  16. package/dist/cli/onboarding/wordmark.js +72 -0
  17. package/dist/core/aggregate-format.d.ts +3 -1
  18. package/dist/core/aggregate-format.js +18 -0
  19. package/dist/core/context-store-eval-cases.d.ts +6 -0
  20. package/dist/core/context-store-eval-cases.js +331 -0
  21. package/dist/core/context-store-eval.d.ts +140 -0
  22. package/dist/core/context-store-eval.js +138 -0
  23. package/dist/core/context-store-fs.d.ts +73 -0
  24. package/dist/core/context-store-fs.js +157 -0
  25. package/dist/core/context-store-sufficiency.d.ts +98 -0
  26. package/dist/core/context-store-sufficiency.js +135 -0
  27. package/dist/core/context-store.d.ts +155 -0
  28. package/dist/core/context-store.js +228 -0
  29. package/dist/core/report-generator.d.ts +32 -1
  30. package/dist/core/report-generator.js +67 -0
  31. package/dist/core/run-aggregator.d.ts +64 -0
  32. package/dist/core/run-aggregator.js +132 -3
  33. package/dist/core/session-aggregate.d.ts +43 -0
  34. package/dist/core/session-aggregate.js +89 -7
  35. package/dist/core/trace-adapters.d.ts +8 -0
  36. package/dist/core/trace-adapters.js +4 -0
  37. package/dist/core/types.d.ts +55 -0
  38. package/package.json +5 -1
@@ -46,6 +46,14 @@ export interface RunRecord {
46
46
  run_directory: string;
47
47
  report_path: string;
48
48
  model: string;
49
+ /**
50
+ * Content-addressed distinctness fingerprint digest of the source trace, when the run's
51
+ * report.json carried one (`trace_fingerprint.content_sha256`). Used to de-duplicate a
52
+ * run captured/aggregated twice (same digest → counted once) so headline savings are never
53
+ * inflated. `null` for older reports without a fingerprint — those are treated as distinct
54
+ * (cannot dedup what cannot be identified) and noted honestly, never collapsed.
55
+ */
56
+ trace_fingerprint: string | null;
49
57
  /** Token evidence tier for this run (drives the provider-reported vs local-estimate split). */
50
58
  token_evidence: RunTokenEvidence;
51
59
  input_tokens_before: number;
@@ -73,6 +81,32 @@ export interface SkippedRunDirectory {
73
81
  run_directory: string;
74
82
  reason: string;
75
83
  }
84
+ /**
85
+ * Distinctness (duplicate-safety) summary over a set of runs, by content fingerprint.
86
+ *
87
+ * This is a count of DISTINCT captured runs by one-way content fingerprint — NOT a billing,
88
+ * provider, or semantic claim. A run captured/aggregated twice with the SAME fingerprint is a
89
+ * duplicate and is counted once; a run WITHOUT a fingerprint cannot be de-duplicated and is
90
+ * treated as distinct (and reported in `runs_without_fingerprint`), never collapsed.
91
+ *
92
+ * The headline token/cost/savings totals in `AggregateTotals` are computed over the
93
+ * duplicate-safe set (each distinct run once), so a re-captured run never double-counts.
94
+ */
95
+ export interface DistinctnessSummary {
96
+ /** Total runs considered before de-duplication (M in "distinct N of M"). */
97
+ total_runs: number;
98
+ /**
99
+ * Distinct runs after de-duplication (N): distinct verified fingerprints + every run without
100
+ * a fingerprint (each unidentifiable run is its own distinct contribution).
101
+ */
102
+ distinct_runs: number;
103
+ /** Duplicate runs detected and excluded from the headline double-count (K = total - distinct). */
104
+ duplicate_runs: number;
105
+ /** Runs whose fingerprint was present and verifiable (contributed to `distinct_runs`, de-duplicated). */
106
+ runs_with_fingerprint: number;
107
+ /** Runs with NO fingerprint (older reports): each counted as distinct; cannot be de-duplicated. */
108
+ runs_without_fingerprint: number;
109
+ }
76
110
  /** Aggregated token/cost/savings totals over a set of runs, with the evidence split. */
77
111
  export interface AggregateTotals {
78
112
  run_count: number;
@@ -134,6 +168,8 @@ export interface SessionSummary {
134
168
  };
135
169
  totals: AggregateTotals;
136
170
  verification: VerificationCounts;
171
+ /** Duplicate-safety summary for this session's runs (distinct N of M by content fingerprint). */
172
+ distinctness: DistinctnessSummary;
137
173
  }
138
174
  export interface AggregateReport {
139
175
  aggregate_id: string;
@@ -146,6 +182,12 @@ export interface AggregateReport {
146
182
  session_count: number;
147
183
  totals: AggregateTotals;
148
184
  verification: VerificationCounts;
185
+ /**
186
+ * Cross-session duplicate-safety summary (distinct N of M captured runs by content fingerprint).
187
+ * The headline `totals` above are computed over the duplicate-safe set, so a run captured/
188
+ * aggregated twice with the same fingerprint is counted once and never inflates savings.
189
+ */
190
+ distinctness: DistinctnessSummary;
149
191
  sessions: SessionSummary[];
150
192
  /** Provider/runtime breakdown (operator-asserted provider label; unlabeled when absent). */
151
193
  by_provider: BreakdownBucket[];
@@ -166,6 +208,7 @@ export interface AggregateEvidenceLabels {
166
208
  compactions: string;
167
209
  verification: string;
168
210
  provider_runtime: string;
211
+ distinctness: string;
169
212
  }
170
213
  export declare const AGGREGATE_EVIDENCE_LABELS: AggregateEvidenceLabels;
171
214
  /** True when a run passes every supplied local-label filter (case-sensitive exact match). */
@@ -33,6 +33,7 @@ import { randomUUID } from "node:crypto";
33
33
  import { readdir, readFile } from "node:fs/promises";
34
34
  import { join } from "node:path";
35
35
  import { NO_LABEL, parseRunLabels } from "./run-labels.js";
36
+ import { classifyDistinctness } from "./trace-fingerprint.js";
36
37
  export const AGGREGATE_ARTIFACT_DIRECTORY = ".compaction/aggregates";
37
38
  export const AGGREGATE_EVIDENCE_LABELS = {
38
39
  input_tokens: "input tokens: provider-reported where the run's token accounting was measured (provider usage " +
@@ -50,7 +51,12 @@ export const AGGREGATE_EVIDENCE_LABELS = {
50
51
  "task-check outcomes copied from each run's strong-eval; runs without a strong-eval are not_evaluated, " +
51
52
  "never counted as a pass. These are DETERMINISTIC recoverability checks, NOT semantic guarantees.",
52
53
  provider_runtime: "provider/runtime: operator-ASSERTED local label (never provider-verified); runs without a provider " +
53
- "label are grouped under 'unlabeled'. This label is distinct from the token-evidence tier."
54
+ "label are grouped under 'unlabeled'. This label is distinct from the token-evidence tier.",
55
+ distinctness: "distinct runs: a count of DISTINCT captured runs by one-way content fingerprint (SHA-256 over the " +
56
+ "normalized trace). A run captured/aggregated twice with the SAME fingerprint is a duplicate and is " +
57
+ "counted once, so headline savings are never inflated; runs without a fingerprint cannot be " +
58
+ "de-duplicated and are each counted as distinct. This is a local content-distinctness count — NOT a " +
59
+ "billing, provider, or semantic claim."
54
60
  };
55
61
  const STANDARD_LIMITATIONS = [
56
62
  "Aggregated from LOCAL artifacts only (.compaction/runs/*/{report.json,strong-eval.json,run-labels.json}); no provider calls, no upload, no telemetry.",
@@ -59,11 +65,66 @@ const STANDARD_LIMITATIONS = [
59
65
  "Output tokens are summed only over runs that recorded them; runs without a strong-eval contribute unknown output tokens and are counted separately, never inferred.",
60
66
  "Verification outcomes are deterministic recoverability checks copied from each run's strong-eval; they are NOT semantic or meaning-preservation guarantees. Runs without a strong-eval are not_evaluated, never a pass.",
61
67
  "Labels (project/workflow/provider/user-label/session) are operator-supplied local tags with no evidence weight; provider here is operator-asserted, not provider-verified.",
68
+ "Headline savings are duplicate-safe: runs sharing a content fingerprint (a re-captured/re-aggregated run) are counted once; runs without a fingerprint cannot be de-duplicated and are each counted as distinct. This is a local content-distinctness count, not a billing/provider/semantic claim.",
62
69
  "This is a LOCAL, file-based rollup. There is no hosted dashboard, database, auth, billing, model routing, or provider integration."
63
70
  ];
64
71
  function round6(value) {
65
72
  return Number((value + Number.EPSILON).toFixed(6));
66
73
  }
74
+ /**
75
+ * De-duplicate a set of runs by content fingerprint for duplicate-safe headline totals.
76
+ *
77
+ * Keeps the FIRST run per distinct verified fingerprint; later runs with the same fingerprint
78
+ * are duplicates and are dropped from the headline (so a re-captured run never double-counts its
79
+ * savings). Runs WITHOUT a fingerprint are kept as-is — each is its own distinct contribution,
80
+ * because distinctness that cannot be verified is never collapsed (the anti-overcount contract in
81
+ * `classifyDistinctness`). Returns the duplicate-safe run set plus an honest distinctness summary.
82
+ *
83
+ * PURE: order-stable for the kept runs; makes savings MORE conservative, never larger.
84
+ */
85
+ function dedupeRunsByFingerprint(runs) {
86
+ const seenFingerprints = new Set();
87
+ const distinctRuns = [];
88
+ let runsWithFingerprint = 0;
89
+ let runsWithoutFingerprint = 0;
90
+ for (const run of runs) {
91
+ const fp = run.trace_fingerprint;
92
+ if (typeof fp === "string" && fp.length > 0) {
93
+ runsWithFingerprint += 1;
94
+ if (seenFingerprints.has(fp))
95
+ continue; // duplicate fingerprint: excluded from headline.
96
+ seenFingerprints.add(fp);
97
+ distinctRuns.push(run);
98
+ }
99
+ else {
100
+ runsWithoutFingerprint += 1;
101
+ distinctRuns.push(run); // unidentifiable: always its own distinct contribution.
102
+ }
103
+ }
104
+ // classifyDistinctness gives the verified-distinct count over the SAME records; distinct_runs is
105
+ // that verified-distinct count plus every unidentifiable run (each distinct by construction).
106
+ const classified = classifyDistinctness(runs.map((run) => ({ runId: run.run_id, content_sha256: run.trace_fingerprint })));
107
+ const distinctRunCount = classified.distinct_verified_count + runsWithoutFingerprint;
108
+ return {
109
+ distinctRuns,
110
+ distinctness: {
111
+ total_runs: runs.length,
112
+ distinct_runs: distinctRunCount,
113
+ duplicate_runs: runs.length - distinctRunCount,
114
+ runs_with_fingerprint: runsWithFingerprint,
115
+ runs_without_fingerprint: runsWithoutFingerprint
116
+ }
117
+ };
118
+ }
119
+ function emptyDistinctness() {
120
+ return {
121
+ total_runs: 0,
122
+ distinct_runs: 0,
123
+ duplicate_runs: 0,
124
+ runs_with_fingerprint: 0,
125
+ runs_without_fingerprint: 0
126
+ };
127
+ }
67
128
  function emptyTotals() {
68
129
  return {
69
130
  run_count: 0,
@@ -209,9 +270,12 @@ function buildSessions(runs) {
209
270
  }
210
271
  return [...sessions.entries()]
211
272
  .map(([sessionId, { explicit, runs: sessionRuns }]) => {
273
+ // Duplicate-safe: fold totals/verification over distinct runs only (re-captured runs with
274
+ // the same fingerprint are excluded from the double-count). run_ids still lists every run.
275
+ const { distinctRuns, distinctness } = dedupeRunsByFingerprint(sessionRuns);
212
276
  const totals = emptyTotals();
213
277
  const verification = emptyVerification();
214
- for (const run of sessionRuns) {
278
+ for (const run of distinctRuns) {
215
279
  addRunToTotals(totals, run);
216
280
  addRunToVerification(verification, run);
217
281
  }
@@ -227,7 +291,8 @@ function buildSessions(runs) {
227
291
  user_labels: distinct(sessionRuns.map((r) => r.labels.user_label))
228
292
  },
229
293
  totals,
230
- verification
294
+ verification,
295
+ distinctness
231
296
  };
232
297
  })
233
298
  .sort((a, b) => b.totals.total_input_tokens_saved - a.totals.total_input_tokens_saved ||
@@ -258,9 +323,13 @@ export function buildAggregateReport(runs, options = {}) {
258
323
  const generatedAt = options.generatedAt ?? new Date().toISOString();
259
324
  const filters = options.filters ?? {};
260
325
  const matched = runs.filter((run) => runMatchesFilters(run, filters));
326
+ // Duplicate-safe headline: fold cross-session totals/verification over the distinct run set only.
327
+ // A run captured/aggregated twice with the same content fingerprint is counted once; runs without
328
+ // a fingerprint are each distinct (cannot dedup what cannot be identified). Never inflates savings.
329
+ const { distinctRuns, distinctness } = dedupeRunsByFingerprint(matched);
261
330
  const totals = emptyTotals();
262
331
  const verification = emptyVerification();
263
- for (const run of matched) {
332
+ for (const run of distinctRuns) {
264
333
  addRunToTotals(totals, run);
265
334
  addRunToVerification(verification, run);
266
335
  }
@@ -275,10 +344,13 @@ export function buildAggregateReport(runs, options = {}) {
275
344
  session_count: sessions.length,
276
345
  totals,
277
346
  verification,
347
+ distinctness,
278
348
  sessions,
279
- by_provider: buildBreakdown(matched, (run) => run.labels.provider ?? NO_LABEL),
280
- by_project: buildBreakdown(matched, (run) => run.labels.project ?? NO_LABEL),
281
- by_workflow: buildBreakdown(matched, (run) => run.labels.workflow ?? NO_LABEL),
349
+ // Breakdowns also fold over the duplicate-safe distinct run set so per-bucket savings match the
350
+ // duplicate-safe headline (a re-captured run never double-counts inside its label bucket either).
351
+ by_provider: buildBreakdown(distinctRuns, (run) => run.labels.provider ?? NO_LABEL),
352
+ by_project: buildBreakdown(distinctRuns, (run) => run.labels.project ?? NO_LABEL),
353
+ by_workflow: buildBreakdown(distinctRuns, (run) => run.labels.workflow ?? NO_LABEL),
282
354
  skipped_run_directories: options.skippedRunDirectories ?? [],
283
355
  evidence_labels: AGGREGATE_EVIDENCE_LABELS,
284
356
  limitations: STANDARD_LIMITATIONS
@@ -320,6 +392,15 @@ function readinessFrom(status) {
320
392
  export function assembleRunRecord(input) {
321
393
  const { report, strongEval } = input;
322
394
  const notes = [];
395
+ // Content-addressed run identity: read the OPTIONAL trace_fingerprint digest if the report
396
+ // carried one. Older reports omit it; such a run cannot be de-duplicated and is treated as
397
+ // distinct (noted), never collapsed. Read defensively (digest string only; no content).
398
+ const fingerprintDigest = isRecord(report.trace_fingerprint) && typeof report.trace_fingerprint.content_sha256 === "string"
399
+ ? report.trace_fingerprint.content_sha256
400
+ : null;
401
+ if (fingerprintDigest === null) {
402
+ notes.push("No trace_fingerprint on report.json: this run is treated as distinct (cannot de-duplicate an unidentifiable run).");
403
+ }
323
404
  let tokenEvidence = "local_estimate";
324
405
  let inputBefore = report.original_input_tokens;
325
406
  let inputAfter = report.compacted_input_tokens;
@@ -389,6 +470,7 @@ export function assembleRunRecord(input) {
389
470
  run_directory: input.runDirectory,
390
471
  report_path: input.reportPath,
391
472
  model,
473
+ trace_fingerprint: fingerprintDigest,
392
474
  token_evidence: tokenEvidence,
393
475
  input_tokens_before: inputBefore,
394
476
  input_tokens_after: inputAfter,
@@ -19,6 +19,14 @@ export interface TraceAdapter {
19
19
  displayName: string;
20
20
  description: string;
21
21
  supportedSource: TraceAdapterSource;
22
+ /**
23
+ * Honest integration-readiness label for this source (AGENTS.md Level 0–6 +
24
+ * validation status). All import sources are Level 1 (local export/import — no live
25
+ * integration); this string also discloses how PROVEN the source is (e.g. validated
26
+ * on a synthetic fixture vs real-artifact validation still open), so `--list-sources`
27
+ * never reads as a stronger integration claim than the evidence supports.
28
+ */
29
+ readiness: string;
22
30
  limitations: string[];
23
31
  canHandle(input: unknown): boolean;
24
32
  normalize(input: unknown, options: TraceAdapterOptions): TraceAdapterResult;
@@ -504,6 +504,7 @@ export const agentTraceAdapter = {
504
504
  displayName: "Internal AgentTrace",
505
505
  description: "Validates a local file that already matches the internal AgentTrace JSON shape.",
506
506
  supportedSource: "agent-trace",
507
+ readiness: "Level 1 (local import) · canonical internal format, schema-validated",
507
508
  limitations: [
508
509
  "Only local JSON files are supported.",
509
510
  "Live provider/runtime capture is future work.",
@@ -519,6 +520,7 @@ export const messagesAdapter = {
519
520
  displayName: "Simple messages",
520
521
  description: "Normalizes a local array of role/content messages, or an object with a messages array, into AgentTrace.",
521
522
  supportedSource: "messages",
523
+ readiness: "Level 1 (local import) · generic role/content messages shape",
522
524
  limitations: [
523
525
  "Only local JSON files are supported.",
524
526
  "Messages need supported roles and string content.",
@@ -535,6 +537,7 @@ export const codexExecJsonlAdapter = {
535
537
  displayName: "Codex exec JSONL",
536
538
  description: "Normalizes a local JSONL stream produced by Codex exec --json into AgentTrace without launching Codex or calling provider APIs.",
537
539
  supportedSource: "codex-exec-jsonl",
540
+ readiness: "Level 1 (local export import) · validated end-to-end on a synthetic fixture; real-artifact validation OPEN (a real `codex exec --json` export has not yet been walked)",
538
541
  limitations: [
539
542
  "Only local user-supplied JSONL files are supported.",
540
543
  "This adapter does not launch Codex, call OpenAI APIs, scrape private data, or upload artifacts.",
@@ -564,6 +567,7 @@ export const unknownAdapter = {
564
567
  displayName: "Unknown local trace source",
565
568
  description: "Detects supported local AgentTrace and simple messages shapes before normalizing with the matching adapter.",
566
569
  supportedSource: "unknown",
570
+ readiness: "Level 1 (local import) · best-effort shape detection (AgentTrace / messages / Codex JSONL)",
567
571
  limitations: [
568
572
  "Detection is shape-based and local-file only.",
569
573
  "AgentTrace, simple messages, and Codex exec JSONL shapes are detected in v0.",
@@ -225,6 +225,61 @@ export interface CompactionReport {
225
225
  * price-table), NOT billing-confirmed and NOT realized savings.
226
226
  */
227
227
  spend_by_source?: SpendBySourceSummary;
228
+ /**
229
+ * Content-addressed per-run distinctness fingerprint of the source trace (additive,
230
+ * OPTIONAL, backward-compatible). When present, it lets the aggregate attribute this
231
+ * run's savings to a verifiable run identity and de-duplicate a run captured/aggregated
232
+ * twice (same fingerprint → counted once) so the headline savings are never inflated.
233
+ *
234
+ * Older reports omit this field; a run WITHOUT a fingerprint cannot be de-duplicated and
235
+ * is treated as distinct (you cannot dedup what you cannot identify) and noted honestly.
236
+ *
237
+ * Privacy: this is a one-way SHA-256 digest plus non-sensitive structural counts — NO
238
+ * message/prompt/tool-output text. Structurally matches `TraceFingerprint` from
239
+ * `src/core/trace-fingerprint.ts` (kept inline here so `types.ts` stays import-free).
240
+ * This is an attribution/integrity signal, NOT a billing, provider, or semantic claim.
241
+ */
242
+ trace_fingerprint?: {
243
+ /** Algorithm + canonicalization version (e.g. `sha256-canonical-content-v1`). */
244
+ algorithm: string;
245
+ /** Hex SHA-256 digest over the canonical normalized trace content (one-way; never raw content). */
246
+ content_sha256: string;
247
+ /** Non-sensitive structural count: number of messages hashed. */
248
+ message_count: number;
249
+ };
250
+ /**
251
+ * Composed per-run SAVINGS EVIDENCE record (additive, OPTIONAL, backward-compatible) —
252
+ * the V0.2 "evidence labels per result" surface. Older reports omit it. See
253
+ * `SavingsEvidence`. Composes EXISTING report numbers; makes no new/stronger claim.
254
+ */
255
+ savings_evidence?: SavingsEvidence;
256
+ }
257
+ /**
258
+ * Composed, per-run savings-evidence record (V0.2 "evidence labels per result"). Ties the
259
+ * evidence chain into ONE place and states, per run, the cost-source label, the
260
+ * recoverability/semantic status, and exactly which `claims-and-evidence-ladder` rung the
261
+ * figure stands on — so no consumer reads a stronger claim than the evidence supports.
262
+ *
263
+ * A single run is rung 1 (local estimate). The measured rung 1.5
264
+ * (`measured_caveated_estimate_delta`) is produced ONLY by the aggregate increment across
265
+ * N≥3 distinct runs, NEVER here. `billing_confirmed` is ALWAYS false; `semantic_preservation`
266
+ * is ALWAYS `not_evaluated` (never auto-certified).
267
+ */
268
+ export interface SavingsEvidence {
269
+ /** Is the run attributable to a verifiable identity (trace fingerprint present)? */
270
+ trace_identity: "fingerprinted" | "unidentified";
271
+ /** Honest cost-source label for this run's figures (mirrors `CostSource`). */
272
+ cost_source: "provider_reported" | "price_table_estimate" | "local_estimate" | "missing" | "unknown";
273
+ /** Deterministic recoverability status when an eval ran (`compact --eval`); else `not_evaluated`. */
274
+ recoverability: "passed" | "failed" | "not_computed" | "not_evaluated";
275
+ /** Semantic / meaning preservation is NEVER auto-certified — always `not_evaluated`. */
276
+ semantic_preservation: "not_evaluated";
277
+ /** Which claims-and-evidence-ladder rung this single-run figure stands on. */
278
+ evidence_rung: "local_estimate_single_run";
279
+ /** No v0 per-run figure is billing-confirmed. Always `false`. */
280
+ billing_confirmed: false;
281
+ /** One-line honest label (never asserts a stronger claim than the rung). */
282
+ label: string;
228
283
  }
229
284
  export interface CompactionPolicy {
230
285
  policy_name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compaction/cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -79,10 +79,14 @@
79
79
  "dependencies": {
80
80
  "chalk": "^5.4.1",
81
81
  "commander": "^12.1.0",
82
+ "ink": "^5.2.1",
83
+ "react": "^18.3.1",
82
84
  "zod": "^3.24.1"
83
85
  },
84
86
  "devDependencies": {
85
87
  "@types/node": "^22.10.2",
88
+ "@types/react": "^18.3.31",
89
+ "ink-testing-library": "^4.0.0",
86
90
  "tsx": "^4.19.2",
87
91
  "typescript": "^5.7.2",
88
92
  "vitest": "^4.1.8"