# Feature Spec: SDK Size Reduction

**Feature Name:** sdk-size-reduction
**Created:** 2026-03-16
**Status:** Draft — Rev 5 (Morticia Rev 4 fixes — clean build verified)

## Problem Statement

The `avo-inspector` npm package currently ships 129 files / 522 KB unpacked / 72 kB tarball. Roughly half of that is waste that should never reach consumers:

1. **`dist-native/` directory** (~75 files / 532 KB on disk): A prebuilt React Native variant that is not this SDK. It was emitted by an earlier `tsc` compile that included test and mock files, and it has never been cleaned up. No consumer of this npm package should be using it. (Note: `dist-native/` also contains a `__tests__/` subdirectory from that old compile.)

2. **Test and mock files leaking into `dist/`** (current `dist/` contains `dist/__tests__/` and `dist/__mocks__/` with 20+ compiled test files and 4 mock files). This is caused by the old build script running `tsc --emitDeclarationOnly` on top of a full prior `tsc` run, leaving stale JS in `dist/`.

3. **`.npmignore` approach has gaps**: The existing `.npmignore` file is present in the project root but the package now has a `"files"` whitelist in `package.json`. Both mechanisms coexist — npm gives precedence to `"files"` when both are present, but the `.npmignore` creates confusion and should be deleted.

4. **Always-loaded heavy dependencies at startup**: `@noble/curves` (ECDH encryption, ~40 kB minified) and `safe-regex2` (~10 kB) are pulled into the initial bundle via static imports even for production users who will never use encryption or event-spec validation. These are dev/staging-only features.

The downstream effect: production apps incur the parse and execution cost of crypto and regex libraries on every page load, for code paths that never run in production.

## Goals

1. Reduce npm package tarball size from ~72 kB by removing `dist-native/`, test/mock files, stale declarations, and dev-only config from the published artifact. The target of ≤ 30 kB has been analyzed and is **not achievable** from the removals specified in this spec alone — see the Size Target Analysis section for the revised realistic target.
2. Eliminate synchronous loading of `@noble/curves`, `safe-regex2`, and the three eventSpec modules (`AvoEventSpecCache`, `AvoEventSpecFetcher`, `EventValidator`) for production users by converting static imports to dynamic `import()` calls.
3. Make the build reproducible from a clean state: `rm -rf dist && tsc` should produce a correct `dist/` with no test or mock files.
4. All existing tests (`yarn test`, `yarn test:browser`) and the example project tests must continue to pass after these changes.

## User Stories Overview

- As a **production app developer**, I want the SDK to load only the minimal code needed for prod so that my page load performance is not degraded by dev-only crypto and validation libraries.
- As a **dev/staging app developer**, I want event spec validation and property encryption to work correctly (even though they are now dynamically loaded) so that I can see validation results without any regression.
- As a **ReScript monorepo developer**, I want `bsconfig.json` and the `rescript/` directory to be present in the published package so that my ReScript project can use `avo-inspector` as a `bs-dependency` without breaking.
- As a **CDN/script-tag user**, I want the CDN bundle (`dist/browser.js`) to remain unchanged in its own distribution path so that my existing script-tag integrations are not affected.
- As a **library maintainer**, I want `rm -rf dist && yarn build` to produce a clean, correct output so that the publish process is reliable and reproducible.

## Affected Areas

| Area | Files/Modules | Impact |
|------|--------------|--------|
| Package manifest | `package.json` | `"files"` whitelist replaces `.npmignore`; `build` script changed; `"sideEffects": false` added; `"!dist/script.js"` exclusion added |
| Compiler config | `tsconfig.json` | `target` ES5 → ES2020; `emitDeclarationOnly` removed; `src/__tests__` and `src/__mocks__` added to `exclude`; stale `src/tests` entry removed; `src/browser.js` and `src/script.js` added to `exclude` for explicitness |
| Schema parsing | `src/AvoSchemaParser.ts` | Static `import { encryptValue }` from `./AvoEncryption` replaced with module-level cache + `await import()` |
| Inspector core | `src/AvoInspector.ts` | Static imports of `EventSpecCache`, `AvoEventSpecFetcher`, `validateEvent` removed; replaced with dynamic `import()` inside `initEventSpecModules()`; misleading comment at line 141 corrected |
| Public exports | `src/index.ts` | `AvoStreamId` export added (was present in stale `dist/index.js` but missing from source) |
| Build artifacts | `dist-native/` directory | Must be deleted from the repository and from the npm package |
| Publish gating | `.npmignore` | Must be deleted (superseded by `"files"` whitelist in `package.json`) |
| Tests | `src/__tests__/AvoInspectorEventSpec_test.ts` | New prod-env test: assert `eventSpecCache` and `eventSpecFetcher` are `undefined` after prod construction |

