# PRD: Lite Entry Point

**Feature:** lite-entry-point
**Branch:** feat/lite-entry-point
**Base:** main
**Date:** 2026-03-18
**Spec:** planning/lite-entry-point/spec.md (Rev 4)
**Spec review:** planning/lite-entry-point/spec-review.md (Rev 4/5, score 95/100)
**Status:** Draft — Rev 2 (Thing Rev 1 fixes)

---

## Background

The current `avo-inspector` package always ships `@noble/curves` (P-256 elliptic curve) and the event-spec validation pipeline. These are required only when a `publicEncryptionKey` is provided and the inspector is in dev/staging mode. Customers who never use property-value encryption pay a bundle-size cost for code they cannot use.

The lite entry point ships a stripped build (`avo-inspector/lite`) that omits:
- `AvoEncryption.ts` and `@noble/curves`
- `AvoEventSpecFetcher`, `AvoEventSpecCache`, `EventValidator`, `safe-regex2`

Target: under 7 KB gzipped after terser. The full entry point is unchanged.

---

## Architecture Summary

Six new source files in `src/lite/` are created as minimal copies of their full counterparts. Each copy changes exactly one import statement — replacing `AvoInspector` with `AvoInspectorLite`, or `AvoSchemaParser` with `AvoSchemaParserLite`. No full source files are modified.

| Lite File | Copied From | Change |
|---|---|---|
| `AvoSchemaParserLite.ts` | `AvoSchemaParser.ts` | Remove `encryptValue` import and all encryption logic |
| `AvoDeduplicatorLite.ts` | `AvoDeduplicator.ts` | Import `AvoSchemaParserLite` instead of `AvoSchemaParser` |
| `AvoNetworkCallsHandlerLite.ts` | `AvoNetworkCallsHandler.ts` | Import `AvoInspectorLite` + `AvoStreamIdLite`; fix relative paths |
| `AvoBatcherLite.ts` | `AvoBatcher.ts` | Import `AvoInspectorLite` + `AvoNetworkCallsHandlerLite`; fix relative paths |
| `AvoStreamIdLite.ts` | `AvoStreamId.ts` | Import `AvoInspectorLite` instead of `AvoInspector`; fix relative paths |
| `AvoInspectorLite.ts` | `AvoInspector.ts` | No eventSpec, no `publicEncryptionKey`, imports all Lite variants |

The `src/lite/index.ts` entry re-exports `AvoInspectorLite as AvoInspector` and `AvoInspectorEnv` — identical surface to the full entry point.

---

## Quality Checks (all stories)

- Build: `yarn build` — required on every story
- Unit tests: `yarn test --roots='<rootDir>/src'`
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'`

---

## Stories

### Story 1 — Lite source files: AvoSchemaParserLite + AvoNetworkCallsHandlerLite

**Approach:** general-purpose

**Goal:** Create the two foundational lite source files. These have no circular dependency on `AvoInspectorLite` and can be verified standalone.

**Files created (2):**
- `src/lite/AvoSchemaParserLite.ts` — copy of `AvoSchemaParser.ts` with encryption stripped
- `src/lite/AvoNetworkCallsHandlerLite.ts` — copy of `AvoNetworkCallsHandler.ts` with `AvoInspectorLite`/`AvoStreamIdLite` import placeholders

**Key implementation notes:**
- Create the `src/lite/` directory first: `mkdir -p src/lite` (the directory does not exist in the repo)
- `AvoSchemaParserLite`: remove `import { encryptValue } from "./AvoEncryption"` entirely; change types import to `from "./AvoNetworkCallsHandlerLite"`; delete `canSendEncryptedValues`, `getEncryptedPropertyValueIfEnabled`; in `extractSchema` remove the `canSendEncryptedValues` variable and the entire `encryptedValue` block in the primitive branch; keep method signature `static async extractSchema(eventProperties, publicEncryptionKey?, env?)` (params accepted but unused)
- `AvoNetworkCallsHandlerLite`: change `import { AvoInspector } from "./AvoInspector"` to `import { AvoInspectorLite as AvoInspector } from "./AvoInspectorLite"`; change `import AvoGuid from "./AvoGuid"` to `import AvoGuid from "../AvoGuid"`; change `import { AvoStreamId } from "./AvoStreamId"` to `import { AvoStreamIdLite as AvoStreamId } from "./AvoStreamIdLite"`; change `import type { EventSpecMetadata } from "./eventSpec/AvoEventSpecFetchTypes"` to `import type { EventSpecMetadata } from "../eventSpec/AvoEventSpecFetchTypes"`; add file header comment
- Both files start with the LITE COPY header comment documenting sync obligation
- At this stage `AvoInspectorLite` and `AvoStreamIdLite` are forward references — the files reference them in imports but they are created in later stories. TypeScript will not complain until a full `tsc` is run; the build is not expected to pass cleanly until Story 3

