@hotcodepush/protocol, the one update-protocol client in TypeScript: the wire types, the evaluator and the fixture suite.
The Swift package HotCodePushCore (core-ios) and the Android library com.hotcodepush:core-android (core-android) implement the same functions and types, proven equal by this repo's fixtures.
The monorepo, the SDKs and the CLI consume it from pkg.pr.new until its publish decision — npm install https://pkg.pr.new/hotcodepush-team/protocol/@hotcodepush/protocol@<sha>, a consumer pinning one commit and bumping it deliberately, never @main; it is not the supported API, apps use the SDK for their framework.
Stack: TypeScript compiled by tsc into ESM in dist/, ESLint, Prettier, Vitest, Node 24.
The plan is the private handbook repo, checked out beside this one: ../handbook/docs/.
Its sdk-api.md (the types, statuses, reasons and events) and architecture.md (The device protocol, Evolving the wire format, Testing) are binding here.
When code and plan disagree, stop and surface it; never improvise.
src/ the package source; index.ts is the root entry point and fingerprint/index.ts the one code subpath entry, so a Worker that never fingerprints carries none of it
fixtures/ the fixture suite, shipped in the package: one JSON case per rule of the device protocol
catalog.json every value of the catalog in src/results.ts, shipped in the package for readers outside TypeScript
scripts/ the fixture and catalog generators and the pack-entries one's two input patches; each runs with `node` on a developer's machine with nothing installed, the signatures one after `npm run build`, and CI reads the committed files
dist/ the build output, never committed
Tests live beside the code they test, *.test.ts next to the file.
| Command | Does |
|---|---|
npm run build |
compile src/ into dist/ with declarations and source maps |
npm run catalog |
write catalog.json from src/results.ts; a test fails when it is stale |
npm run fmt |
format with Prettier |
npm run lint |
ESLint |
npm test |
Vitest |
npm run typecheck |
tsc --noEmit |
Run npm run fmt before every commit; lint, typecheck, test and build must pass, as ci.yml checks on every push and pull request.
ci.yml's preview job publishes every push to main and every pull request to pkg.pr.new through its GitHub App, no secret involved; a consumer's install runs no script of ours, and a commit a lockfile pins stays installable.
No releases yet: the version stays 0.0.0, and release-please and npm provenance arrive with the publish decision.
The channel index and the bundle manifest are parsed by native code compiled into customer binaries that stay in the field for years.
- Additive only: a field is never removed, renamed or retyped; a new field is optional, and readers ignore what they do not know.
The rule binds from the first customer; the reshaping of 2026-10-02 — the manifest signing the client-known content alone, the directive gone — happened before anything shipped, on the owner's decision, as did the signing of 2026-10-04:
rsa-v1_5-sha256the one scheme, and the resource file's public keys in the encoding each platform's own API imports, and the renaming of 2026-10-07, every reason naming the thing at fault first and the verification bag split in two. So did the delivery change of the same day:patchesleft the manifest, the envelope and the signed content, and the pack gained the patch entrypatches/{from}/{to}, a BSDIFF40 patch between two files named by their content hashes. A manifest or an envelope stored withpatchesstill parses, the field ignored, and a pack reader skips an entry of any other name with its body, so a later entry kind is additive. So did the renaming of 2026-10-25: every value of the catalog lowercase snake_case,SIGNATURE_INVALIDnowsignature_invalid,next-startnownext_startand the device eventrolledBacknowrolled_back; from there the catalog is additive only again. - A change that cannot be additive is a new schema major with its own path,
v2, and a handbook decision first — never an edit. CHANNEL_INDEX_SCHEMAis the format major and equals thev1in the path; a reader refuses an index whoseschemais not the one it asked for.- Conditions are frozen once shipped: a new meaning is a new type, and an unknown type fails closed; the index carries no directive, revocation being the plain revoked list every SDK understands.
- The bundle manifest has no majors: a reader reads every manifest ever released.
- Three numbers, three names:
schemais the format major,sequencethe materialization counter,versionthe team's version of a bundle. - A rule of the protocol lands with its case in
fixtures/, so all three implementations are held to it.
- A name says what the function does on first read: the verb, the object and, where it matters, the qualifier.
- Prefixes from the monorepo's vocabulary:
fetchHTTP,resolvederivations without I/O,buildfunctions that assemble a document without sending it. - Result variables carry the past participle of their operation,
fetchedIndex. - Alphabetical ordering within a scope.
- One thing per function, its name saying which; never a function that both decides something and phrases the message about it.
- Booleans carry
isorhas; a state with a moment is a timestamp such aspausedAt, never a boolean. - Every value is lowercase snake_case with no prefix, error codes included; a reason names the thing at fault first,
signature_invalid,channel_paused; a name a platform owns keeps that platform's form: environment variables, HTTP headers, MCP tool names, CLI flags and each language's identifiers. - Never the non-null assertion; nullish coalescing or a real check.
- Test titles read
should <verb> …, lowercase, conditions starting withwhen. - Fixtures and examples carry invented data only.
.mcp.jsonis absent on purpose: the one server for this repo's stack, HotCodePush's own athttps://mcp.hotcodepush.com/mcp, is not live yet; the file arrives with it..claude/skills/holds the developer skills copied fromhotcodepush-team/.githubwith the skills CLI and pinned inskills-lock.json; update them withnpx skills update..github/copilot-instructions.mdholds the review criteria, the only Copilot-specific file.- Commits are conventional commits;
mainis trunk, CI is the gate, and a commit that lands an issue saysCloses #<n>.