@ascenda-one/history-import 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @ascenda-one/history-import
2
+
3
+ Retrospective AI-usage import: extracts the baseline that already exists on
4
+ an engineer's machine — Claude Code transcripts, Cursor's conversation store,
5
+ VS Code local history and Copilot sessions, git — and ships it as
6
+ provenance-classed historical events on the existing telemetry wire.
7
+
8
+ The research behind this package (what is actually in each store, verified on
9
+ a real machine 2026-08-18) lives in the Flow workspace:
10
+ `asc-ascenda-app-workspace/docs/HISTORICAL_TELEMETRY_IMPORT.md`. Read it
11
+ before touching an extractor — the store formats are reverse-engineered and
12
+ every design rule here traces to a finding there.
13
+
14
+ ## Why this package exists here, not in the app
15
+
16
+ The macOS Flow app is sandboxed and child processes inherit the sandbox, so
17
+ it can never read `~/.claude` or the editors' Application Support stores. The
18
+ app is the consent and display surface; it hands the user one terminal
19
+ command (same pattern as hooks pairing) and this CLI does the reading.
20
+
21
+ ## Design rules (non-negotiable)
22
+
23
+ 1. **Evaporation order.** Claude Code first — its 30-day rolling purge is
24
+ deleting a day of baseline per day. Then Cursor, then VS Code, then git.
25
+ 2. **Copy, then parse.** Extraction only ever reads a staged snapshot
26
+ (`src/staging.ts`), never live files. SQLite snapshots carry their `-wal`.
27
+ 3. **Sniff per record, dispatch on the self-labelled version** (`version` on
28
+ Claude lines, `_v` on Cursor records, `version: 1` in VS Code entries).
29
+ Unknown shapes become `historical_unparsed` — raw retained in staging,
30
+ nothing inferred. Fixture tests per known (tool, version) pair.
31
+ 4. **Metrics only by default.** Prompt/response text, thinking blocks and
32
+ file contents never leave the machine. Content-level ingestion, if it ever
33
+ ships, is a separate explicit opt-in — not this package's default path.
34
+ 5. **Aggregate before shipping.** Per-session / per-day events, not one event
35
+ per bubble (one observed machine holds 17,727 bubbles).
36
+ 6. **Provenance is data.** Every event carries `historical_direct`,
37
+ `historical_derived` or `historical_unparsed` — never the live
38
+ `ai_work_telemetry` provenance — so no chart can pass history off as
39
+ live observation.
40
+
41
+ ## Status (2026-08-18)
42
+
43
+ | Piece | State |
44
+ |---|---|
45
+ | `scan` (per-store inventory, content never opened) | implemented |
46
+ | `fix-retention` (Claude `cleanupPeriodDays`, merge-not-clobber) | implemented |
47
+ | Staging/snapshot (copy-then-parse, WAL-aware, APFS clone-on-write) | implemented |
48
+ | **Claude Code extractor** (human-prompt/tool-result split, session folds incl. recursive subagent transcripts, after-hours, compaction, tool failures, context-window peak, human-corrected edits, correction cadence, gap-split active minutes, epoch marker) | **implemented, verified live** |
49
+ | **Batch shipper** (`POST /v1/tool-events/batch`, salted hashes, stable importKey) | **implemented, verified live** |
50
+ | **Cursor extractor** (composerHeaders + bubble aggregation via SQL-side `json_extract`, prompt text never parsed into the process, subagent-composer folding, epoch marker) | **implemented, verified live** |
51
+ | **VS Code extractor** (Timeline-history Chat-Edit day×workspace aggregation, Copilot chatSessions folding, workspace identity via `workspace.json` longest-prefix match, epoch marker) | **implemented, verified live** |
52
+ | `import [--ship]` end to end | **dry run 2026-08-18: Claude Code 9,198 events (333 sessions, 8,401 human prompts, 135 after-hours sessions, 261 sessions with ≥1 tool failure) + Cursor 661 events (104 sessions, 550 human prompts, 6 after-hours sessions) + VS Code 7,338 events (548 edit-days summing to the full 13,780 verified Chat Edits — monthly rollup reproduces the documented May-2026 cliff: Apr 2,079 → May 343 → Jun 1,047 with no manual tuning; 419 Copilot sessions, 6,131 human prompts, 114 after-hours sessions, 125 sessions with ≥1 request error; 2 Timeline-history files unparsed) — 17,197 total across all three stores. Last `--ship` run: 8,720/8,720 accepted by the backend, 2026-08-18, before this extractor's friction-signal, Cursor and VS Code additions.** |
53
+ | git extractor | stub — throws with a pointer |
54
+ | zsh `EXTENDED_HISTORY` apply | snippet only |
55
+
56
+ The human-prompt classifier is the load-bearing piece: on the verified store,
57
+ 108,528 user-role lines reduce to 8,272 actual typed prompts — the other 92%
58
+ are tool-result round-trips that would have inflated every prompt metric ~13x.
59
+
60
+ ## Gaps that block a real user running this twice
61
+
62
+ Both blockers are **implemented but unmerged** — asc-core-be branch
63
+ `claude/historical-import-dedup-and-consent`, not `origin/main` (verified
64
+ 19 Aug 2026). Neither is enforced by the backend a published client would
65
+ actually reach. What they become **once that branch merges**:
66
+
67
+ - **Consent scope — specified, not yet enforced.** `historical_import` is a
68
+ real `ToolConsentScope` here, and on that branch it is backed by
69
+ `ConsentType.HistoricalImport` (507) with ingestion enforcing it.
70
+
71
+ On `origin/main` today none of that exists: `ResolveConsentType` does not
72
+ recognise the scope and falls to its default arm, `AiDataProcessing` — the
73
+ lease already granted for live IDE telemetry. A backfill would ride in on
74
+ live-telemetry consent, which is exactly what the separate scope exists to
75
+ stop. **This gates publishing the package**, independently of any tier or
76
+ entitlement decision: paying for a capability is not the same act as
77
+ agreeing to a specific read of nine months of local history.
78
+
79
+ How it behaves once merged: Enforcement keys on the event's **provenance**, not
80
+ on the scope string it sends: any of the three `historical_*` classes
81
+ requires an active historical-import lease, so a client that keeps claiming
82
+ `ide_telemetry` over backdated events is rejected rather than waved through
83
+ to the default consent type. Nothing grants the lease at pairing — a paired
84
+ tool has consent to watch you from now on, never to read backwards.
85
+ - **Idempotency — closed.** `(pairedUser, toolInstallation, importKey)` is
86
+ unique in the database, checked before insert and enforced by a partial
87
+ unique index for the concurrent case. A replayed event answers `duplicate`,
88
+ writes nothing at all (not the event, not `lastSeenAt`, not an audit row),
89
+ and the batch response counts duplicates apart from both accepted and
90
+ rejected. `importKey` is **required** on any historical event. (Same caveat:
91
+ this is the branch's behaviour, not main's.)
92
+
93
+ **The dedup key is the source record ref alone — not `extractionId` + ref,
94
+ as this list originally said.** An extraction id is minted per run, so
95
+ including it would make every key unique and dedup nothing, which is exactly
96
+ the case a re-run is. The source record is the stable identity, so it is the
97
+ whole key; a re-run with a fresh `extractionId` over the same records
98
+ therefore still dedups, and the first run's extraction stays on record.
99
+ Pinned by `HistoricalImportIngestTests` in asc-core-be.
100
+ - **The key survives a changing store.** `ordinal` numbers an event only
101
+ among events sharing its whole identity — store, session, kind, instant —
102
+ so it separates genuine same-millisecond duplicates without encoding the
103
+ event's position in the run. Deleting a day, a session or an entire store
104
+ between runs leaves every surviving key untouched. Pinned by
105
+ `tests/importKeyStability.test.mjs`. (Until 20 Aug 2026 this was the
106
+ event's index in the whole shipped array, which re-keyed unchanged records
107
+ after a purge — the one case a re-run is for.)
108
+
109
+ Still open, and worth knowing before trusting a second run completely:
110
+
111
+ - **The 18 Aug 2026 shipment cannot be deduped against.** Production accepted
112
+ 8,720 backdated events from one machine before the `ImportKey` column
113
+ existed, so those rows carry `NULL` and the unique index is filtered on
114
+ `IS NOT NULL`. Nothing will ever match them. A clean re-ship needs them
115
+ removed backend-side first, or that window lands twice — once. Runs from
116
+ here are idempotent with each other.
117
+ - **Epoch markers** (Copilot→Cursor→Claude eras, Ascenda install) are not yet
118
+ event types anywhere.
119
+
120
+ ## Usage
121
+
122
+ ```
123
+ ascenda-history-import scan # human-readable inventory
124
+ ascenda-history-import scan --json # what the app's consent surface renders
125
+ ascenda-history-import fix-retention # dry-run; --apply to write
126
+ ascenda-history-import import # Claude Code + Cursor + VS Code, dry run; --ship to send
127
+ ```