**Dependencies:** none

**Quality checks:**
- Build: `yarn build` (expected to fail until Story 3 — note that in the story, only type-check individual files if desired)
- Tests: `yarn test --roots='<rootDir>/src'` (non-required — ts-jest will fail to compile `AvoNetworkCallsHandlerLite.ts` due to the forward import of `AvoInspectorLite`, which does not exist until Story 3; existing tests in other directories should still pass if jest is configured to only compile tested files, but this cannot be guaranteed)
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` (non-required — same ts-jest forward-import constraint applies)

---

### Story 2 — Lite source files: AvoStreamIdLite + AvoDeduplicatorLite + AvoBatcherLite

**Approach:** general-purpose

**Goal:** Create the remaining three lite copy files that form the infrastructure layer.

**Files created (3):**
- `src/lite/AvoStreamIdLite.ts`
- `src/lite/AvoDeduplicatorLite.ts`
- `src/lite/AvoBatcherLite.ts`

**Key implementation notes:**
- `AvoStreamIdLite`: change `import { AvoInspector } from "./AvoInspector"` to `import { AvoInspectorLite as AvoInspector } from "./AvoInspectorLite"`; change `import AvoGuid from "./AvoGuid"` to `import AvoGuid from "../AvoGuid"`; alias `as AvoInspector` means zero body changes; add LITE COPY header
- `AvoDeduplicatorLite`: change `import { AvoSchemaParser } from "./AvoSchemaParser"` to `import { AvoSchemaParserLite as AvoSchemaParser } from "./AvoSchemaParserLite"`; change `import { deepEquals } from "./utils"` to `import { deepEquals } from "../utils"`; alias `as AvoSchemaParser` means zero body changes; add LITE COPY header
- `AvoBatcherLite`: change `import { AvoInspector } from "./AvoInspector"` to `import { AvoInspectorLite as AvoInspector } from "./AvoInspectorLite"`; change import of `AvoNetworkCallsHandler` types to from `./AvoNetworkCallsHandlerLite`; change `import type { EventSpecMetadata } from "./eventSpec/AvoEventSpecFetchTypes"` to `import type { EventSpecMetadata } from "../eventSpec/AvoEventSpecFetchTypes"`; keep `eventSpecMetadata?` in `AvoBatcherType` interface; add LITE COPY header
- Build still not expected to fully pass (AvoInspectorLite not yet created)

**Dependencies:** Story 1

**Quality checks:**
- Build: `yarn build` (note: will fail until Story 3 completes; run only to verify no regression in existing output)
- Tests: `yarn test --roots='<rootDir>/src'` (non-required — ts-jest will fail to compile lite files that forward-import `AvoInspectorLite`, which does not exist until Story 3)
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'` (non-required — same ts-jest forward-import constraint applies)

---

### Story 3 — AvoInspectorLite + index + tsconfig.lite.json + package.json

**Approach:** general-purpose

**Goal:** Create `AvoInspectorLite.ts` and `src/lite/index.ts`, add `tsconfig.lite.json`, and update `package.json` with the exports map and files field. After this story, `yarn build` must pass cleanly.

**Files created (3):**
- `src/lite/AvoInspectorLite.ts`
- `src/lite/index.ts`
- `tsconfig.lite.json`

**Files modified (2):**
- `package.json` — add `"exports"` map, `"files"` field, update `"build"` script, add `"check:lite-size"` script, add `"verify:lite-sync"` script