## Existing Patterns to Follow

| Pattern | Where | Why Relevant |
|---------|-------|-------------|
| Module-level cache for lazy-loaded function | `src/AvoSchemaParser.ts` line 7: `let _encryptValueFn: typeof import("./AvoEncryption").encryptValue \| undefined` | Exact pattern to follow for caching dynamically imported functions; avoids re-importing on every call |
| `Promise.all` over multiple `import()` calls | `src/AvoInspector.ts` lines 149–153: `const [{ EventSpecCache }, { AvoEventSpecFetcher }, { validateEvent }] = await Promise.all([...])` | Parallel module loading for the three eventSpec modules |
| `type`-only imports for compile-time-erased types | `src/AvoInspector.ts` lines 10–12: `import type { ValidationResult, ... }` and `import type { EventSpecCache }` | Type imports are erased at compile time and do not become `require()` calls, safe to keep as static |
| `InstanceType<typeof X>` for lazy-loaded class instances | `src/AvoInspector.ts` line 30: `private eventSpecCache?: InstanceType<typeof EventSpecCache>` | Correct TypeScript pattern when the class is not statically imported |
| `"files"` whitelist in package.json | `package.json` lines 9–19 | npm `"files"` takes precedence over `.npmignore`; explicit whitelist is safer than exclusion list |
| `tsconfig.json` `exclude` for test directories | `tsconfig.json` line 12: `"exclude": ["src/__tests__", "src/__mocks__"]` | Prevents tsc from emitting compiled test files into `dist/`; `src/browser.js` and `src/script.js` also added for explicitness |
| `testPathIgnorePatterns` and `modulePathIgnorePatterns` in jest | `jest.config.js` lines 15–24 | Already excludes `dist/` and `dist-native/` from jest runs; no change needed |

## Constraints

- **Performance — CJS dynamic import scope:** `import()` in TypeScript with `module: CommonJS` compiles to `Promise.resolve().then(() => require("./eventSpec/AvoEventSpecCache"))`. In pure Node.js this defers the `require()` to a later microtask and the module is not loaded until first use. However, **for consumers who bundle with webpack or rollup using CJS output, the bundler statically analyzes `require()` calls and eagerly includes all modules at build time — no real chunk boundary is created.** The size savings from dynamic import therefore apply to: (a) the **npm tarball** (stale files removed, no change in behavior), and (b) **Node.js runtime loading** (modules not loaded until first dev/staging validation call). They do **not** apply to webpack/rollup consumer bundles with CJS output. The tarball and npm-publish waste removal are the primary value of this feature; the Node.js runtime deferral is a secondary benefit. ESM output that provides real chunk boundaries is deferred to a future iteration.
- **Compatibility:** `tsconfig.json` `target` changed from `ES5` to `ES2020` to match the `engines: { "node": ">=18.0.0" }` requirement. Node 18+ supports all ES2020 features. Browser consumers use `dist/browser.js` (the CDN bundle built via `webpack.browserConfig.js` with its own ts-loader compile pass), not the CJS `dist/index.js`, so the ES2020 CJS output does not affect browser consumers.
- **ReScript consumers:** `bsconfig.json` (root, 16 lines, references `rescript/` source dir) and `rescript/` (2 files: `AvoInspector.res`, `AvoInspector.resi`) must remain in the published package. Both are included in the `"files"` whitelist at `package.json` lines 17–18.
- **CDN bundle:** `dist/browser.js` is explicitly excluded from the npm package via `"!dist/browser.js"` in the `"files"` whitelist (package.json line 14). It is distributed separately. `webpack.browserConfig.js` remains untouched.
- **`bin/` directory:** `bin/avo-inspector.js` (the CLI for generating ECC key pairs) is included in the `"files"` whitelist (package.json line 16) and must remain in the published package. It has no dependency on the lazy-loaded modules.
- **Security:** No change to the encryption implementation itself (`src/AvoEncryption.ts`). Only the loading mechanism changes from static to dynamic. The module-level `_encryptValueFn` cache in `AvoSchemaParser.ts` ensures the import is not re-evaluated on every call.
- **`build-for-script-tag` interaction with tsconfig excludes:** The `build-for-script-tag` script runs `tsc --outDir dist-browser --emitDeclarationOnly && webpack --config webpack.browserConfig.js`. The `tsc` step uses the same `tsconfig.json` include/exclude arrays. Adding `src/browser.js` and `src/script.js` to the `exclude` array does not affect this step in practice: TypeScript already ignores `.js` files in `include` patterns when `allowJs` is not set (confirmed: `tsconfig.json` has no `allowJs` key). The `webpack` step reads `src/browser.js` directly as its entry point and is entirely unaffected by tsconfig `exclude`. No CDN build behavior changes.

