6.4 KiB
CLAUDE.md — OCAS
Object Content Addressable Store — self-describing CAS with JSON Schema typed nodes.
Project Structure
Monorepo with 3 packages under packages/:
| Package | Directory | Description |
|---|---|---|
@ocas/core |
packages/core |
Core CAS engine — hashing, schema, store, verify, bootstrap |
@ocas/fs |
packages/fs |
Filesystem-backed CAS store |
@ocas/cli |
packages/cli |
CLI tool (ocas binary) |
Tech Stack
- Runtime: Bun
- Language: TypeScript (strict mode,
exactOptionalPropertyTypes,noUncheckedIndexedAccess) - Build:
tsc --build(composite project references) - Test:
bun test - Lint/Format: Biome (
biome check ./biome format --write .) - Publish: Changesets +
bun publish→ npmjs (@ocas/*)
Commands
bun test # Run all tests
bun run build # Build all packages
bun run check # Biome lint
bun run format # Biome format (auto-fix)
Code Conventions
TypeScript
- Strict mode — no
any, no unchecked index access, no implicit overrides verbatimModuleSyntax— useimport typefor type-only imports- Import paths — use
.jsextension in imports (ESM convention with bundler resolution) - Export style — named exports only, re-export from
index.ts
Biome Rules
noConsole: "error"globally (exceptpackages/cli)- Recommended ruleset enabled
- Auto-organize imports via
assist.actions.source.organizeImports - Indent: 2 spaces
Naming
- Types:
PascalCase(CasNode,Hash,Store) - Functions:
camelCase(computeHash,createMemoryStore) - Constants:
UPPER_SNAKE_CASE(BOOTSTRAP_STORE) - Files:
kebab-case.ts - Test files: co-located as
*.test.ts
Key Types
Hash— 13-character uppercase Crockford Base32 string (XXH64)CasNode— content-addressed node with schemaStore— unified storage interface{ cas: CasStore, var: VarStore, tag: TagStore }ListOptions— sorting/pagination options (sort,desc,limit,offset)ListEntry— list result entry (hash,created,updated)
List Utilities
packages/core/src/list-utils.ts provides applyListOptions() — in-memory sort/paginate for ListEntry[] arrays. Used by MemoryStore; FsStore pushes sort/limit to SQLite. Core layer treats limit: undefined as "no limit"; the CLI defaults to 100 in parseListOptions().
Architecture Notes
- No "alias" concept — every name resolution flows through
store.var. Builtin schemas (@ocas/schema,@ocas/string,@ocas/output/*, …) are registered as variables duringbootstrap(store), alongside user-defined variables created viaocas var set. bootstrap(store)synchronously writes builtin name → hash bindings into the unified store; called automatically byopenStore().resolveHash(input, store)is the unified hash/name resolver in the CLI. Ifinputmatches the 13-char hash format it is returned as-is; otherwisestore.varis queried by exact name. This means every CLI command that accepts a hash argument also accepts a variable name (schema names, user vars, etc.).- Variable naming: all names must follow
@scope/nameformat (@[a-zA-Z][a-zA-Z0-9]*/segments).@ocas/*is reserved for builtins. The@prefix ensures names are visually distinct from hashes. openStore()returns a unifiedStorewithcas,var, andtagsub-stores, and bootstraps automatically.@ocas/corehas zerobun:sqlitedependency.
Internal Dependencies
Workspace packages reference each other with workspace:* in package.json.
This is resolved to real version numbers only during publishing (see below).
Git
- Commit format:
type: description(conventional commits) - Reference issues:
Fixes #N/Closes #N - Author:
小橘 <xiaoju@shazhou.work>
Project Rules
- docs/sync-readme.md — README sync conventions
Before Submitting
bun test— all tests passbun run check— no lint errorsbun run build— builds cleanly
Release Process
Releases use a release branch workflow with three phases: prepare → candidate → finalize.
main always keeps workspace:* for internal deps; release branches fix them to real versions.
Changeset files are only consumed once during finalize — prerelease (rc) never touches them.
Adding a Changeset
Add changesets alongside feature PRs on main:
<!-- .changeset/my-change.md -->
---
"@ocas/cli": patch
---
Description of the change
Changesets live in .changeset/ as markdown files. Bump types: patch / minor / major.
One changeset can cover multiple packages.
Phase 1: Prepare (cut release branch)
- Precondition: on
main, clean tree,.changeset/has pending changesets - Steps:
- Determine target version (from changeset bump types or manually)
git checkout -b release/<version>- Fix
workspace:*→ real version numbers in allpackage.json - Commit
- Does NOT run
changeset version, does NOT write CHANGELOG
Phase 2: Candidate (publish rc for validation)
- Precondition: on
release/*branch - Steps:
- Set version to
<version>-rc.N(first time rc.1, increment on subsequent runs) bun install && bun run build && bun test && bun run check- Publish:
bun publish --tag rc(order: core → fs → cli) - Commit + push
- Set version to
- Repeatable: fix bugs → add new changesets on the release branch → rc.N+1
- Does NOT consume changesets, does NOT write CHANGELOG
- Install for testing:
bun add -g @ocas/cli@rc
Phase 3: Finalize (official release)
- Precondition: on
release/*branch, rc validated - Steps:
- Consume all
.changeset/*.md→ write CHANGELOG entries (usechangeset versionor manual) - Set final version
<version>(remove-rc.N) bun install && bun run build && bun test && bun run check- Publish:
bun publish --tag latest(order: core → fs → cli) - Git tag
v<version> - Merge back to
main(CHANGELOG comes along) - Restore
workspace:*onmain - Delete release branch
- Consume all
Key Rules
- Publish order is always
@ocas/core→@ocas/fs→@ocas/cli workspace:*must be fixed before any publish —bun publishdoes NOT auto-replace them- CHANGELOG only contains official releases, never rc entries
- Changesets added on release branch (bug fixes during rc) are consumed together at finalize