**Key implementation notes for `AvoInspectorLite.ts`:**
- Imports: `AvoInspectorEnv` from `"../AvoInspectorEnv"`; `AvoSchemaParserLite` from `"./AvoSchemaParserLite"`; `AvoBatcherLite as AvoBatcher` from `"./AvoBatcherLite"`; `AvoNetworkCallsHandlerLite as AvoNetworkCallsHandler, type EventProperty` from `"./AvoNetworkCallsHandlerLite"`; `AvoStorage` from `"../AvoStorage"`; `AvoDeduplicatorLite as AvoDeduplicator` from `"./AvoDeduplicatorLite"`; `AvoStreamIdLite as AvoStreamId` from `"./AvoStreamIdLite"`; `isValueEmpty` from `"../utils"`; `libVersion = require("../../package.json").version`
- Constructor options: `{ apiKey, env, version, appName?, suffix? }` — no `publicEncryptionKey`
- Constructor body: identical to full except: no `this.publicEncryptionKey = ...`; no `this.streamId = AvoStreamId.streamId`; no `if (this.streamId) { ... }` eventSpec init block; pass `undefined` as 6th arg to `new AvoNetworkCallsHandlerLite(..., undefined)`
- Static fields: `avoStorage`, `_batchSize` (get+set), `_batchFlushSeconds` (get only — no static setter), `_shouldLog` (get+set), `_networkTimeout` (get+set) — exactly mirroring the full class
- `trackSchemaFromEvent`: no `fetchAndValidateEvent` call, no `validationResult` branch; always calls `trackSchemaInternal` directly
- `_avoFunctionTrackSchemaFromEvent`: mirrors `trackSchemaFromEvent` but passes `fromAvoFunction: true` and forwards `eventId`/`eventHash` to `trackSchemaInternal`
- `trackSchema`: identical to full but without `await this.fetchEventSpecIfNeeded(eventName)` line
- `extractSchema`: calls `AvoSchemaParserLite.extractSchema(eventProperties)` — no key, no env

**`tsconfig.lite.json`:**
```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": { "outDir": "./dist" },
  "include": [
    "src/lite/**/*",
    "src/AvoInspectorEnv.ts",
    "src/AvoStorage.ts",
    "src/AvoGuid.ts",
    "src/utils.ts",
    "src/eventSpec/AvoEventSpecFetchTypes.ts"
  ]
}
```

**`package.json` changes:**
- Add `"files": ["dist", "bin"]`
- Add `"exports"` map with `.` and `./lite` conditions, each with `types` and `default`
- Update `"build"` to: `"tsc --emitDeclarationOnly && tsc --project tsconfig.lite.json --noEmit && webpack --config webpack.config.js"`
- Add `"check:lite-size": "node scripts/check-lite-size.js"`
- Add `"verify:lite-sync": "bash scripts/verify-lite-sync.sh"`
- Update `"prepublishOnly"` to append `&& yarn check:lite-size && yarn verify:lite-sync` so the size gate and drift check are publish-blocking

**Acceptance verification after this story:**
- `yarn build` passes
- `ls dist/lite/index.js dist/lite/index.d.ts` succeeds
- `grep -r "AvoEncryption" dist/lite/` returns no matches
- `grep -r "AvoEventSpecFetcher" dist/lite/` returns no matches
- `grep -r "noble" dist/lite/` returns no matches

**Dependencies:** Stories 1, 2

**Quality checks:**
- Build: `yarn build` (must pass)
- Tests: `yarn test --roots='<rootDir>/src'`
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'`

---

### Story 4 — Tests for lite build

**Approach:** general-purpose

**Goal:** Add Jest tests for `AvoInspectorLite` and `AvoSchemaParserLite` covering all acceptance criteria test categories.

**Files created (2):**
- `src/__tests__/AvoInspectorLite_test.ts`
- `src/__tests__/AvoSchemaParserLite_test.ts`

**`AvoInspectorLite_test.ts` test cases:**
1. Constructor accepts `apiKey`, `env`, `version`, `appName`, `suffix`
2. `publicEncryptionKey` causes TypeScript compile error (`@ts-expect-error`)
3. `trackSchemaFromEvent` returns correct schema with no `encryptedPropertyValue`
4. `trackSchemaFromEvent` returns `[]` for deduplicated events
5. `trackSchema` completes without throwing
6. `extractSchema` returns correct schema without encryption
7. Static getters: `batchSize`, `batchFlushSeconds`, `shouldLog`, `networkTimeout` all readable
8. Static setters: `batchSize`, `shouldLog`, `networkTimeout` settable; `batchFlushSeconds` set only via `setBatchFlushSeconds(n)` instance method
9. Import from `src/lite/index.ts` exports `AvoInspector` and `AvoInspectorEnv`
10. Static isolation test: `AvoInspector.batchSize = 99` does NOT change `AvoInspectorLite.batchSize`