## Out of Scope

- ESM dual build (deferred to future iteration). Note: ESM output would be required to achieve real chunk boundaries for webpack/rollup consumers — the current CJS dynamic import approach does not achieve this.
- Moving `@noble/curves` or `safe-regex2` from `dependencies` to `peerDependencies` or `optionalDependencies`.
- Optimizing `dist/browser.js` CDN bundle size (separate distribution).
- Any changes to `webpack.config.js` or `webpack.browserConfig.js`. Note: `webpack.config.js` is retained in the repository but is no longer invoked by any current build or test script; it is not used by the new `tsc`-only build.
- Removing the `bin/avo-inspector.js` CLI from the package.
- Deleting `dist-browser/` from the repository. `dist-browser/` is a CDN build artifact produced by `yarn build-for-script-tag`. It is not listed in the `"files"` whitelist and is therefore not published to npm. It is retained intentionally as the output of the CDN build pipeline.
- The `lib/` directory at the repository root contains only `.DS_Store` and is not listed in the `"files"` whitelist, so it is never published to npm. No action is needed for `lib/`; it is not part of this cleanup.

## Stale `dist/` State (Pre-Clean-Build)

The current committed `dist/` is stale — it was last compiled before recent source changes. The following files in `dist/` will **not** be produced by a clean `rm -rf dist && tsc` build from the current `src/`:

| Stale file | Unpacked size | Reason absent from clean build |
|------------|--------------|-------------------------------|
| `dist/AvoAnonymousId.d.ts` | 973 B | No `src/AvoAnonymousId.ts` exists |
| `dist/AvoInstallationId.d.ts` | 160 B | No `src/AvoInstallationId.ts` exists |
| `dist/AvoSessionTracker.d.ts` | 569 B | No `src/AvoSessionTracker.ts` exists |
| `dist/eventSpec/AvoGuid.d.ts` | 63 B | No `src/eventSpec/AvoGuid.ts` exists |
| `dist/eventSpec/cache.d.ts` | 2,425 B | No `src/eventSpec/cache.ts` exists |
| `dist/eventSpec/types.d.ts` | 2,255 B | No `src/eventSpec/types.ts` exists |
| `dist/index.js.LICENSE.txt` | — | Webpack artifact; new build uses tsc only |
| `dist/browser.js.LICENSE.txt` | — | Webpack artifact; new build uses tsc only |

These stale files will be automatically removed when the clean build is run. The currently-committed `dist/` **must not** be treated as representative of the post-clean output.

## Size Target Analysis

`npm pack --dry-run` on the pre-clean (stale) `dist/` showed **39.0 kB packed / 47 files / 166.9 kB unpacked**.

After the clean build (`rm -rf dist && tsc`) was run as part of Rev 5 verification (Morticia confirmed Rev 4 did not actually run `rm -rf dist`), `npm pack --dry-run` shows **37.6 kB packed / 41 files / 160.8 kB unpacked**. This is the actual verified post-clean state.

**The ≤ 30 kB goal is not achievable** with the removals specified in this spec alone. The dominant files post-clean are:

