|
| 1 | +# Architecture Guide |
| 2 | + |
| 3 | +This document explains how `@startdoing/tc` is structured, the algorithms it uses, and the design patterns behind each layer. It is written for developers who want to learn from or extend the codebase. |
| 4 | + |
| 5 | +## What this project does |
| 6 | + |
| 7 | +The library turns **UTF-8 text** or **entire folder trees** into a single pasteable string (Base64 or Z85), and back again. The pipeline is: |
| 8 | + |
| 9 | +``` |
| 10 | +Input → tag → Brotli (max quality) → Base64 or Z85 → pasteable string |
| 11 | +``` |
| 12 | + |
| 13 | +Folder archives use a custom binary format before compression. Large outputs can be split into numbered part files for chat paste limits. |
| 14 | + |
| 15 | +## Source layout (domain modules) |
| 16 | + |
| 17 | +``` |
| 18 | +src/ |
| 19 | +├── index.ts # Public API barrel (npm package surface) |
| 20 | +├── types.ts # Shared types (Encoding) |
| 21 | +├── encoding/ # Base64 and Z85 text encodings |
| 22 | +│ ├── base64.ts |
| 23 | +│ ├── z85.ts |
| 24 | +│ └── index.ts # encodeBuffer / decodeBuffer facade |
| 25 | +├── compression/ |
| 26 | +│ └── brotli.ts # Brotli sync + streaming wrappers |
| 27 | +├── payload/ |
| 28 | +│ └── tags.ts # Type tags, wrap/decompress payload |
| 29 | +├── archive/ # Custom folder archive format |
| 30 | +│ ├── types.ts |
| 31 | +│ ├── format.ts # Serialize / deserialize / stream writes |
| 32 | +│ ├── collect.ts # In-memory directory walk |
| 33 | +│ └── unpack.ts # Restore archive to disk |
| 34 | +├── split/ |
| 35 | +│ └── parts.ts # Split output into numbered files |
| 36 | +├── fs/ |
| 37 | +│ └── paths.ts # Path validation and file reading |
| 38 | +├── api/ |
| 39 | +│ ├── text.ts # compress() / decompress() |
| 40 | +│ └── folder.ts # compressFolder() / decompressToPath() |
| 41 | +├── streaming/ |
| 42 | +│ └── folder.ts # Large-folder pipeline (disk-backed) |
| 43 | +└── cli/ # Terminal interface |
| 44 | + ├── main.ts |
| 45 | + ├── args.ts |
| 46 | + ├── paths.ts |
| 47 | + ├── analytics.ts |
| 48 | + ├── output.ts |
| 49 | + ├── usage.ts |
| 50 | + └── commands/ |
| 51 | + ├── compress.ts |
| 52 | + └── decompress.ts |
| 53 | +``` |
| 54 | + |
| 55 | +Each file has a `@module` header comment describing its role, algorithms, and patterns. |
| 56 | + |
| 57 | +## End-to-end data flow |
| 58 | + |
| 59 | +### Text compression |
| 60 | + |
| 61 | +```mermaid |
| 62 | +flowchart LR |
| 63 | + A[UTF-8 text] --> B[wrapPayload TAG_TEXT] |
| 64 | + B --> C[Brotli compress] |
| 65 | + C --> D[Base64 or Z85 encode] |
| 66 | + D --> E[Pasteable string] |
| 67 | +``` |
| 68 | + |
| 69 | +### Folder compression (in-memory) |
| 70 | + |
| 71 | +```mermaid |
| 72 | +flowchart LR |
| 73 | + A[Directory tree] --> B[collectEntries walk] |
| 74 | + B --> C[serializeArchive] |
| 75 | + C --> D[wrapPayload TAG_FOLDER] |
| 76 | + D --> E[Brotli compress] |
| 77 | + E --> F[Base64 or Z85 encode] |
| 78 | + F --> G[Pasteable string] |
| 79 | +``` |
| 80 | + |
| 81 | +### Folder compression (streaming — CLI default for folders) |
| 82 | + |
| 83 | +```mermaid |
| 84 | +flowchart LR |
| 85 | + A[Directory tree] --> B[buildArchiveFile temp] |
| 86 | + B --> C[stream Brotli temp] |
| 87 | + C --> D[stream encode output.txt] |
| 88 | + D --> E[optional split parts] |
| 89 | +``` |
| 90 | + |
| 91 | +Peak memory stays bounded by chunk buffers (1–3 MiB), not total archive size. |
| 92 | + |
| 93 | +## Applied mechanisms and patterns |
| 94 | + |
| 95 | +### 1. Type tag (discriminated union) |
| 96 | + |
| 97 | +After Brotli decompression, the **first byte** identifies the payload: |
| 98 | + |
| 99 | +| Tag byte | Constant | Meaning | |
| 100 | +|----------|--------------|----------------| |
| 101 | +| `0x01` | `TAG_TEXT` | UTF-8 string | |
| 102 | +| `0x02` | `TAG_FOLDER` | Binary archive | |
| 103 | + |
| 104 | +This lets one encoded string represent either text or a folder without external metadata — similar to MIME types or protobuf field tags. |
| 105 | + |
| 106 | +**Module:** `src/payload/tags.ts` |
| 107 | + |
| 108 | +### 2. Strategy pattern (encoding selection) |
| 109 | + |
| 110 | +`encodeBuffer(buffer, encoding)` and `decodeBuffer(str, encoding)` dispatch to Base64 or Z85 based on the `Encoding` type (`64 | 85`). Callers never branch on encoding elsewhere. |
| 111 | + |
| 112 | +**Module:** `src/encoding/index.ts` |
| 113 | + |
| 114 | +### 3. Brotli maximum-quality compression |
| 115 | + |
| 116 | +We always use `BROTLI_MAX_QUALITY` (11) and `BROTLI_MAX_WINDOW_BITS` because the tool optimises for **smallest output**, not speed. `SIZE_HINT` is set to input length so the encoder can pre-allocate. |
| 117 | + |
| 118 | +**Module:** `src/compression/brotli.ts` |
| 119 | + |
| 120 | +### 4. Custom flat archive format |
| 121 | + |
| 122 | +Instead of ZIP or tar, we use a minimal length-prefixed binary format: |
| 123 | + |
| 124 | +``` |
| 125 | +directory: [0x44] [pathLen u32le] [path utf-8] |
| 126 | +file: [0x46] [pathLen u32le] [path] [contentLen u32le] [content] |
| 127 | +``` |
| 128 | + |
| 129 | +Design choices: |
| 130 | + |
| 131 | +- **Flat list** — produced by depth-first walk; easy to stream to disk. |
| 132 | +- **Sorted children** — deterministic output for testing. |
| 133 | +- **No metadata** — permissions and timestamps dropped for simplicity. |
| 134 | +- **Path validation** — rejects `..` and absolute paths (zip-slip prevention). |
| 135 | + |
| 136 | +**Modules:** `src/archive/format.ts`, `src/archive/collect.ts`, `src/archive/unpack.ts` |
| 137 | + |
| 138 | +### 5. Z85 Base85 encoding |
| 139 | + |
| 140 | +Z85 (ZeroMQ RFC 32) maps 4 bytes → 5 printable characters using a base-85 alphabet chosen to avoid quotes and backslashes. It is ~8% more compact than Base64. |
| 141 | + |
| 142 | +Padding: a 1-byte prefix stores how many zero bytes were appended so arbitrary-length data round-trips. |
| 143 | + |
| 144 | +**Module:** `src/encoding/z85.ts` |
| 145 | + |
| 146 | +### 6. Split-file output |
| 147 | + |
| 148 | +When encoded output exceeds 30,000 characters (or a user `-s` limit), it is split into numbered parts: |
| 149 | + |
| 150 | +``` |
| 151 | +output.txt → output.1.txt, output.2.txt, … |
| 152 | +``` |
| 153 | + |
| 154 | +Zero-padding width matches total part count so lexical sort equals numeric sort. |
| 155 | + |
| 156 | +**Module:** `src/split/parts.ts` |
| 157 | + |
| 158 | +### 7. Staged streaming pipeline |
| 159 | + |
| 160 | +For large folders, each stage writes to a temp file and the next stage reads from it: |
| 161 | + |
| 162 | +``` |
| 163 | +walk → archive.bin → compressed.bin → output.txt |
| 164 | +``` |
| 165 | + |
| 166 | +Pattern: **bounded memory via temp files** instead of loading everything into RAM. |
| 167 | + |
| 168 | +**Module:** `src/streaming/folder.ts` |
| 169 | + |
| 170 | +## Dependency graph |
| 171 | + |
| 172 | +```mermaid |
| 173 | +flowchart TB |
| 174 | + subgraph public [Public API] |
| 175 | + index[index.ts] |
| 176 | + end |
| 177 | +
|
| 178 | + subgraph api [API layer] |
| 179 | + text[api/text.ts] |
| 180 | + folder[api/folder.ts] |
| 181 | + end |
| 182 | +
|
| 183 | + subgraph core [Core domains] |
| 184 | + tags[payload/tags.ts] |
| 185 | + brotli[compression/brotli.ts] |
| 186 | + encoding[encoding/] |
| 187 | + archive[archive/] |
| 188 | + split[split/parts.ts] |
| 189 | + fspaths[fs/paths.ts] |
| 190 | + end |
| 191 | +
|
| 192 | + subgraph internal [Internal only] |
| 193 | + streaming[streaming/folder.ts] |
| 194 | + cli[cli/] |
| 195 | + end |
| 196 | +
|
| 197 | + index --> text |
| 198 | + index --> folder |
| 199 | + index --> tags |
| 200 | + index --> archive |
| 201 | + index --> split |
| 202 | + index --> fspaths |
| 203 | +
|
| 204 | + text --> tags |
| 205 | + folder --> archive |
| 206 | + folder --> tags |
| 207 | +
|
| 208 | + tags --> brotli |
| 209 | + tags --> encoding |
| 210 | +
|
| 211 | + streaming --> archive |
| 212 | + streaming --> brotli |
| 213 | + streaming --> encoding |
| 214 | + streaming --> split |
| 215 | + streaming --> tags |
| 216 | +
|
| 217 | + cli --> text |
| 218 | + cli --> streaming |
| 219 | + cli --> tags |
| 220 | + cli --> archive |
| 221 | + cli --> split |
| 222 | + cli --> fspaths |
| 223 | +``` |
| 224 | + |
| 225 | +## Two folder compression paths |
| 226 | + |
| 227 | +| Path | Function | When used | Memory | |
| 228 | +|------|----------|-----------|--------| |
| 229 | +| In-memory | `compressFolder()` | Library API, small folders | Loads all files into RAM | |
| 230 | +| Streaming | `compressFolderToPath()` | CLI folder compress | Bounded by chunk buffers | |
| 231 | + |
| 232 | +Both produce the same wire format; only the build strategy differs. |
| 233 | + |
| 234 | +## Public vs internal modules |
| 235 | + |
| 236 | +**Exported** via `src/index.ts` (published on npm): |
| 237 | + |
| 238 | +- `compress`, `decompress`, `compressFolder`, `decompressToPath` |
| 239 | +- `decompressPayload`, `TAG_TEXT`, `TAG_FOLDER` |
| 240 | +- Split helpers, path helpers, `unpackDirectory` |
| 241 | + |
| 242 | +**Internal** (not in package exports): |
| 243 | + |
| 244 | +- `src/streaming/` — used by CLI only |
| 245 | +- `src/cli/` — terminal interface |
| 246 | +- `src/encoding/z85.ts` internals — use `encodeBuffer` instead |
| 247 | + |
| 248 | +## Extending the codebase |
| 249 | + |
| 250 | +| Goal | Where to start | |
| 251 | +|------|----------------| |
| 252 | +| New encoding (e.g. hex) | Add `encoding/hex.ts`, extend `Encoding` type and facade | |
| 253 | +| Archive metadata (mtime) | Extend `ArchiveEntry` and wire format in `archive/format.ts` | |
| 254 | +| Async text API | Mirror `api/text.ts` with async Brotli from `zlib/promises` | |
| 255 | +| New payload type | Add tag constant in `payload/tags.ts`, route in decompress | |
| 256 | + |
| 257 | +## Running and testing |
| 258 | + |
| 259 | +```bash |
| 260 | +npm install |
| 261 | +npm test # builds then runs Vitest |
| 262 | +npm run build # compile src/ → dist/ |
| 263 | +npm run check # Biome lint + TypeScript |
| 264 | +``` |
| 265 | + |
| 266 | +Tests import from `src/index.js` (library) and execute `dist/cli.js` (CLI integration). |
| 267 | + |
| 268 | +## Further reading |
| 269 | + |
| 270 | +- [Brotli format (RFC 7932)](https://www.rfc-editor.org/rfc/rfc7932) |
| 271 | +- [Z85 specification (ZeroMQ RFC 32)](https://rfc.zeromq.org/spec/32/) |
| 272 | +- [Base64 (RFC 4648)](https://www.rfc-editor.org/rfc/rfc4648) |
0 commit comments