**`AvoSchemaParserLite_test.ts` test cases:**
1. Correct property types (string, int, float, boolean, null, list, object)
2. Never sets `encryptedPropertyValue` even if `publicEncryptionKey` is passed
3. Nested objects extracted correctly
4. Arrays/lists extracted correctly

**Pattern notes:**
- Follow existing test patterns from `src/__tests__/Parsing_test.ts` and `src/__tests__/AvoInspectorEventSpec_test.ts`
- Mock `XMLHttpRequest` for network calls (same pattern as existing tests)
- Use `src/__tests__/constants.ts` `defaultOptions` as the base

**Dependencies:** Story 3

**Quality checks:**
- Build: `yarn build`
- Tests: `yarn test --roots='<rootDir>/src'` (all new tests must pass, existing must still pass)
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'`

---

### Story 5 — Drift detection and size verification scripts

**Approach:** general-purpose

**Goal:** Add the CI scripts that enforce drift detection between lite copies and their full originals, and the automated 7 KB gzip size gate.

**Files created (3):**
- `scripts/verify-lite-sync.sh`
- `scripts/check-lite-size.js`
- `scripts/webpack.lite-size.config.js`

**Key implementation notes:**

`scripts/verify-lite-sync.sh`:
- Uses `PAIRS` array of 4 pairs (AvoNetworkCallsHandler, AvoBatcher, AvoStreamId, AvoDeduplicator with their Lite copies)
- Add comment above PAIRS explaining `AvoSchemaParserLite` is excluded because it removes entire methods (encryption), not just one import line — requires manual review
- Fails if any pair has more than 10 changed lines (unexpected drift)
- Script is `chmod +x` executable

`scripts/check-lite-size.js`:
- Before running webpack, call `fs.mkdirSync(path.resolve(__dirname, '../test-bundle-size/output'), { recursive: true })` to guard against a missing output directory on fresh clones (mirrors the defensive `mkdir -p test-bundle-size/output` in the existing `analyze.sh`)
- Runs `npx webpack --config scripts/webpack.lite-size.config.js`
- Then `npx terser test-bundle-size/output/bundle-lite.js --compress passes=2,unsafe=true --mangle` writing to `test-bundle-size/output/bundle-lite-terser.js`
- Gzips `bundle-lite-terser.js` and fails if > 7168 bytes

`scripts/webpack.lite-size.config.js`:
- Entry: `./dist/lite/index.js`
- Output: `bundle-lite.js` in `test-bundle-size/output/`
- Mode: production, no `ts-loader` (compiles from already-built JS), `libraryTarget: "umd"`, `splitChunks: false`

**Note:** The `scripts/` directory does not exist in the repo. Create it with `mkdir -p scripts` before writing files.

**Dependencies:** Story 3

**Quality checks:**
- Build: `yarn build`
- Tests: `yarn test --roots='<rootDir>/src'`
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'`
- Manual: `yarn verify:lite-sync` passes; `yarn check:lite-size` passes (confirms < 7 KB)

---

### Story 6 — Example apps: examples/lite-size-demos/

**Approach:** general-purpose

**Goal:** Add three self-contained example apps demonstrating the lite bundle with different toolchains. These serve as documentation and proof that the lite build works universally.

**Files created (11):**
- `examples/lite-size-demos/terser-only/entry.js`
- `examples/lite-size-demos/terser-only/build.sh`
- `examples/lite-size-demos/terser-only/README.md`
- `examples/lite-size-demos/webpack/entry.js`
- `examples/lite-size-demos/webpack/webpack.config.js`
- `examples/lite-size-demos/webpack/package.json` — declares `webpack` and `webpack-cli` as devDependencies so the example is self-contained
- `examples/lite-size-demos/webpack/README.md`
- `examples/lite-size-demos/rollup/entry.js`
- `examples/lite-size-demos/rollup/rollup.config.js`
- `examples/lite-size-demos/rollup/package.json` — declares `rollup`, `@rollup/plugin-node-resolve`, and `@rollup/plugin-commonjs` as devDependencies so the example is self-contained
- `examples/lite-size-demos/rollup/README.md`