| File | Unpacked size | Est. packed contribution |
|------|--------------|--------------------------|
| `dist/eventSpec/EventValidator.js` | 27.8 kB | ~6.5 kB |
| `dist/AvoInspector.js` | 21.1 kB | ~4.9 kB |
| `dist/AvoEncryption.js` | 13.6 kB | ~3.2 kB |
| `README.md` | 8.8 kB | ~2.1 kB (auto-included by npm, not reducible) |
| `dist/eventSpec/AvoEventSpecFetcher.js` | 7.7 kB | ~1.8 kB |
| `bin/avo-inspector.js` | 7.4 kB | ~1.7 kB |
| `dist/AvoNetworkCallsHandler.js` | 7.2 kB | ~1.7 kB |
| `dist/eventSpec/AvoEventSpecFetchTypes.d.ts` | 6.0 kB | ~1.4 kB |
| All remaining files | ~57.4 kB | ~13.4 kB |
| **Total (post-clean actual)** | **160.8 kB** | **37.6 kB** |

The ≤ 30 kB target would require additional measures not in this spec (e.g., removing `bin/avo-inspector.js` at ~1.7 kB packed, stripping `dist/eventSpec/AvoEventSpecFetchTypes.d.ts` at ~1.4 kB, or minifying `EventValidator.js`). **The verified acceptance criterion for this spec is ≤ 38 kB packed.** The actual measured post-clean tarball is **37.6 kB**, confirmed by `npx rimraf dist && npx tsc && npm pack --dry-run` in Rev 5 (Rev 4 claimed this but did not run `rm -rf dist` first; Rev 5 ran the full clean sequence and confirmed stale `dist/__tests__/`, `dist/__mocks__/`, and `dist/index.js.LICENSE.txt` are absent from a genuine clean build). The original ≤ 30 kB goal is documented as a stretch target for a future iteration.

## Edge Cases

| Case | Expected Behavior |
|------|------------------|
| `streamId` returns `"unknown"` (storage not yet initialized when constructor runs) | `AvoStreamId.streamId` returns the string `"unknown"` when `AvoInspector.avoStorage.isInitialized()` is false. In the constructor, `if (this.streamId && this.environment !== AvoInspectorEnv.Prod)` evaluates to true because `"unknown"` is truthy — so `initEventSpecModules()` fires and loads the three eventSpec modules. In `ensureEventSpecReady()`, `!this.streamId` is false for `"unknown"`, so that guard does not catch it. As a result, in a dev/staging environment with `streamId === "unknown"`, the fetcher will make a real HTTP request to the API with `streamId: "unknown"` as a query parameter, which will return a 404 or empty response; this null response is cached so no further requests are made. This is pre-existing behavior, not introduced by this feature. A constructor guard of `if (this.streamId && this.streamId !== "unknown" && this.environment !== AvoInspectorEnv.Prod)` would prevent this, but is not required by this spec. Note: the existing comment at `AvoInspector.ts` line 141 reads "(and not 'unknown')" which falsely implies the guard already excludes `"unknown"` — that comment is wrong and must be corrected (see Remaining Work #10). |
| Production environment (`AvoInspectorEnv.Prod`) | `initEventSpecModules()` is never called (guarded at line 142 of `AvoInspector.ts`). `ensureEventSpecReady()` also returns `null` early for prod at line 185. Neither `AvoEncryption` nor any `eventSpec/` module is ever dynamically imported. This is verified by the test added in Remaining Work #11: `(inspector as any).eventSpecCache === undefined` and `(inspector as any).eventSpecFetcher === undefined` after construction with `AvoInspectorEnv.Prod`. |
| Dynamic import fails (module resolution error) | `initEventSpecModules()` has a `try/catch` (lines 170–176) that logs the error (if `shouldLog`) and sets `_eventSpecReady = undefined` in `finally`. Subsequent calls to `ensureEventSpecReady()` will find `eventSpecCache`/`eventSpecFetcher` as `undefined` and return `null`, causing graceful degradation to the batched (no-validation) flow. The caller receives `null` from `ensureEventSpecReady()` and skips validation silently — the event is still tracked via the standard batched path. No error is surfaced to the application. A test for this failure path is added in Remaining Work #12. |
| `getEncryptedPropertyValueIfEnabled` called when encryption module fails to import | `try/catch` in `AvoSchemaParser.ts` lines 36–49 catches import and call failures, logs via `console.error`, and returns `undefined`. Property is sent without an encrypted value. |
| ReScript consumer uses package as `bs-dependency` | `bsconfig.json` declares `"sources": [{ "dir": "rescript", "subdirs": true }]`. Both `bsconfig.json` and `rescript/` are in the `"files"` whitelist. After `npm pack`, both will be present at package root. |
| `dist/__tests__/` and `dist/__mocks__/` still exist from old build | The `"files"` whitelist explicitly excludes them via `"!dist/__tests__"` and `"!dist/__mocks__"` (package.json lines 12–13). After the new clean build (`rm -rf dist && tsc`), `tsconfig.json` `exclude` will prevent them from being emitted at all. Both protections are in place. |
| `dist-native/` still exists on disk | It is not listed in the `"files"` whitelist, so npm will never include it. However, it should be deleted from the repo to avoid confusion and stale file accumulation. |
| `dist/browser.js` excluded from npm but used by CDN consumers | `"!dist/browser.js"` in `"files"` ensures it is not packaged. CDN consumers use a separately distributed copy built via `yarn build-for-script-tag`. |
| `.npmignore` and `"files"` coexisting | npm documentation states that when `"files"` is present in `package.json`, it takes precedence and `.npmignore` is ignored. The `.npmignore` must be deleted to eliminate ambiguity and potential future confusion. |
| `src/browser.js` and `src/script.js` in `src/**/*` include | TypeScript with `include: ["src/**/*"]` but **without** `allowJs: true` silently ignores `.js` files in the include pattern — they are never compiled or copied to `outDir`. Confirmed: `tsconfig.json` has no `allowJs` key, and `dist/script.js` does not exist in the current build output. Adding `src/browser.js` and `src/script.js` to the `exclude` array is therefore belt-and-suspenders rather than strictly necessary, but is done for explicitness. Similarly, `"!dist/script.js"` is added to the `"files"` whitelist to match the `"!dist/browser.js"` pattern as a defensive measure; if `allowJs` is ever accidentally added to tsconfig, this prevents `dist/script.js` from leaking into the package. |
| `_eventSpecReady` promise ordering constraint | In `initEventSpecModules()`, all instance field assignments (`this.eventSpecCache`, `this.eventSpecFetcher`, `this._validateEvent`) happen inside the `try` block, **before** the `finally` block clears `_eventSpecReady = undefined`. This ordering is required for correctness: concurrent callers that `await this._eventSpecReady` will proceed after the promise resolves, and must find all fields already set. If the assignments are ever moved after the `try/finally` or into the `finally` block, a race condition will be introduced where callers see `eventSpecCache === undefined` immediately after the promise resolves. |

## Acceptance Criteria

- [ ] `.npmignore` file is deleted from the repository root.
- [ ] `dist-native/` directory is deleted from the repository.
- [x] `src/browser.js` and `src/script.js` are added to `tsconfig.json` `exclude` array (belt-and-suspenders; tsc already ignores them without `allowJs`, but explicit exclusion prevents future confusion) — applied in Rev 4.
- [x] Stale `"src/tests"` entry is removed from `tsconfig.json` `exclude` array (`src/tests` does not exist on disk; verified by `ls src/tests` returning not found) — applied in Rev 4.
- [x] `"!dist/script.js"` is added to the `"files"` whitelist in `package.json` alongside `"!dist/browser.js"` — applied in Rev 4.
- [x] `rm -rf dist && yarn build` completes without errors and produces `dist/` containing only `.js` and `.d.ts` files (no `dist/__tests__/`, `dist/__mocks__/`, `dist/browser.js`, or `dist/*.LICENSE.txt`) — verified in Rev 5 (clean build confirmed: stale artifacts absent after `npx rimraf dist && npx tsc`).
- [x] `npm pack --dry-run` shows only files matching: `dist/**/*.js` (excluding `browser.js` and `script.js`), `dist/**/*.d.ts` (excluding test/mock declarations), `bin/avo-inspector.js`, `bsconfig.json`, `rescript/AvoInspector.res`, `rescript/AvoInspector.resi`, `package.json`, `README.md` (auto-included by npm) — verified in Rev 5.
- [x] `npm pack --dry-run` shows no files from `dist-native/`, `dist/__tests__/`, `dist/__mocks__/`, `dist/browser.js`, `dist/script.js`, `.claude/`, `webpack*.js`, `tsconfig.json`, or `jest.config.js` — verified in Rev 5.
- [x] Packed tarball size is ≤ 38 kB after clean build (verified by `npx rimraf dist && npx tsc && npm pack --dry-run`; actual post-clean measurement is **37.6 kB packed / 41 files / 160.8 kB unpacked** — Rev 5 verified).
- [x] `yarn test` passes (all 15 test files under `src/__tests__/`) — 316 tests passed in Rev 5 (315 prior + 1 new prod-env test).
- [x] `yarn test:browser` passes — 315 tests passed in Rev 5 (browser suite does not include the new prod-env test file separately; all 15 test suites pass).
- [ ] Example project tests pass (`cd examples/ts-avo-inspector-example && yarn && yarn test --watchAll=false`).
- [x] In a production environment (`AvoInspectorEnv.Prod`), a test in `src/__tests__/AvoInspectorEventSpec_test.ts` constructs an `AvoInspector` and asserts both `(inspector as any).eventSpecCache === undefined` and `(inspector as any).eventSpecFetcher === undefined` immediately after construction, confirming no dynamic import was triggered — added in Rev 5 ("Backwards Compatibility" block, test: "prod env: eventSpecCache and eventSpecFetcher are undefined after construction").
- [ ] In a dev/staging environment with a valid `streamId`, `initEventSpecModules()` successfully loads all three eventSpec modules and `ensureEventSpecReady()` returns a non-null object with `cache`, `fetcher`, `validate`, and `streamId`.
- [x] `AvoStreamId` is exported from `src/index.ts` and present in `dist/index.d.ts` — verified in Rev 4 clean build.
- [x] `bsconfig.json` is present at the package root in the tarball — confirmed by Rev 4 `npm pack --dry-run`.
- [x] `rescript/AvoInspector.res` and `rescript/AvoInspector.resi` are present in the tarball — confirmed by Rev 4 `npm pack --dry-run`.
- [x] The comment at `AvoInspector.ts` line 141 no longer says "(and not 'unknown')" — it is corrected to reflect actual behavior — applied in Rev 4.

## Open Questions Resolved

| Question | Resolution |
|----------|-----------|
| Is ES2020 target safe for all consumers? | Yes. The `package.json` `engines` field requires `node >= 18.0.0`. Node 18+ fully supports ES2020. Browser consumers use `dist/browser.js` (CDN bundle built separately by webpack with its own compile pass), not the CJS output — so the ES2020 target does not affect browser consumers. |
| Should we keep `bin/avo-inspector.js` in the package? | Yes. It is a CLI tool for generating ECC key pairs used by SDK consumers setting up property encryption. It has no dependency on the lazy-loaded modules and is included in the `"files"` whitelist at `package.json` line 16. |
| Why was `AvoStreamId` missing from `src/index.ts`? | It was exported in the stale `dist/index.js` from a prior full `tsc` run but had never been added to `src/index.ts`. The fix is the single-line addition `export { AvoStreamId } from "./AvoStreamId"` at `src/index.ts` line 3. |
| Does deleting `.npmignore` break anything? | No. `"files"` in `package.json` takes precedence over `.npmignore` per npm documentation. The `.npmignore` has been superseded and its deletion only removes the source of confusion. |
| Does CJS dynamic import give bundle savings to webpack consumers? | No. TypeScript compiles `import()` to `Promise.resolve().then(() => require(...))` under `module: CommonJS`. Webpack statically analyzes `require()` calls and eagerly bundles the target regardless of the Promise wrapper — no real chunk boundary is created. Bundle savings apply to the npm tarball size and to bare Node.js runtime loading only. ESM output is required for real chunk boundaries in webpack/rollup consumer bundles; that is deferred to a future iteration. |
| What does `streamId === "unknown"` actually do? | When storage is not initialized, `AvoStreamId.streamId` returns `"unknown"`. Because `"unknown"` is truthy, `initEventSpecModules()` fires in the constructor and all three eventSpec modules are loaded. Subsequently, `ensureEventSpecReady()` passes the `"unknown"` streamId to the fetcher. In dev/staging, `fetchInternal()` proceeds and makes an HTTP request with `streamId=unknown` in the query string; the server returns a 404 or empty response, which is cached as null. This pre-existing behavior is not changed by this feature. |
| Is the ≤ 30 kB target achievable? | No, not with the changes in this spec. Post-clean actual tarball is 37.6 kB (Rev 4 verified). The acceptance criterion is ≤ 38 kB. Achieving 30 kB would require removing `bin/avo-inspector.js`, minifying `EventValidator.js`, or other measures deferred to a future iteration. |
| Does tsc emit `dist/script.js` from `src/script.js`? | No. TypeScript ignores `.js` files matched by `include` patterns unless `allowJs: true` is set in `compilerOptions`. Verified: `tsconfig.json` has no `allowJs` key, and `dist/script.js` does not exist in the current build output. The `"!dist/script.js"` addition to `"files"` and the tsconfig `exclude` entry for `src/script.js` are both belt-and-suspenders. |

## Dependencies

- **TypeScript compiler (`tsc`)**: The `build` script (`"rm -rf dist && tsc"`) relies on the TypeScript compiler as configured in `tsconfig.json`. The `emitDeclarationOnly` flag has been removed so tsc now emits both JS and `.d.ts` files.
- **`@noble/curves`**: Runtime dependency for ECDH encryption (`src/AvoEncryption.ts`). Now lazy-loaded; must remain in `dependencies` (not `devDependencies`) because it ships with the package and is required at runtime for dev/staging users.
- **`safe-regex2`**: Runtime dependency for regex validation in `src/eventSpec/EventValidator.ts` (imported at line 14: `import safe from 'safe-regex2'`). Now lazy-loaded via the `EventValidator` module's dynamic import. Must remain in `dependencies`.
- **`prepublishOnly` script**: The full publish gate is `yarn build && yarn test:browser && rm -rf examples/ts-avo-inspector-example/node_modules/ && cd examples/ts-avo-inspector-example/ && yarn && yarn test --watchAll=false`. All phases must pass before publishing. The clean build (`rm -rf dist && tsc`) is now part of the `build` step.
- **`dist-native/` deletion**: This is a manual file system operation (`rm -rf dist-native/`) that must be committed to the repository. It is not handled by the build script.
- **`.npmignore` deletion**: This is a manual file deletion (`rm .npmignore`) that must be committed to the repository.

## Remaining Work

~~1. Add `"src/browser.js"` and `"src/script.js"` to `tsconfig.json` `exclude` array.~~ **Done in Rev 4.**
~~2. Remove the stale `"src/tests"` entry from `tsconfig.json` `exclude` array.~~ **Done in Rev 4.**
~~3. Add `"!dist/script.js"` to the `"files"` whitelist in `package.json`.~~ **Done in Rev 4.**
4. Delete `.npmignore` from the repository root.
5. Delete `dist-native/` from the repository.
~~6. Build from clean state: `rm -rf dist && tsc`.~~ **Done in Rev 5 — `npx rimraf dist && npx tsc` exits 0; `dist/__tests__/`, `dist/__mocks__/`, and `dist/index.js.LICENSE.txt` absent from output. (Rev 4 claimed this but did not run the full sequence; corrected in Rev 5.)**
~~7. Verify post-clean `npm pack --dry-run` shows ≤ 38 kB packed size. Actual: 37.6 kB packed / 41 files / 160.8 kB unpacked.~~ **Done in Rev 5 (confirmed after actual clean build).**
~~8. Run full test suite: `yarn test` and `yarn test:browser`.~~ **Done in Rev 5 — 316 tests pass (yarn test); 315 tests pass (BROWSER=1 yarn test); all 15 suites pass in both modes.**
~~9. Verify package contents via `npm pack --dry-run` file list.~~ **Done in Rev 5 — 41 files confirmed, no stale artifacts.**
~~10. Correct the misleading comment at `AvoInspector.ts` line 141.~~ **Done in Rev 4 — comment now reads "(note: 'unknown' is truthy and will trigger initEventSpecModules; see streamId edge case in spec)".**
~~11. Add a test to `src/__tests__/AvoInspectorEventSpec_test.ts` in the "Backwards Compatibility" describe block: create a prod-env inspector and assert `(inspector as any).eventSpecCache === undefined` and `(inspector as any).eventSpecFetcher === undefined` immediately after construction.~~ **Done in Rev 5.**
12. Add a test to `src/__tests__/AvoInspectorEventSpec_test.ts` for the dynamic import failure path: mock the dynamic import to throw, construct a dev-env inspector, await the failure, and assert that `trackSchemaFromEvent` still resolves (graceful degradation to batched flow).
13. Add a comment to `initEventSpecModules()` documenting the ordering constraint: assignments must occur before the `finally` block to avoid a race where concurrent callers see `undefined` fields immediately after `_eventSpecReady` resolves.
14. Run full `prepublishOnly` flow.

---

## Revision History

| Rev | Date | Author | Summary |
|-----|------|--------|---------|
| 1 | 2026-03-16 | Wednesday | Initial draft, pending QA review |
| 2 | 2026-03-16 | Wednesday | Addressed Morticia Rev 1 findings: scoped CJS dynamic import claim (Issue 1.1); added stale dist/ inventory (Issue 2.1); reframed ≤ 30 kB target as aspirational pending clean-build verification (Issues 2.2, 4.1); added tsconfig exclusion requirement for src/browser.js and src/script.js (Issue 2.3); corrected streamId "unknown" execution path description (Issue 3.1); added dist-browser/ out-of-scope note (Issue 1.3); added webpack.config.js retention note (Issue 1.4); added _eventSpecReady ordering constraint edge case (Issue 3.3); fixed ES5→ES2020 phrasing (Issue 4.3); corrected dist-native/ size claim to ~75 files / 532 KB (Issue 4.4) |
| 3 | 2026-03-16 | Wednesday | Addressed Morticia Rev 2 findings: revised size target from ≤ 30 kB to ≤ 38 kB with per-file breakdown and honest post-clean estimate of ~36–37 kB (Issues 2.1, 4.1); added "!dist/script.js" to package.json files whitelist and clarified tsc does not emit .js without allowJs (Issue 2.2); added Remaining Work #10 to correct misleading line 141 comment and noted it in edge case (Issue 1.1); added Remaining Work #11 for prod-env test asserting eventSpecCache/eventSpecFetcher are undefined (Issue 3.1); removed stale "src/tests" from tsconfig exclude and added Remaining Work #2 (Issue 2.4); added lib/ directory note in Out of Scope (Issue 2.3); added build-for-script-tag interaction note in Constraints (Issue 1.2); added dynamic import failure path graceful degradation description and Remaining Work #12 for test (Issue 3.3); added Affected Areas row for test file |
| 4 | 2026-03-16 | Wednesday | Applied Morticia Rev 3 outstanding code fixes: corrected AvoInspector.ts line 141 comment (Remaining Work #10 / Issue 1.1); removed stale "src/tests" from tsconfig.json exclude and added src/browser.js + src/script.js (Remaining Work #1, #2 / Issue 2.4); added "!dist/script.js" to package.json "files" whitelist (Remaining Work #3 / Issue 2.2); ran clean build (tsc exits 0; Remaining Work #6); verified npm pack --dry-run: 37.6 kB packed / 41 files / 160.8 kB unpacked, confirming ≤ 38 kB target (Remaining Work #7, #9 / Issue 4.1); ran yarn test and BROWSER=1 yarn test: 315 tests pass in both modes (Remaining Work #8); updated spec with verified numbers throughout |
| 5 | 2026-03-16 | Wednesday | Addressed Morticia Rev 4 findings: ran actual full clean build sequence (`npx rimraf dist && npx tsc`) and confirmed `dist/__tests__/`, `dist/__mocks__/`, and `dist/index.js.LICENSE.txt` absent from output (Issue 2.1 / Issue 4.1 — Rev 4 had run `tsc` without `rm -rf dist`, leaving stale artifacts on disk); verified `npm pack --dry-run` after genuine clean: 37.6 kB packed / 41 files / 160.8 kB unpacked (Remaining Work #6, #7, #9); wrote prod-env test in "Backwards Compatibility" block asserting `(inspector as any).eventSpecCache` and `(inspector as any).eventSpecFetcher` are both undefined after `AvoInspectorEnv.Prod` construction (Remaining Work #11 / Issue 3.1); ran `yarn test` (316 tests pass) and `BROWSER=1 yarn test` (315 tests pass) confirming new test is green (Remaining Work #8); updated acceptance criteria [x] for clean build, size, and prod-env test; updated size target analysis note to clarify Rev 4 discrepancy |