**Key implementation notes:**
- `terser-only/entry.js`: `const { AvoInspector, AvoInspectorEnv } = require("avo-inspector/lite"); ...` — construct and call `trackSchemaFromEvent`
- `terser-only/build.sh`: esbuild bundles to `build/bundle.js`, terser minifies to `build/bundle.min.js`
- `webpack/entry.js`: ES module import from `"avo-inspector/lite"`
- `webpack/webpack.config.js`: production mode, UMD output
- `webpack/package.json`: include `webpack` and `webpack-cli` in `devDependencies`; a user running this example needs to `npm install` inside the directory first
- `rollup/entry.js`: ES module import from `"avo-inspector/lite"`
- `rollup/rollup.config.js`: uses `@rollup/plugin-node-resolve` and `@rollup/plugin-commonjs`
- `rollup/package.json`: include `rollup`, `@rollup/plugin-node-resolve`, and `@rollup/plugin-commonjs` in `devDependencies`; a user running this example needs to `npm install` inside the directory first
- Each `README.md` shows: exact import, exact build command, expected raw/minified/gzipped sizes (placeholder `~X KB` until built), and grep command proving no encryption code present
- Add `"examples/lite-size-demos/"` to the `"files"` array in `package.json` (update Story 3's `package.json` change if needed, or do it here)
- **Merge-conflict warning:** Story 6 and Story 3 both modify `package.json`. Although Story 6 depends on Story 3 (so they are logically sequenced), if both stories are executed in parallel by different agents they will produce a merge conflict on `package.json`. Story 6's `package.json` edit (adding `"examples/lite-size-demos/"` to `files`) must be applied only after Story 3's `package.json` changes are fully committed.

**Dependencies:** Story 3

**Quality checks:**
- Build: `yarn build`
- Tests: `yarn test --roots='<rootDir>/src'`
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'`

---

### Story 7 — Final verification pass

**Approach:** general-purpose

**Goal:** Run all acceptance criteria checks end-to-end, fix any issues found, and confirm the complete feature is ready for review.

**Files modified (0 expected — fixes only if needed):**
- Any file that needs correction based on end-to-end acceptance criteria checks

**Verification checklist:**
1. `yarn build` — succeeds, produces `dist/index.js` and `dist/lite/index.js`
2. `tsc --project tsconfig.lite.json --noEmit` — passes (already part of build)
3. `grep -r "AvoEncryption" dist/lite/` — no matches
4. `grep -r "AvoEventSpecFetcher" dist/lite/` — no matches
5. `grep -r "EventSpecCache" dist/lite/` — no matches
6. `grep -r "noble" dist/lite/` — no matches
7. `grep -r "safe-regex" dist/lite/` — no matches
8. `yarn test --roots='<rootDir>/src'` — all tests pass
9. `BROWSER=1 yarn test --roots='<rootDir>/src'` — all tests pass
10. `npm pack --dry-run | grep "dist/lite"` — includes `dist/lite/index.js` and `dist/lite/index.d.ts`
11. `yarn verify:lite-sync` — no drift detected
12. `yarn check:lite-size` — under 7168 bytes gzipped
13. Static isolation test passes (batchSize isolation)
14. All 24 spec acceptance criteria verified
15. AC 21 — `cd examples/lite-size-demos/terser-only && bash build.sh` runs successfully and `build/bundle.min.js` gzipped is under 7 KB
16. AC 22 — `cd examples/lite-size-demos/webpack && npm install && npx webpack` runs successfully and output gzipped is under 7 KB
17. AC 23 — `cd examples/lite-size-demos/rollup && npm install && npx rollup -c` runs successfully and output gzipped is under 7 KB

**Dependencies:** Stories 1, 2, 3, 4, 5, 6

**Quality checks:**
- Build: `yarn build`
- Tests: `yarn test --roots='<rootDir>/src'`
- Browser tests: `BROWSER=1 yarn test --roots='<rootDir>/src'`

---

## Story Dependency Graph

```
Story 1 (AvoSchemaParserLite + AvoNetworkCallsHandlerLite)
  |
Story 2 (AvoStreamIdLite + AvoDeduplicatorLite + AvoBatcherLite)
  |
Story 3 (AvoInspectorLite + index + tsconfig.lite.json + package.json)
  |---------------|---------------|
Story 4 (Tests) Story 5 (Scripts) Story 6 (Examples)
  |               |               |
  `---------------+---------------'
                  |
            Story 7 (Final verification)
```

Stories 4, 5, and 6 are independent of each other after Story 3 and can be worked in parallel.
