Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c20c6df2bf | |||
| b2ee62dce2 | |||
| 1dacd699d5 | |||
| 0e38fd3ea9 | |||
| e00a23dd80 | |||
| d2225c8cdf | |||
| 4d7b439aaa | |||
| ccca0e60d1 |
@@ -0,0 +1,3 @@
|
|||||||
|
# Sync README
|
||||||
|
|
||||||
|
See [docs/sync-readme.md](../../docs/sync-readme.md) for full rules.
|
||||||
+108
-134
@@ -9,10 +9,10 @@ roles:
|
|||||||
- planning
|
- planning
|
||||||
procedure: |
|
procedure: |
|
||||||
On first run (no previous steps):
|
On first run (no previous steps):
|
||||||
1. Read the issue and all comments from Gitea using `tea issues <number>`
|
1. Read the issue and all comments from Gitea using `tea issues <number> -r <owner/repo>`
|
||||||
2. Read project conventions (CLAUDE.md, CONTRIBUTING.md, .cursor/rules/) to understand coding standards
|
2. Look for project conventions files (CLAUDE.md, CONTRIBUTING.md, .cursor/rules/) in the repo
|
||||||
3. Assess whether the issue has enough information to produce a test spec
|
3. Assess whether the issue has enough information to produce a test spec
|
||||||
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number>` (skip if you already commented), then output status=insufficient_info and terminate
|
4. If insufficient info: comment on the issue via `echo "..." | tea comment <number> -r <owner/repo>` (skip if you already commented), then output $status=insufficient_info
|
||||||
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
|
5. If sufficient: produce a detailed TDD test spec in markdown covering all scenarios
|
||||||
|
|
||||||
On subsequent runs (bounced back by tester with fix_spec):
|
On subsequent runs (bounced back by tester with fix_spec):
|
||||||
@@ -21,17 +21,19 @@ roles:
|
|||||||
|
|
||||||
After producing the test spec:
|
After producing the test spec:
|
||||||
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
|
1. Store it via `uwf cas put-text "<markdown content>"` and capture the returned hash
|
||||||
2. Put the hash in frontmatter.plan (required when status=ready)
|
2. Put the hash in frontmatter.plan (required when $status=ready)
|
||||||
output: "Output a brief summary of the test spec. Frontmatter must include: status (ready or insufficient_info) and plan (CAS hash of the test spec, required when status=ready)."
|
3. Set repoPath to the absolute path of the repository root
|
||||||
|
output: "Output a brief summary of the test spec. Set $status to ready (with plan hash and repoPath) or insufficient_info."
|
||||||
frontmatter:
|
frontmatter:
|
||||||
type: object
|
oneOf:
|
||||||
properties:
|
- properties:
|
||||||
status:
|
$status: { const: "ready" }
|
||||||
type: string
|
plan: { type: string }
|
||||||
enum: [ready, insufficient_info]
|
repoPath: { type: string }
|
||||||
plan:
|
required: [$status, plan, repoPath]
|
||||||
type: string
|
- properties:
|
||||||
required: [status]
|
$status: { const: "insufficient_info" }
|
||||||
|
required: [$status]
|
||||||
developer:
|
developer:
|
||||||
description: "TDD implementation per test spec"
|
description: "TDD implementation per test spec"
|
||||||
goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation."
|
goal: "You are a developer agent. You implement code changes following TDD — write tests first, then implementation."
|
||||||
@@ -39,38 +41,41 @@ roles:
|
|||||||
- coding
|
- coding
|
||||||
procedure: |
|
procedure: |
|
||||||
IMPORTANT: Always work in a git worktree, NEVER modify the main working directory directly.
|
IMPORTANT: Always work in a git worktree, NEVER modify the main working directory directly.
|
||||||
|
The repo path and other details are provided in your task prompt.
|
||||||
Set up variables from the current working directory:
|
|
||||||
```
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
||||||
WORKTREE_BASE=$(dirname $REPO_ROOT)/$(basename $REPO_ROOT)-worktrees
|
|
||||||
```
|
|
||||||
|
|
||||||
Before starting any work, set up an isolated worktree:
|
Before starting any work, set up an isolated worktree:
|
||||||
1. `cd $REPO_ROOT && git fetch origin` to get latest refs
|
1. cd into the repo path provided in your task prompt
|
||||||
2. First time (no existing branch):
|
2. `git fetch origin` to get latest refs
|
||||||
- `git worktree add $WORKTREE_BASE/fix/<issue-number>-<short-slug> -b fix/<issue-number>-<short-slug> origin/main`
|
3. First time (no existing branch):
|
||||||
- `cd $WORKTREE_BASE/fix/<issue-number>-<short-slug> && bun install`
|
- `git worktree add .worktrees/fix/<issue-number>-<short-slug> -b fix/<issue-number>-<short-slug> origin/main`
|
||||||
3. If bounced back from reviewer or tester (branch already exists):
|
- `cd .worktrees/fix/<issue-number>-<short-slug> && bun install`
|
||||||
- `cd $WORKTREE_BASE/fix/<issue-number>-<short-slug>`
|
4. If bounced back from reviewer or tester (branch already exists):
|
||||||
|
- cd into the existing worktree under `.worktrees/fix/<issue-number>-<short-slug>`
|
||||||
- `git fetch origin && git rebase origin/main`
|
- `git fetch origin && git rebase origin/main`
|
||||||
4. ALL subsequent work must happen inside the worktree directory.
|
5. ALL subsequent work must happen inside the worktree directory.
|
||||||
|
|
||||||
Then implement TDD:
|
Then implement TDD:
|
||||||
5. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's frontmatter.plan)
|
6. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner's output in your task prompt)
|
||||||
6. If bounced back from reviewer or tester: read the previous role's output to understand what needs fixing
|
7. If bounced back from reviewer or tester: read the previous role's feedback in your task prompt
|
||||||
7. Write tests first based on the spec
|
8. Write tests first based on the spec
|
||||||
8. Implement the code to make tests pass
|
9. Implement the code to make tests pass
|
||||||
9. Ensure `bun run build` passes with no errors
|
10. Ensure `bun run build` passes with no errors
|
||||||
10. Run `bun test` to verify all tests pass
|
11. Run `bun test` to verify all tests pass
|
||||||
output: "List all files changed and provide a summary. Frontmatter must include: status (done or failed)."
|
|
||||||
|
If you cannot complete the implementation (e.g. the issue is too complex, blocked by external factors,
|
||||||
|
or repeated attempts fail), set $status=failed with a reason.
|
||||||
|
output: "List all files changed and provide a summary. Set $status to done (with branch/worktree), or failed (with reason)."
|
||||||
frontmatter:
|
frontmatter:
|
||||||
type: object
|
oneOf:
|
||||||
properties:
|
- properties:
|
||||||
status:
|
$status: { const: "done" }
|
||||||
type: string
|
branch: { type: string }
|
||||||
enum: [done, failed]
|
worktree: { type: string }
|
||||||
required: [status]
|
required: [$status, branch, worktree]
|
||||||
|
- properties:
|
||||||
|
$status: { const: "failed" }
|
||||||
|
reason: { type: string }
|
||||||
|
required: [$status, reason]
|
||||||
reviewer:
|
reviewer:
|
||||||
description: "Code standards compliance check"
|
description: "Code standards compliance check"
|
||||||
goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)."
|
goal: "You are a code reviewer. You verify code standards compliance — NOT functionality (that's the tester's job)."
|
||||||
@@ -78,7 +83,7 @@ roles:
|
|||||||
- code-review
|
- code-review
|
||||||
- static-analysis
|
- static-analysis
|
||||||
procedure: |
|
procedure: |
|
||||||
Find and cd into the worktree directory for this issue.
|
The worktree path is provided in your task prompt. cd into it first.
|
||||||
|
|
||||||
Before reviewing, verify the git branch:
|
Before reviewing, verify the git branch:
|
||||||
1. Run `git branch --show-current` — confirm the branch name references the issue number being worked on
|
1. Run `git branch --show-current` — confirm the branch name references the issue number being worked on
|
||||||
@@ -90,135 +95,104 @@ roles:
|
|||||||
4. `bunx biome check` — no lint violations
|
4. `bunx biome check` — no lint violations
|
||||||
5. TypeScript strict mode — no type errors
|
5. TypeScript strict mode — no type errors
|
||||||
|
|
||||||
Soft checks (review against project conventions file if present):
|
Soft checks (review against project conventions if CLAUDE.md / .cursor/rules exist):
|
||||||
- Code style and naming conventions
|
- Naming conventions, module boundaries, code style
|
||||||
- Module organization
|
- No `console.log` in production code
|
||||||
- No debug logging left behind
|
- No dynamic imports in production code
|
||||||
|
|
||||||
Only review standards compliance. Do NOT test functionality.
|
Only review standards compliance. Do NOT test functionality.
|
||||||
If rejecting, you MUST explain the specific reason in your output.
|
If rejecting, you MUST explain the specific reason in your output.
|
||||||
output: "Explain your decision with specific file/line references. Frontmatter must include: approved (true or false)."
|
output: "Explain your decision with specific file/line references. Set $status to approved (with branch/worktree) or rejected (with comments)."
|
||||||
frontmatter:
|
frontmatter:
|
||||||
type: object
|
oneOf:
|
||||||
properties:
|
- properties:
|
||||||
approved:
|
$status: { const: "approved" }
|
||||||
type: boolean
|
branch: { type: string }
|
||||||
required: [approved]
|
worktree: { type: string }
|
||||||
|
required: [$status, branch, worktree]
|
||||||
|
- properties:
|
||||||
|
$status: { const: "rejected" }
|
||||||
|
comments: { type: string }
|
||||||
|
worktree: { type: string }
|
||||||
|
required: [$status, comments, worktree]
|
||||||
tester:
|
tester:
|
||||||
description: "Functional correctness verification"
|
description: "Functional correctness verification"
|
||||||
goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec."
|
goal: "You are a tester agent. You verify that the implementation correctly satisfies every scenario in the test spec."
|
||||||
capabilities:
|
capabilities:
|
||||||
- testing
|
- testing
|
||||||
procedure: |
|
procedure: |
|
||||||
Find and cd into the worktree directory for this issue.
|
The worktree path is provided in your task prompt. cd into it first.
|
||||||
|
|
||||||
1. Run `bun test` for automated test verification
|
1. Run `bun test` for automated test verification
|
||||||
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the latest planner step's frontmatter.plan)
|
2. Read the test spec from CAS: `uwf cas get <plan hash>` (find the hash from the planner step in the thread history)
|
||||||
3. Verify each scenario in the spec is covered and passing
|
3. Verify each scenario in the spec is covered and passing
|
||||||
4. Determine outcome:
|
4. Determine outcome:
|
||||||
- passed: all scenarios verified, tests pass
|
- passed: all scenarios verified, tests pass
|
||||||
- fix_code: tests fail or implementation doesn't match spec → send back to developer
|
- fix_code: tests fail or implementation doesn't match spec → send back to developer
|
||||||
- fix_spec: the spec itself is wrong or incomplete → send back to planner
|
- fix_spec: the spec itself is wrong or incomplete → send back to planner
|
||||||
output: "Report test results per scenario. Frontmatter must include: status (passed, fix_code, or fix_spec)."
|
output: "Report test results per scenario. Set $status to passed (with branch/worktree), fix_code (with report), or fix_spec (with report)."
|
||||||
frontmatter:
|
frontmatter:
|
||||||
type: object
|
oneOf:
|
||||||
properties:
|
- properties:
|
||||||
status:
|
$status: { const: "passed" }
|
||||||
type: string
|
branch: { type: string }
|
||||||
enum: [passed, fix_code, fix_spec]
|
worktree: { type: string }
|
||||||
required: [status]
|
required: [$status, branch, worktree]
|
||||||
|
- properties:
|
||||||
|
$status: { const: "fix_code" }
|
||||||
|
report: { type: string }
|
||||||
|
required: [$status, report]
|
||||||
|
- properties:
|
||||||
|
$status: { const: "fix_spec" }
|
||||||
|
report: { type: string }
|
||||||
|
required: [$status, report]
|
||||||
committer:
|
committer:
|
||||||
description: "Commits and creates PR"
|
description: "Commits and creates PR"
|
||||||
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
|
goal: "You are a committer agent. You create a clean commit and push a PR linking the original issue."
|
||||||
capabilities: []
|
capabilities: []
|
||||||
procedure: |
|
procedure: |
|
||||||
Find and cd into the worktree directory for this issue.
|
The worktree path, branch name, and repo info are provided in your task prompt.
|
||||||
|
cd into the worktree first.
|
||||||
Set up variables:
|
|
||||||
```
|
|
||||||
OWNER_REPO=$(git remote get-url origin | sed 's/.*[:/]\([^/]*\/[^.]*\).*/\1/')
|
|
||||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
||||||
MAIN_REPO=$(cd $REPO_ROOT && git worktree list --porcelain | head -1 | sed 's/worktree //')
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: You inherit the developer's worktree and branch. Do NOT create a new branch.
|
Note: You inherit the developer's worktree and branch. Do NOT create a new branch.
|
||||||
1. Stage all changes: `git add -A`
|
1. Stage all changes: `git add -A`
|
||||||
2. Commit with a descriptive message referencing the issue: `git commit -m "type: description\n\nFixes #N"`
|
2. Commit with a descriptive message referencing the issue: `git commit -m "type: description\n\nFixes #N"`
|
||||||
3. Push the branch: `git push -u origin <branch-name>`
|
3. Push the branch: `git push -u origin <branch-name>`
|
||||||
- If push hook fails: capture the error log in your output, mark hook_failed
|
- If push hook fails: capture the error log in your output, mark hook_failed
|
||||||
4. On push success: create a PR via `tea pr create --repo $OWNER_REPO --title "..." --description "..."`
|
4. On push success: create a PR via `tea pr create --repo <owner/repo> --title "..." --description "..."`
|
||||||
- The `--repo` flag is required to work in worktree directories
|
- Extract owner/repo from: `git remote get-url origin | sed 's/.*[:/]\([^/]*\/[^.]*\).*/\1/'`
|
||||||
- PR description must follow: What / Why / Changes / Ref sections, with `Fixes #N` in Ref
|
- PR description must include: What / Why / Changes / Ref sections, with `Fixes #N` in Ref
|
||||||
- On tea failure: capture stderr/stdout, log the error clearly, include PR details for manual creation, and mark success=false
|
- On tea failure: capture stderr/stdout, include PR details for manual creation, mark hook_failed
|
||||||
5. After PR creation, clean up the worktree:
|
5. After PR creation, clean up the worktree:
|
||||||
- `cd $MAIN_REPO`
|
- cd to the repo root (parent of .worktrees)
|
||||||
- `git worktree remove $REPO_ROOT`
|
- `git worktree remove <worktree-path>`
|
||||||
output: "Include PR URL on success or error log on failure. Frontmatter must include: success (true or false)."
|
output: "Include PR URL on success or error log on failure. Set $status to committed (with prUrl) or hook_failed (with error)."
|
||||||
frontmatter:
|
frontmatter:
|
||||||
type: object
|
oneOf:
|
||||||
properties:
|
- properties:
|
||||||
success:
|
$status: { const: "committed" }
|
||||||
type: boolean
|
prUrl: { type: string }
|
||||||
required: [success]
|
required: [$status, prUrl]
|
||||||
conditions:
|
- properties:
|
||||||
insufficientInfo:
|
$status: { const: "hook_failed" }
|
||||||
description: "Planner determined there's not enough info to proceed"
|
error: { type: string }
|
||||||
expression: "$last('planner').status = 'insufficient_info'"
|
required: [$status, error]
|
||||||
devFailed:
|
|
||||||
description: "Developer failed to implement"
|
|
||||||
expression: "$last('developer').status = 'failed'"
|
|
||||||
rejected:
|
|
||||||
description: "Reviewer rejected the implementation"
|
|
||||||
expression: "$last('reviewer').approved = false"
|
|
||||||
fixCode:
|
|
||||||
description: "Tester found code issues"
|
|
||||||
expression: "$last('tester').status = 'fix_code'"
|
|
||||||
fixSpec:
|
|
||||||
description: "Tester found spec issues"
|
|
||||||
expression: "$last('tester').status = 'fix_spec'"
|
|
||||||
hookFailed:
|
|
||||||
description: "Push hook failed"
|
|
||||||
expression: "$last('committer').success = false"
|
|
||||||
graph:
|
graph:
|
||||||
$START:
|
$START:
|
||||||
- role: "planner"
|
_: { role: "planner", prompt: "Analyze the issue and produce an implementation plan." }
|
||||||
condition: null
|
|
||||||
prompt: "Analyze the issue and produce an implementation plan."
|
|
||||||
planner:
|
planner:
|
||||||
- role: "$END"
|
insufficient_info: { role: "$END", prompt: "Insufficient information to proceed; end the workflow." }
|
||||||
condition: "insufficientInfo"
|
ready: { role: "developer", prompt: "Implement the TDD test spec (CAS hash: {{{plan}}}) in repo {{{repoPath}}}." }
|
||||||
prompt: "Insufficient information to proceed; end the workflow."
|
|
||||||
- role: "developer"
|
|
||||||
condition: null
|
|
||||||
prompt: "Implement the plan from the planner."
|
|
||||||
developer:
|
developer:
|
||||||
- role: "$END"
|
done: { role: "reviewer", prompt: "Review branch {{{branch}}} at {{{worktree}}} for code standards compliance." }
|
||||||
condition: "devFailed"
|
failed: { role: "$END", prompt: "Developer failed: {{{reason}}}. Ending workflow." }
|
||||||
prompt: "Development failed; end the workflow."
|
|
||||||
- role: "reviewer"
|
|
||||||
condition: null
|
|
||||||
prompt: "Send the implementation to the reviewer."
|
|
||||||
reviewer:
|
reviewer:
|
||||||
- role: "developer"
|
rejected: { role: "developer", prompt: "Reviewer rejected: {{{comments}}}. Fix the issues in repo {{{worktree}}}." }
|
||||||
condition: "rejected"
|
approved: { role: "tester", prompt: "Review passed. Run tests on branch {{{branch}}} at {{{worktree}}}." }
|
||||||
prompt: "Reviewer rejected the implementation; fix the issues."
|
|
||||||
- role: "tester"
|
|
||||||
condition: null
|
|
||||||
prompt: "Review passed; run tests on the implementation."
|
|
||||||
tester:
|
tester:
|
||||||
- role: "developer"
|
fix_code: { role: "developer", prompt: "Tests found code issues: {{{report}}}. Fix and re-submit." }
|
||||||
condition: "fixCode"
|
fix_spec: { role: "planner", prompt: "Tests found spec issues: {{{report}}}. Revise the test spec." }
|
||||||
prompt: "Tests found code issues; return to developer."
|
passed: { role: "committer", prompt: "All tests passed. Commit and push branch {{{branch}}} from {{{worktree}}}." }
|
||||||
- role: "planner"
|
|
||||||
condition: "fixSpec"
|
|
||||||
prompt: "Tests found spec issues; return to planner."
|
|
||||||
- role: "committer"
|
|
||||||
condition: null
|
|
||||||
prompt: "Tests passed; commit and push the changes."
|
|
||||||
committer:
|
committer:
|
||||||
- role: "developer"
|
hook_failed: { role: "developer", prompt: "Push hook failed: {{{error}}}. Fix and re-submit." }
|
||||||
condition: "hookFailed"
|
committed: { role: "$END", prompt: "PR created: {{{prUrl}}}. Workflow complete." }
|
||||||
prompt: "Push hook failed; return to developer to fix."
|
|
||||||
- role: "$END"
|
|
||||||
condition: null
|
|
||||||
prompt: "Commit succeeded; complete the workflow."
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ Monorepo with 4 packages under `packages/`:
|
|||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `json-cas` | Core CAS engine — hashing, schema, store, verify, bootstrap |
|
| `json-cas` | Core CAS engine — hashing, schema, store, verify, bootstrap |
|
||||||
| `json-cas-fs` | Filesystem-backed CAS store |
|
| `json-cas-fs` | Filesystem-backed CAS store |
|
||||||
| `json-cas-workflow` | Workflow integration layer (schemas + types) |
|
|
||||||
| `cli-json-cas` | CLI tool |
|
| `cli-json-cas` | CLI tool |
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
@@ -67,6 +66,10 @@ bun run format # Biome format (auto-fix)
|
|||||||
- Reference issues: `Fixes #N` / `Closes #N`
|
- Reference issues: `Fixes #N` / `Closes #N`
|
||||||
- Author: `小橘 <xiaoju@shazhou.work>`
|
- Author: `小橘 <xiaoju@shazhou.work>`
|
||||||
|
|
||||||
|
## Project Rules
|
||||||
|
|
||||||
|
- [docs/sync-readme.md](docs/sync-readme.md) — README sync conventions
|
||||||
|
|
||||||
## Before Submitting
|
## Before Submitting
|
||||||
|
|
||||||
1. `bun test` — all tests pass
|
1. `bun test` — all tests pass
|
||||||
|
|||||||
@@ -1,3 +1,135 @@
|
|||||||
# json-cas
|
# json-cas
|
||||||
|
|
||||||
Self-describing content-addressable storage with JSON Schema typed nodes
|
Self-describing content-addressable storage with JSON Schema typed nodes.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
json-cas is a monorepo for storing and validating JSON data in a content-addressable store (CAS). Each node has a typed payload: its `type` field is the hash of a JSON Schema node that describes the payload shape. Hashes are 13-character Crockford Base32 strings derived from XXH64 over deterministic CBOR encoding.
|
||||||
|
|
||||||
|
A bootstrap meta-schema is stored as a self-referencing seed node (`type === hash`). All other schemas are registered as nodes typed by that meta-schema. Payloads can reference other nodes via `format: "cas_ref"` fields; the library provides traversal, reference extraction, and integrity verification.
|
||||||
|
|
||||||
|
Use the in-memory store for tests and embedded apps, the filesystem store for persistence, and the CLI for local store management.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ cli-json-cas │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ json-cas-fs │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ json-cas │ (core)
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| Layer | Package | Role |
|
||||||
|
|-------|---------|------|
|
||||||
|
| Core | `@uncaged/json-cas` | Hashing, schemas, stores, verify, bootstrap |
|
||||||
|
| Storage | `@uncaged/json-cas-fs` | Filesystem-backed `Store` |
|
||||||
|
| CLI | `@uncaged/cli-json-cas` | `json-cas` command-line tool |
|
||||||
|
|
||||||
|
## Packages
|
||||||
|
|
||||||
|
| Package | Description | Type |
|
||||||
|
|---------|-------------|------|
|
||||||
|
| [`@uncaged/json-cas`](packages/json-cas/README.md) | Core CAS engine — hashing, schema, store, verify, bootstrap | lib |
|
||||||
|
| [`@uncaged/json-cas-fs`](packages/json-cas-fs/README.md) | Filesystem-backed CAS store | lib |
|
||||||
|
| [`@uncaged/cli-json-cas`](packages/cli-json-cas/README.md) | CLI tool (`json-cas` binary) | cli |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd json-cas
|
||||||
|
bun install --no-cache
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
bootstrap,
|
||||||
|
createMemoryStore,
|
||||||
|
putSchema,
|
||||||
|
validate,
|
||||||
|
} from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
const store = createMemoryStore();
|
||||||
|
await bootstrap(store);
|
||||||
|
|
||||||
|
const typeHash = await putSchema(store, {
|
||||||
|
type: "object",
|
||||||
|
properties: { message: { type: "string" } },
|
||||||
|
required: ["message"],
|
||||||
|
additionalProperties: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = await store.put(typeHash, { message: "hello" });
|
||||||
|
const node = store.get(hash);
|
||||||
|
console.log(validate(store, node!)); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
For a persistent store:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
import { bootstrap } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
const store = createFsStore("/path/to/store");
|
||||||
|
await bootstrap(store);
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the CLI (see [CLI Reference](#cli-reference) and [`packages/cli-json-cas/README.md`](packages/cli-json-cas/README.md)).
|
||||||
|
|
||||||
|
## CLI Reference
|
||||||
|
|
||||||
|
Binary: `json-cas` (from `@uncaged/cli-json-cas`). Default store: `~/.uncaged/json-cas`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
init Create store dir and write bootstrap seed
|
||||||
|
bootstrap Write meta-schema seed, print hash
|
||||||
|
schema put <file.json> Register schema, print type hash
|
||||||
|
schema get <type-hash> Print schema JSON
|
||||||
|
schema list List all schemas (name + hash)
|
||||||
|
schema validate <hash> Validate node against its schema
|
||||||
|
put <type-hash> <file.json> Store node, print hash
|
||||||
|
get <hash> Print node as JSON
|
||||||
|
has <hash> Print true/false
|
||||||
|
verify <hash> Verify integrity, print ok/corrupted
|
||||||
|
refs <hash> List direct cas_ref edges
|
||||||
|
walk <hash> [--format tree] Recursive traversal
|
||||||
|
hash <type-hash> <file.json> Compute hash without storing (dry run)
|
||||||
|
cat <hash> [--payload] Output node (--payload for payload only)
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--store <path> Store directory (default: ~/.uncaged/json-cas)
|
||||||
|
--json Compact JSON output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install --no-cache # install workspace dependencies
|
||||||
|
bun run build # tsc --build (libs)
|
||||||
|
bun run check # biome check
|
||||||
|
bun run format # biome format --write
|
||||||
|
bun test # run all package tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
Releases use [Changesets](https://github.com/changesets/changesets). From the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run release # changeset version → build → publish to npm (@uncaged/*)
|
||||||
|
```
|
||||||
|
|
||||||
|
Individual packages block `prepublishOnly` and expect releases via the workspace `release` script.
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Sync README
|
||||||
|
|
||||||
|
When updating README.md files in this monorepo, follow these conventions.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Root `README.md` — project overview and navigation hub
|
||||||
|
- Per-package `packages/*/README.md` — each package self-contained
|
||||||
|
|
||||||
|
## Root README Structure
|
||||||
|
|
||||||
|
The root README should have these sections in order:
|
||||||
|
|
||||||
|
1. **Title and one-liner** — content-addressed storage for JSON with schema validation
|
||||||
|
2. **Overview** — 2-3 paragraphs explaining what it does and key concepts
|
||||||
|
3. **Architecture** — dependency layer diagram (text-based)
|
||||||
|
4. **Packages** — table with ALL packages from packages/ directory, columns: Package, Description, Type (cli/lib)
|
||||||
|
5. **Quick Start** — install, build, basic usage
|
||||||
|
6. **CLI Reference** — brief command list, detailed usage in cli-json-cas README
|
||||||
|
7. **Development** — bun install / build / check / test
|
||||||
|
8. **Publishing** — changeset workflow (bun run release)
|
||||||
|
|
||||||
|
## Per-Package README Structure
|
||||||
|
|
||||||
|
Each package README should have:
|
||||||
|
|
||||||
|
1. **Title** — package name
|
||||||
|
2. **One-line description** — matching package.json
|
||||||
|
3. **Overview** — what it does, where it sits in the architecture, dependencies
|
||||||
|
4. **Installation** — bun add (for libs) or "included as binary" (for cli)
|
||||||
|
5. **API** (lib packages) — all exports from src/index.ts with type signatures, grouped by category, minimal usage examples
|
||||||
|
6. **CLI Usage** (cli packages) — command reference with examples
|
||||||
|
7. **Internal Structure** — brief src/ file organization
|
||||||
|
8. **Configuration** (if applicable)
|
||||||
|
|
||||||
|
## Execution Steps
|
||||||
|
|
||||||
|
### Step 1: Gather current state
|
||||||
|
For each package read:
|
||||||
|
- package.json (name, version, description, dependencies, bin)
|
||||||
|
- src/index.ts (public API exports)
|
||||||
|
- Existing README.md (preserve hand-written content worth keeping)
|
||||||
|
|
||||||
|
### Step 2: Update root README
|
||||||
|
- Ensure ALL packages in packages/ directory are listed in the table
|
||||||
|
- Update CLI command reference from actual --help output
|
||||||
|
- Keep Quick Start examples valid
|
||||||
|
|
||||||
|
### Step 3: Write/update each package README
|
||||||
|
- Follow the per-package structure
|
||||||
|
- API section MUST match actual src/index.ts exports — never invent
|
||||||
|
- For cli packages: document CLI binary name, how it is invoked
|
||||||
|
- For lib packages: document exported types and functions
|
||||||
|
- Internal structure: list actual files in src/
|
||||||
|
|
||||||
|
### Step 4: Verify
|
||||||
|
- All relative links work
|
||||||
|
- Package names match package.json
|
||||||
|
- No references to removed/renamed packages
|
||||||
|
- bun run build still passes
|
||||||
|
|
||||||
|
## Guidelines
|
||||||
|
|
||||||
|
- Only document what src/index.ts actually exports
|
||||||
|
- Root README summarizes, package READMEs go into detail
|
||||||
|
- Verify CLI examples against actual commands
|
||||||
|
- Preserve existing good prose when updating
|
||||||
|
- English for all README content
|
||||||
+3
-2
@@ -12,9 +12,10 @@
|
|||||||
"typescript": "^5.8.0"
|
"typescript": "^5.8.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build packages/json-cas packages/json-cas-fs packages/json-cas-workflow",
|
"build": "tsc --build packages/json-cas packages/json-cas-fs",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"check": "biome check .",
|
"check": "biome check .",
|
||||||
"format": "biome format --write ."
|
"format": "biome format --write .",
|
||||||
|
"release": "changeset version && bun run build && changeset publish"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# @uncaged/cli-json-cas
|
# @uncaged/cli-json-cas
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @uncaged/json-cas@0.5.3
|
||||||
|
- @uncaged/json-cas-fs@0.5.3
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# @uncaged/cli-json-cas
|
||||||
|
|
||||||
|
CLI tool for json-cas stores.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@uncaged/cli-json-cas` provides the `json-cas` command for managing a filesystem-backed store: bootstrap, schema registration, node CRUD, integrity checks, reference listing, and graph walks. It uses `@uncaged/json-cas-fs` for persistence and `@uncaged/json-cas` for core operations.
|
||||||
|
|
||||||
|
**Dependencies:** `@uncaged/json-cas`, `@uncaged/json-cas-fs`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Published as an npm package with a binary entry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add -g @uncaged/cli-json-cas
|
||||||
|
# or from the monorepo workspace:
|
||||||
|
bun link
|
||||||
|
```
|
||||||
|
|
||||||
|
**Binary name:** `json-cas` (points to `src/index.ts`, run with Bun).
|
||||||
|
|
||||||
|
In development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun packages/cli-json-cas/src/index.ts <command> [args]
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: json-cas [--store <path>] [--json] <command> [args]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global flags
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--store <path>` | Store directory (default: `~/.uncaged/json-cas`) |
|
||||||
|
| `--json` | Compact JSON output for commands that print JSON |
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `init` | Create store directory and write bootstrap seed; prints meta hash |
|
||||||
|
| `bootstrap` | Write meta-schema seed into existing store; prints hash |
|
||||||
|
| `schema put <file.json>` | Register schema from file; prints type hash |
|
||||||
|
| `schema get <type-hash>` | Print schema JSON |
|
||||||
|
| `schema list` | List all schemas (`hash name`) |
|
||||||
|
| `schema validate <hash>` | Validate node against its schema; prints `valid` / `invalid` |
|
||||||
|
| `put <type-hash> <file.json>` | Store node; prints content hash |
|
||||||
|
| `get <hash>` | Print full node as JSON |
|
||||||
|
| `has <hash>` | Print `true` or `false` |
|
||||||
|
| `verify <hash>` | Verify integrity; prints `ok` or `corrupted` |
|
||||||
|
| `refs <hash>` | Print direct `cas_ref` targets (one per line) |
|
||||||
|
| `walk <hash>` | BFS traversal; one hash per line |
|
||||||
|
| `walk <hash> --format tree` | Tree-formatted traversal |
|
||||||
|
| `hash <type-hash> <file.json>` | Compute hash without storing |
|
||||||
|
| `cat <hash>` | Print node JSON |
|
||||||
|
| `cat <hash> --payload` | Print payload only |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize default store at ~/.uncaged/json-cas
|
||||||
|
json-cas init
|
||||||
|
|
||||||
|
# Use a custom store path
|
||||||
|
json-cas --store ./data/cas bootstrap
|
||||||
|
|
||||||
|
# Register a schema and store a payload
|
||||||
|
json-cas schema put ./schemas/item.json
|
||||||
|
# → prints type hash, e.g. 0123456789ABCD
|
||||||
|
|
||||||
|
json-cas put 0123456789ABCD ./payloads/item.json
|
||||||
|
# → prints content hash
|
||||||
|
|
||||||
|
json-cas get <content-hash> --json
|
||||||
|
json-cas verify <content-hash>
|
||||||
|
json-cas walk <content-hash> --format tree
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internal Structure
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `index.ts` | Argument parsing, command dispatch, and all CLI logic |
|
||||||
|
|
||||||
|
There is no separate `src/` module tree; the CLI is a single entry file. Tests (if present) are co-located under the package.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Setting | Default | Override |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| Store directory | `~/.uncaged/json-cas` | `--store <path>` |
|
||||||
|
|
||||||
|
No config file is read; all behavior is controlled via flags and command arguments.
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@uncaged/cli-json-cas",
|
"name": "@uncaged/cli-json-cas",
|
||||||
"version": "0.5.0",
|
"version": "0.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"json-cas": "./src/index.ts"
|
"json-cas": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test",
|
||||||
|
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/json-cas": "workspace:^",
|
"@uncaged/json-cas": "^0.5.3",
|
||||||
"@uncaged/json-cas-fs": "workspace:^"
|
"@uncaged/json-cas-fs": "^0.5.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# @uncaged/json-cas-fs
|
# @uncaged/json-cas-fs
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- feat: add oneOf support to meta-schema validation
|
||||||
|
|
||||||
|
Added `oneOf` to `ALLOWED_SCHEMA_KEYS` and corresponding validation logic
|
||||||
|
in `isValidSchema`. This enables workflow frontmatter schemas that use
|
||||||
|
`oneOf` discriminated unions for multi-exit role definitions.
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @uncaged/json-cas@0.5.3
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# @uncaged/json-cas-fs
|
||||||
|
|
||||||
|
Filesystem-backed CAS store.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@uncaged/json-cas-fs` implements a persistent `Store` on disk. Each node is stored as `<hash>.bin` (CBOR-encoded `CasNode`). A `_index/` directory maps type hashes to content hashes for `listByType`. Stores support bootstrap via the same `BOOTSTRAP_STORE` symbol as the in-memory implementation.
|
||||||
|
|
||||||
|
Depends on `@uncaged/json-cas` for hashing, CBOR encoding, and types.
|
||||||
|
|
||||||
|
**Dependencies:** `@uncaged/json-cas`, `cborg`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/json-cas-fs
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Exported from `src/index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createFsStore(dir: string): BootstrapCapableStore;
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a `BootstrapCapableStore` from `@uncaged/json-cas`. The store loads existing `.bin` files on open and migrates or builds the type index on first use.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { bootstrap, putSchema } from "@uncaged/json-cas";
|
||||||
|
import { createFsStore } from "@uncaged/json-cas-fs";
|
||||||
|
|
||||||
|
const store = createFsStore("./my-cas-store");
|
||||||
|
await bootstrap(store);
|
||||||
|
|
||||||
|
const typeHash = await putSchema(store, {
|
||||||
|
type: "object",
|
||||||
|
properties: { id: { type: "string" } },
|
||||||
|
required: ["id"],
|
||||||
|
additionalProperties: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const hash = await store.put(typeHash, { id: "item-1" });
|
||||||
|
console.log(store.has(hash)); // true after restart if same dir
|
||||||
|
```
|
||||||
|
|
||||||
|
### On-disk layout
|
||||||
|
|
||||||
|
```
|
||||||
|
my-cas-store/
|
||||||
|
├── <hash>.bin # CBOR CasNode
|
||||||
|
├── _index/
|
||||||
|
│ └── <typeHash> # newline-separated content hashes
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes use atomic rename (`<hash>.tmp` → `<hash>.bin`).
|
||||||
|
|
||||||
|
## Internal Structure
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `store.ts` | `createFsStore`, load/save nodes and type index |
|
||||||
|
| `index.ts` | Public export |
|
||||||
|
| `store.test.ts` | Filesystem store tests |
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@uncaged/json-cas-fs",
|
"name": "@uncaged/json-cas-fs",
|
||||||
"version": "0.5.0",
|
"version": "0.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -15,10 +15,11 @@
|
|||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test",
|
||||||
|
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uncaged/json-cas": "workspace:^",
|
"@uncaged/json-cas": "^0.5.3",
|
||||||
"cborg": "^4.2.3"
|
"cborg": "^4.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# @uncaged/json-cas-workflow
|
|
||||||
|
|
||||||
## 0.3.0
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies []:
|
|
||||||
- @uncaged/json-cas@0.3.0
|
|
||||||
|
|
||||||
## 0.2.0
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies []:
|
|
||||||
- @uncaged/json-cas@0.2.0
|
|
||||||
|
|
||||||
## 0.1.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- fix: replace workspace:^ with actual version numbers in published dependencies
|
|
||||||
|
|
||||||
- Updated dependencies []:
|
|
||||||
- @uncaged/json-cas@0.1.3
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@uncaged/json-cas-workflow",
|
|
||||||
"version": "0.5.0",
|
|
||||||
"type": "module",
|
|
||||||
"main": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"types": "./dist/index.d.ts",
|
|
||||||
"import": "./dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist",
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"test": "bun test"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@uncaged/json-cas": "workspace:^"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,646 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
|
||||||
import type { CasNode } from "@uncaged/json-cas";
|
|
||||||
import {
|
|
||||||
createMemoryStore,
|
|
||||||
getSchema,
|
|
||||||
refs,
|
|
||||||
validate,
|
|
||||||
walk,
|
|
||||||
} from "@uncaged/json-cas";
|
|
||||||
import type { WorkflowSchemaHashes } from "./schemas.js";
|
|
||||||
import { registerWorkflowSchemas } from "./schemas.js";
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Step 1: registerWorkflowSchemas() — registers all 11 schemas
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
describe("registerWorkflowSchemas", () => {
|
|
||||||
test("returns an object with all 11 schema hashes", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const hashes = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
const keys: (keyof WorkflowSchemaHashes)[] = [
|
|
||||||
"agent",
|
|
||||||
"roleSchema",
|
|
||||||
"role",
|
|
||||||
"workflow",
|
|
||||||
"threadStart",
|
|
||||||
"threadStep",
|
|
||||||
"threadEnd",
|
|
||||||
"content",
|
|
||||||
"reactSession",
|
|
||||||
"reactTurn",
|
|
||||||
"reactToolCall",
|
|
||||||
];
|
|
||||||
expect(Object.keys(hashes)).toHaveLength(11);
|
|
||||||
for (const key of keys) {
|
|
||||||
expect(hashes[key]).toBeDefined();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all hashes are valid 13-char Crockford Base32 strings", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const hashes = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
for (const hash of Object.values(hashes)) {
|
|
||||||
expect(hash).toHaveLength(13);
|
|
||||||
expect(hash).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("all 11 hashes are distinct", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const hashes = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
const values = Object.values(hashes);
|
|
||||||
const unique = new Set(values);
|
|
||||||
expect(unique.size).toBe(11);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("is idempotent: repeated calls return the same hashes", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const first = await registerWorkflowSchemas(store);
|
|
||||||
const second = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
for (const key of Object.keys(first) as (keyof WorkflowSchemaHashes)[]) {
|
|
||||||
expect(first[key]).toBe(second[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("schemas are stored in the store (getSchema returns non-null)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const hashes = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
for (const hash of Object.values(hashes)) {
|
|
||||||
expect(getSchema(store, hash)).not.toBeNull();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Step 2: getSchema() — schema round-trip for each of the 11 types
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
describe("getSchema round-trip", () => {
|
|
||||||
test("agent schema has the expected properties", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { agent } = await registerWorkflowSchemas(store);
|
|
||||||
const schema = getSchema(store, agent);
|
|
||||||
|
|
||||||
expect(schema).not.toBeNull();
|
|
||||||
expect(schema?.type).toBe("object");
|
|
||||||
const props = schema?.properties as Record<string, unknown>;
|
|
||||||
expect(props).toHaveProperty("package");
|
|
||||||
expect(props).toHaveProperty("version");
|
|
||||||
expect(props).toHaveProperty("config");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("role schema references cas_ref for the schema field", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { role } = await registerWorkflowSchemas(store);
|
|
||||||
const schema = getSchema(store, role);
|
|
||||||
|
|
||||||
expect(schema).not.toBeNull();
|
|
||||||
const props = schema?.properties as Record<string, { format?: string }>;
|
|
||||||
expect(props.schema?.format).toBe("cas_ref");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-step schema has six required fields", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStep } = await registerWorkflowSchemas(store);
|
|
||||||
const schema = getSchema(store, threadStep);
|
|
||||||
|
|
||||||
expect(schema?.required).toHaveLength(6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-turn schema has nested tokens object", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
|
||||||
const schema = getSchema(store, reactTurn);
|
|
||||||
|
|
||||||
const props = schema?.properties as Record<
|
|
||||||
string,
|
|
||||||
{ type: string; properties?: unknown }
|
|
||||||
>;
|
|
||||||
expect(props.tokens?.type).toBe("object");
|
|
||||||
expect(props.tokens?.properties).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("workflow schema has roles with additionalProperties cas_ref", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { workflow } = await registerWorkflowSchemas(store);
|
|
||||||
const schema = getSchema(store, workflow);
|
|
||||||
|
|
||||||
const props = schema?.properties as Record<
|
|
||||||
string,
|
|
||||||
{ additionalProperties?: { format?: string } }
|
|
||||||
>;
|
|
||||||
expect(props.roles?.additionalProperties?.format).toBe("cas_ref");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Step 3: validate() — correct payloads pass for all 11 schema types
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
describe("validate – valid payloads", () => {
|
|
||||||
const HASH = "AAAAAAAAAAAAA";
|
|
||||||
|
|
||||||
test("agent payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { agent } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(agent, {
|
|
||||||
package: "gpt-4o",
|
|
||||||
version: "2024-11",
|
|
||||||
config: { temperature: 0.7 },
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("role-schema payload is valid (any object)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { roleSchema } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(roleSchema, {
|
|
||||||
type: "object",
|
|
||||||
properties: { answer: { type: "string" } },
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("role payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { role } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(role, {
|
|
||||||
name: "analyst",
|
|
||||||
description: "Analyses data",
|
|
||||||
systemPrompt: "You are an analyst.",
|
|
||||||
extractPrompt: "Extract the findings.",
|
|
||||||
schema: HASH,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("workflow payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { workflow } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(workflow, {
|
|
||||||
name: "research",
|
|
||||||
description: "Research workflow",
|
|
||||||
roles: { analyst: HASH },
|
|
||||||
moderator: [{ from: "analyst", to: "analyst", when: null }],
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-start payload is valid (null parentThread)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStart } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadStart, {
|
|
||||||
workflow: HASH,
|
|
||||||
input: "hello",
|
|
||||||
depth: 0,
|
|
||||||
parentThread: null,
|
|
||||||
agents: { main: HASH },
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-start payload is valid (non-null parentThread)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStart } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadStart, {
|
|
||||||
workflow: HASH,
|
|
||||||
input: "nested",
|
|
||||||
depth: 1,
|
|
||||||
parentThread: HASH,
|
|
||||||
agents: {},
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-step payload is valid (null previous)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStep } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadStep, {
|
|
||||||
role: "analyst",
|
|
||||||
meta: { attempt: 1 },
|
|
||||||
content: HASH,
|
|
||||||
react: HASH,
|
|
||||||
start: HASH,
|
|
||||||
previous: null,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-step payload is valid (non-null previous)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStep } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadStep, {
|
|
||||||
role: "analyst",
|
|
||||||
meta: {},
|
|
||||||
content: HASH,
|
|
||||||
react: HASH,
|
|
||||||
start: HASH,
|
|
||||||
previous: HASH,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-end payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadEnd } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadEnd, {
|
|
||||||
returnCode: 0,
|
|
||||||
summary: "Done",
|
|
||||||
start: HASH,
|
|
||||||
lastStep: HASH,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("content payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { content } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(content, { text: "Hello, world!" });
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-session payload is valid (empty turns)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactSession } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactSession, {
|
|
||||||
agent: HASH,
|
|
||||||
role: "analyst",
|
|
||||||
turns: [],
|
|
||||||
totalTokens: 0,
|
|
||||||
durationMs: 42,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-session payload is valid (multiple turns)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactSession } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactSession, {
|
|
||||||
agent: HASH,
|
|
||||||
role: "analyst",
|
|
||||||
turns: [HASH, HASH],
|
|
||||||
totalTokens: 300,
|
|
||||||
durationMs: 1500,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-turn payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactTurn, {
|
|
||||||
input: HASH,
|
|
||||||
output: HASH,
|
|
||||||
toolCalls: [HASH],
|
|
||||||
tokens: { input: 100, output: 50 },
|
|
||||||
latencyMs: 800,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-tool-call payload is valid", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactToolCall } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactToolCall, {
|
|
||||||
name: "search",
|
|
||||||
arguments: HASH,
|
|
||||||
result: HASH,
|
|
||||||
durationMs: 200,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Step 4: validate() — invalid payloads fail for representative types
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
describe("validate – invalid payloads", () => {
|
|
||||||
test("agent: missing required field fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { agent } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(agent, { package: "gpt-4o", version: "1" });
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("agent: wrong type for config fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { agent } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(agent, {
|
|
||||||
package: "gpt-4o",
|
|
||||||
version: "1",
|
|
||||||
config: "not-an-object",
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("role: missing systemPrompt fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { role } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(role, {
|
|
||||||
name: "analyst",
|
|
||||||
description: "d",
|
|
||||||
extractPrompt: "e",
|
|
||||||
schema: "AAAAAAAAAAAAA",
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-start: missing depth fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStart } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadStart, {
|
|
||||||
workflow: "AAAAAAAAAAAAA",
|
|
||||||
input: "hi",
|
|
||||||
parentThread: null,
|
|
||||||
agents: {},
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-end: returnCode as string fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadEnd } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadEnd, {
|
|
||||||
returnCode: "ok",
|
|
||||||
summary: "Done",
|
|
||||||
start: "AAAAAAAAAAAAA",
|
|
||||||
lastStep: "AAAAAAAAAAAAA",
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("content: missing text fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { content } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(content, {});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-turn: tokens.input as string fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactTurn, {
|
|
||||||
input: "AAAAAAAAAAAAA",
|
|
||||||
output: "AAAAAAAAAAAAA",
|
|
||||||
toolCalls: [],
|
|
||||||
tokens: { input: "many", output: 50 },
|
|
||||||
latencyMs: 100,
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-tool-call: missing durationMs fails", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactToolCall } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactToolCall, {
|
|
||||||
name: "tool",
|
|
||||||
arguments: "AAAAAAAAAAAAA",
|
|
||||||
result: "AAAAAAAAAAAAA",
|
|
||||||
});
|
|
||||||
expect(validate(store, store.get(h) as CasNode)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Step 5: refs() — extracts direct cas_ref fields from node payloads
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
describe("refs – cas_ref extraction", () => {
|
|
||||||
const HASH_A = "AAAAAAAAAAAAA";
|
|
||||||
const HASH_B = "BBBBBBBBBBBBB";
|
|
||||||
|
|
||||||
test("content node has no cas_ref fields → empty array", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { content } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(content, { text: "hello" });
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
expect(refs(store, node)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("role node: refs() returns the schema cas_ref", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { role } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(role, {
|
|
||||||
name: "r",
|
|
||||||
description: "d",
|
|
||||||
systemPrompt: "s",
|
|
||||||
extractPrompt: "e",
|
|
||||||
schema: HASH_A,
|
|
||||||
});
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
expect(refs(store, node)).toContain(HASH_A);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-end: refs() returns start and lastStep", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadEnd } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadEnd, {
|
|
||||||
returnCode: 0,
|
|
||||||
summary: "done",
|
|
||||||
start: HASH_A,
|
|
||||||
lastStep: HASH_B,
|
|
||||||
});
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
const result = refs(store, node);
|
|
||||||
expect(result).toContain(HASH_A);
|
|
||||||
expect(result).toContain(HASH_B);
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-tool-call: refs() returns arguments and result", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactToolCall } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactToolCall, {
|
|
||||||
name: "search",
|
|
||||||
arguments: HASH_A,
|
|
||||||
result: HASH_B,
|
|
||||||
durationMs: 100,
|
|
||||||
});
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
const result = refs(store, node);
|
|
||||||
expect(result).toContain(HASH_A);
|
|
||||||
expect(result).toContain(HASH_B);
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-step: refs() returns content, react, and start (previous null is skipped)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStep } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(threadStep, {
|
|
||||||
role: "r",
|
|
||||||
meta: {},
|
|
||||||
content: HASH_A,
|
|
||||||
react: HASH_B,
|
|
||||||
start: HASH_A,
|
|
||||||
previous: null,
|
|
||||||
});
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
const result = refs(store, node);
|
|
||||||
expect(result).toContain(HASH_A);
|
|
||||||
expect(result).toContain(HASH_B);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("thread-step: refs() includes previous when non-null", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadStep } = await registerWorkflowSchemas(store);
|
|
||||||
const HASH_C = "CCCCCCCCCCCCC";
|
|
||||||
const h = await store.put(threadStep, {
|
|
||||||
role: "r",
|
|
||||||
meta: {},
|
|
||||||
content: HASH_A,
|
|
||||||
react: HASH_B,
|
|
||||||
start: HASH_A,
|
|
||||||
previous: HASH_C,
|
|
||||||
});
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
const result = refs(store, node);
|
|
||||||
expect(result).toContain(HASH_C);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("react-session: refs() returns the agent cas_ref", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactSession } = await registerWorkflowSchemas(store);
|
|
||||||
const h = await store.put(reactSession, {
|
|
||||||
agent: HASH_A,
|
|
||||||
role: "r",
|
|
||||||
turns: [],
|
|
||||||
totalTokens: 0,
|
|
||||||
durationMs: 0,
|
|
||||||
});
|
|
||||||
const node = store.get(h) as CasNode;
|
|
||||||
expect(refs(store, node)).toContain(HASH_A);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Step 6: walk() — BFS traversal through linked workflow nodes
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
describe("walk – cross-schema traversal", () => {
|
|
||||||
test("walk visits content node linked from thread-end", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadEnd, content } = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
const contentHash = await store.put(content, { text: "summary text" });
|
|
||||||
const endHash = await store.put(threadEnd, {
|
|
||||||
returnCode: 0,
|
|
||||||
summary: "done",
|
|
||||||
start: contentHash,
|
|
||||||
lastStep: contentHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visited = new Set<string>();
|
|
||||||
walk(store, endHash, (h) => visited.add(h));
|
|
||||||
|
|
||||||
expect(visited.has(endHash)).toBe(true);
|
|
||||||
expect(visited.has(contentHash)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("walk through role → (schema stored in store)", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { role, roleSchema } = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
const schemaDocHash = await store.put(roleSchema, {
|
|
||||||
type: "object",
|
|
||||||
properties: { answer: { type: "string" } },
|
|
||||||
});
|
|
||||||
const roleHash = await store.put(role, {
|
|
||||||
name: "analyst",
|
|
||||||
description: "d",
|
|
||||||
systemPrompt: "s",
|
|
||||||
extractPrompt: "e",
|
|
||||||
schema: schemaDocHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visited = new Set<string>();
|
|
||||||
walk(store, roleHash, (h) => visited.add(h));
|
|
||||||
|
|
||||||
expect(visited.has(roleHash)).toBe(true);
|
|
||||||
expect(visited.has(schemaDocHash)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("walk handles diamond: two thread-end nodes sharing the same start", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { threadEnd, content } = await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
const sharedStart = await store.put(content, { text: "start" });
|
|
||||||
const step1 = await store.put(content, { text: "step1" });
|
|
||||||
const step2 = await store.put(content, { text: "step2" });
|
|
||||||
|
|
||||||
const end1 = await store.put(threadEnd, {
|
|
||||||
returnCode: 0,
|
|
||||||
summary: "path A",
|
|
||||||
start: sharedStart,
|
|
||||||
lastStep: step1,
|
|
||||||
});
|
|
||||||
const end2 = await store.put(threadEnd, {
|
|
||||||
returnCode: 1,
|
|
||||||
summary: "path B",
|
|
||||||
start: sharedStart,
|
|
||||||
lastStep: step2,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use react-turn as the root linking both ends via input/output
|
|
||||||
const { reactTurn } = await registerWorkflowSchemas(store);
|
|
||||||
const turnHash = await store.put(reactTurn, {
|
|
||||||
input: end1,
|
|
||||||
output: end2,
|
|
||||||
toolCalls: [],
|
|
||||||
tokens: { input: 10, output: 5 },
|
|
||||||
latencyMs: 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visited = new Set<string>();
|
|
||||||
walk(store, turnHash, (h) => visited.add(h));
|
|
||||||
|
|
||||||
expect(visited.has(turnHash)).toBe(true);
|
|
||||||
expect(visited.has(end1)).toBe(true);
|
|
||||||
expect(visited.has(end2)).toBe(true);
|
|
||||||
// sharedStart is reached from both end1 and end2, but visited only once
|
|
||||||
expect(visited.has(sharedStart)).toBe(true);
|
|
||||||
expect(visited.has(step1)).toBe(true);
|
|
||||||
expect(visited.has(step2)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("walk visits react-tool-call linked from react-turn", async () => {
|
|
||||||
const store = createMemoryStore();
|
|
||||||
const { reactTurn, reactToolCall, content } =
|
|
||||||
await registerWorkflowSchemas(store);
|
|
||||||
|
|
||||||
const argsHash = await store.put(content, { text: '{"q":"test"}' });
|
|
||||||
const resultHash = await store.put(content, { text: '{"r":"ok"}' });
|
|
||||||
const toolCallHash = await store.put(reactToolCall, {
|
|
||||||
name: "search",
|
|
||||||
arguments: argsHash,
|
|
||||||
result: resultHash,
|
|
||||||
durationMs: 120,
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputHash = await store.put(content, { text: "input" });
|
|
||||||
const outputHash = await store.put(content, { text: "output" });
|
|
||||||
const turnHash = await store.put(reactTurn, {
|
|
||||||
input: inputHash,
|
|
||||||
output: outputHash,
|
|
||||||
toolCalls: [],
|
|
||||||
tokens: { input: 80, output: 40 },
|
|
||||||
latencyMs: 600,
|
|
||||||
});
|
|
||||||
|
|
||||||
const visited = new Set<string>();
|
|
||||||
walk(store, turnHash, (h) => visited.add(h));
|
|
||||||
|
|
||||||
expect(visited.has(turnHash)).toBe(true);
|
|
||||||
expect(visited.has(inputHash)).toBe(true);
|
|
||||||
expect(visited.has(outputHash)).toBe(true);
|
|
||||||
// toolCallHash is not in the turn's cas_ref fields (toolCalls array), only linked manually
|
|
||||||
expect(visited.has(toolCallHash)).toBe(false);
|
|
||||||
|
|
||||||
// walk from toolCallHash to verify it reaches args and result
|
|
||||||
const tcVisited = new Set<string>();
|
|
||||||
walk(store, toolCallHash, (h) => tcVisited.add(h));
|
|
||||||
expect(tcVisited.has(toolCallHash)).toBe(true);
|
|
||||||
expect(tcVisited.has(argsHash)).toBe(true);
|
|
||||||
expect(tcVisited.has(resultHash)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
export {
|
|
||||||
registerWorkflowSchemas,
|
|
||||||
type WorkflowSchemaHashes,
|
|
||||||
} from "./schemas.js";
|
|
||||||
export type {
|
|
||||||
AgentPayload,
|
|
||||||
ContentPayload,
|
|
||||||
ReactSessionPayload,
|
|
||||||
ReactToolCallPayload,
|
|
||||||
ReactTurnPayload,
|
|
||||||
ReactTurnTokens,
|
|
||||||
RolePayload,
|
|
||||||
RoleSchemaPayload,
|
|
||||||
ThreadEndPayload,
|
|
||||||
ThreadStartPayload,
|
|
||||||
ThreadStepPayload,
|
|
||||||
WorkflowPayload,
|
|
||||||
WorkflowTransition,
|
|
||||||
} from "./types.js";
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
import type { Hash, Store } from "@uncaged/json-cas";
|
|
||||||
import { type JSONSchema, putSchema } from "@uncaged/json-cas";
|
|
||||||
|
|
||||||
// ── Definition layer ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const AGENT: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["package", "version", "config"],
|
|
||||||
properties: {
|
|
||||||
package: { type: "string" },
|
|
||||||
version: { type: "string" },
|
|
||||||
config: { type: "object" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** role-schema nodes hold raw JSON Schema documents, so any object is valid. */
|
|
||||||
const ROLE_SCHEMA: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
};
|
|
||||||
|
|
||||||
const ROLE: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["name", "description", "systemPrompt", "extractPrompt", "schema"],
|
|
||||||
properties: {
|
|
||||||
name: { type: "string" },
|
|
||||||
description: { type: "string" },
|
|
||||||
systemPrompt: { type: "string" },
|
|
||||||
extractPrompt: { type: "string" },
|
|
||||||
schema: { type: "string", format: "cas_ref" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const WORKFLOW: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["name", "description", "roles", "moderator"],
|
|
||||||
properties: {
|
|
||||||
name: { type: "string" },
|
|
||||||
description: { type: "string" },
|
|
||||||
roles: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: { type: "string", format: "cas_ref" },
|
|
||||||
},
|
|
||||||
moderator: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
required: ["from", "to", "when"],
|
|
||||||
properties: {
|
|
||||||
from: { type: "string" },
|
|
||||||
to: { type: "string" },
|
|
||||||
when: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Execution layer ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const THREAD_START: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["workflow", "input", "depth", "parentThread", "agents"],
|
|
||||||
properties: {
|
|
||||||
workflow: { type: "string", format: "cas_ref" },
|
|
||||||
input: { type: "string" },
|
|
||||||
depth: { type: "number" },
|
|
||||||
parentThread: {
|
|
||||||
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
|
|
||||||
},
|
|
||||||
agents: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: { type: "string", format: "cas_ref" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const THREAD_STEP: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["role", "meta", "content", "react", "start", "previous"],
|
|
||||||
properties: {
|
|
||||||
role: { type: "string" },
|
|
||||||
meta: { type: "object" },
|
|
||||||
content: { type: "string", format: "cas_ref" },
|
|
||||||
react: { type: "string", format: "cas_ref" },
|
|
||||||
start: { type: "string", format: "cas_ref" },
|
|
||||||
previous: {
|
|
||||||
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const THREAD_END: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["returnCode", "summary", "start", "lastStep"],
|
|
||||||
properties: {
|
|
||||||
returnCode: { type: "number" },
|
|
||||||
summary: { type: "string" },
|
|
||||||
start: { type: "string", format: "cas_ref" },
|
|
||||||
lastStep: { type: "string", format: "cas_ref" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CONTENT: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["text"],
|
|
||||||
properties: {
|
|
||||||
text: { type: "string" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── React layer ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const REACT_SESSION: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["agent", "role", "turns", "totalTokens", "durationMs"],
|
|
||||||
properties: {
|
|
||||||
agent: { type: "string", format: "cas_ref" },
|
|
||||||
role: { type: "string" },
|
|
||||||
turns: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string", format: "cas_ref" },
|
|
||||||
},
|
|
||||||
totalTokens: { type: "number" },
|
|
||||||
durationMs: { type: "number" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const REACT_TURN: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["input", "output", "toolCalls", "tokens", "latencyMs"],
|
|
||||||
properties: {
|
|
||||||
input: { type: "string", format: "cas_ref" },
|
|
||||||
output: { type: "string", format: "cas_ref" },
|
|
||||||
toolCalls: {
|
|
||||||
type: "array",
|
|
||||||
items: { type: "string", format: "cas_ref" },
|
|
||||||
},
|
|
||||||
tokens: {
|
|
||||||
type: "object",
|
|
||||||
required: ["input", "output"],
|
|
||||||
properties: {
|
|
||||||
input: { type: "number" },
|
|
||||||
output: { type: "number" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
},
|
|
||||||
latencyMs: { type: "number" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const REACT_TOOL_CALL: JSONSchema = {
|
|
||||||
type: "object",
|
|
||||||
required: ["name", "arguments", "result", "durationMs"],
|
|
||||||
properties: {
|
|
||||||
name: { type: "string" },
|
|
||||||
arguments: { type: "string", format: "cas_ref" },
|
|
||||||
result: { type: "string", format: "cas_ref" },
|
|
||||||
durationMs: { type: "number" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type WorkflowSchemaHashes = {
|
|
||||||
agent: Hash;
|
|
||||||
roleSchema: Hash;
|
|
||||||
role: Hash;
|
|
||||||
workflow: Hash;
|
|
||||||
threadStart: Hash;
|
|
||||||
threadStep: Hash;
|
|
||||||
threadEnd: Hash;
|
|
||||||
content: Hash;
|
|
||||||
reactSession: Hash;
|
|
||||||
reactTurn: Hash;
|
|
||||||
reactToolCall: Hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register all 11 workflow schemas into the given store.
|
|
||||||
* Returns a map from camelCase schema name to its CAS type hash.
|
|
||||||
* Idempotent: safe to call multiple times on the same store.
|
|
||||||
*/
|
|
||||||
export async function registerWorkflowSchemas(
|
|
||||||
store: Store,
|
|
||||||
): Promise<WorkflowSchemaHashes> {
|
|
||||||
const [
|
|
||||||
agent,
|
|
||||||
roleSchema,
|
|
||||||
role,
|
|
||||||
workflow,
|
|
||||||
threadStart,
|
|
||||||
threadStep,
|
|
||||||
threadEnd,
|
|
||||||
content,
|
|
||||||
reactSession,
|
|
||||||
reactTurn,
|
|
||||||
reactToolCall,
|
|
||||||
] = await Promise.all([
|
|
||||||
putSchema(store, AGENT),
|
|
||||||
putSchema(store, ROLE_SCHEMA),
|
|
||||||
putSchema(store, ROLE),
|
|
||||||
putSchema(store, WORKFLOW),
|
|
||||||
putSchema(store, THREAD_START),
|
|
||||||
putSchema(store, THREAD_STEP),
|
|
||||||
putSchema(store, THREAD_END),
|
|
||||||
putSchema(store, CONTENT),
|
|
||||||
putSchema(store, REACT_SESSION),
|
|
||||||
putSchema(store, REACT_TURN),
|
|
||||||
putSchema(store, REACT_TOOL_CALL),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
agent,
|
|
||||||
roleSchema,
|
|
||||||
role,
|
|
||||||
workflow,
|
|
||||||
threadStart,
|
|
||||||
threadStep,
|
|
||||||
threadEnd,
|
|
||||||
content,
|
|
||||||
reactSession,
|
|
||||||
reactTurn,
|
|
||||||
reactToolCall,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import type { Hash } from "@uncaged/json-cas";
|
|
||||||
|
|
||||||
// ── Definition layer ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type AgentPayload = {
|
|
||||||
package: string;
|
|
||||||
version: string;
|
|
||||||
config: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** A JSON Schema document stored as-is. */
|
|
||||||
export type RoleSchemaPayload = Record<string, unknown>;
|
|
||||||
|
|
||||||
export type RolePayload = {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
systemPrompt: string;
|
|
||||||
extractPrompt: string;
|
|
||||||
/** cas_ref → role-schema */
|
|
||||||
schema: Hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowTransition = {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
when: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkflowPayload = {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
/** cas_ref → role */
|
|
||||||
roles: Record<string, Hash>;
|
|
||||||
moderator: WorkflowTransition[];
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Execution layer ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type ThreadStartPayload = {
|
|
||||||
/** cas_ref → workflow */
|
|
||||||
workflow: Hash;
|
|
||||||
input: string;
|
|
||||||
depth: number;
|
|
||||||
/** cas_ref → thread-start | null */
|
|
||||||
parentThread: Hash | null;
|
|
||||||
/** cas_ref → agent */
|
|
||||||
agents: Record<string, Hash>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ThreadStepPayload = {
|
|
||||||
role: string;
|
|
||||||
meta: Record<string, unknown>;
|
|
||||||
/** cas_ref → content */
|
|
||||||
content: Hash;
|
|
||||||
/** cas_ref → react-session */
|
|
||||||
react: Hash;
|
|
||||||
/** cas_ref → thread-start */
|
|
||||||
start: Hash;
|
|
||||||
/** cas_ref → thread-step | null */
|
|
||||||
previous: Hash | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ThreadEndPayload = {
|
|
||||||
returnCode: number;
|
|
||||||
summary: string;
|
|
||||||
/** cas_ref → thread-start */
|
|
||||||
start: Hash;
|
|
||||||
/** cas_ref → thread-step */
|
|
||||||
lastStep: Hash;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ContentPayload = {
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── React layer ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type ReactSessionPayload = {
|
|
||||||
/** cas_ref → agent */
|
|
||||||
agent: Hash;
|
|
||||||
role: string;
|
|
||||||
/** cas_ref → react-turn */
|
|
||||||
turns: Hash[];
|
|
||||||
totalTokens: number;
|
|
||||||
durationMs: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReactTurnTokens = {
|
|
||||||
input: number;
|
|
||||||
output: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReactTurnPayload = {
|
|
||||||
/** cas_ref → content */
|
|
||||||
input: Hash;
|
|
||||||
/** cas_ref → content */
|
|
||||||
output: Hash;
|
|
||||||
/** cas_ref → react-tool-call */
|
|
||||||
toolCalls: Hash[];
|
|
||||||
tokens: ReactTurnTokens;
|
|
||||||
latencyMs: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ReactToolCallPayload = {
|
|
||||||
name: string;
|
|
||||||
/** cas_ref → content (arguments) */
|
|
||||||
arguments: Hash;
|
|
||||||
/** cas_ref → content (result) */
|
|
||||||
result: Hash;
|
|
||||||
durationMs: number;
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"rootDir": "src",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"exclude": ["src/**/*.test.ts"],
|
|
||||||
"references": [{ "path": "../json-cas" }]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,15 @@
|
|||||||
# @uncaged/json-cas
|
# @uncaged/json-cas
|
||||||
|
|
||||||
|
## 0.5.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- feat: add oneOf support to meta-schema validation
|
||||||
|
|
||||||
|
Added `oneOf` to `ALLOWED_SCHEMA_KEYS` and corresponding validation logic
|
||||||
|
in `isValidSchema`. This enables workflow frontmatter schemas that use
|
||||||
|
`oneOf` discriminated unions for multi-exit role definitions.
|
||||||
|
|
||||||
## 0.3.0
|
## 0.3.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# @uncaged/json-cas
|
||||||
|
|
||||||
|
Core CAS engine — hashing, schema, store, verify, bootstrap.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@uncaged/json-cas` is the foundation of the json-cas monorepo. It defines content-addressed nodes (`CasNode`), the `Store` interface, XXH64-based hashing with deterministic CBOR, JSON Schema registration and validation (including `cas_ref` links between nodes), bootstrap seeding, and integrity verification.
|
||||||
|
|
||||||
|
Other packages build on this layer: `json-cas-fs` provides persistence, and `cli-json-cas` exposes store operations on the command line.
|
||||||
|
|
||||||
|
**Dependencies:** `ajv`, `cborg`, `xxhash-wasm`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @uncaged/json-cas
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
All symbols below are exported from `src/index.ts`.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/** 13-character uppercase Crockford Base32 (XXH64) */
|
||||||
|
type Hash = string;
|
||||||
|
|
||||||
|
type CasNode<T = unknown> = {
|
||||||
|
type: Hash;
|
||||||
|
payload: T;
|
||||||
|
timestamp: number; // Unix epoch ms
|
||||||
|
};
|
||||||
|
|
||||||
|
type Store = {
|
||||||
|
put(typeHash: Hash, payload: unknown): Promise<Hash>;
|
||||||
|
get(hash: Hash): CasNode | null;
|
||||||
|
has(hash: Hash): boolean;
|
||||||
|
listByType(typeHash: Hash): Hash[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type JSONSchema = Record<string, unknown>;
|
||||||
|
|
||||||
|
type BootstrapCapableStore = Store & {
|
||||||
|
[BOOTSTRAP_STORE](payload: unknown): Promise<Hash>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hashing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function computeHash(typeHash: Hash, payload: unknown): Promise<Hash>;
|
||||||
|
function computeSelfHash(payload: unknown): Promise<Hash>;
|
||||||
|
function cborEncode(value: unknown): Uint8Array;
|
||||||
|
```
|
||||||
|
|
||||||
|
`computeHash` — `XXH64(utf8(typeHash) ++ CBOR(payload))` for normal nodes.
|
||||||
|
|
||||||
|
`computeSelfHash` — `XXH64(CBOR(payload))` for bootstrap nodes where `type === hash`.
|
||||||
|
|
||||||
|
### Bootstrap
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const BOOTSTRAP_STORE: unique symbol;
|
||||||
|
|
||||||
|
async function bootstrap(store: Store): Promise<Hash>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Writes the meta-schema seed node (idempotent). Requires a `BootstrapCapableStore` (e.g. from `createMemoryStore()`).
|
||||||
|
|
||||||
|
### Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class SchemaValidationError extends Error;
|
||||||
|
|
||||||
|
async function putSchema(store: Store, jsonSchema: JSONSchema): Promise<Hash>;
|
||||||
|
function getSchema(store: Store, typeHash: Hash): JSONSchema | null;
|
||||||
|
function validate(store: Store, node: CasNode): boolean;
|
||||||
|
function refs(store: Store, node: CasNode): Hash[];
|
||||||
|
function walk(
|
||||||
|
store: Store,
|
||||||
|
rootHash: Hash,
|
||||||
|
visitor: (hash: Hash, node: CasNode) => void,
|
||||||
|
): void;
|
||||||
|
```
|
||||||
|
|
||||||
|
- `putSchema` — stores a schema typed by the meta-schema; returned hash is the `typeHash` for conforming payloads.
|
||||||
|
- `refs` — collects all `format: "cas_ref"` values in the payload per schema shape.
|
||||||
|
- `walk` — BFS from `rootHash`, following `cas_ref` edges; cycles are visited once.
|
||||||
|
|
||||||
|
### Store
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function createMemoryStore(): BootstrapCapableStore;
|
||||||
|
```
|
||||||
|
|
||||||
|
In-memory `Store` with type indexing, suitable for tests and ephemeral use.
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function verify(hash: Hash, node: CasNode): Promise<boolean>;
|
||||||
|
```
|
||||||
|
|
||||||
|
Recomputes hash from `node` and compares to `hash` (self-referencing vs normal rules).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
bootstrap,
|
||||||
|
createMemoryStore,
|
||||||
|
putSchema,
|
||||||
|
refs,
|
||||||
|
validate,
|
||||||
|
walk,
|
||||||
|
} from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
const store = createMemoryStore();
|
||||||
|
const metaHash = await bootstrap(store);
|
||||||
|
|
||||||
|
const personType = await putSchema(store, {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
friend: { type: "string", format: "cas_ref" },
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
additionalProperties: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const aliceHash = await store.put(personType, { name: "Alice" });
|
||||||
|
const bobHash = await store.put(personType, {
|
||||||
|
name: "Bob",
|
||||||
|
friend: aliceHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bob = store.get(bobHash)!;
|
||||||
|
console.log(validate(store, bob)); // true
|
||||||
|
console.log(refs(store, bob)); // [aliceHash]
|
||||||
|
walk(store, bobHash, (h) => console.log(h)); // bobHash, aliceHash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internal Structure
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `types.ts` | `Hash`, `CasNode`, `Store` |
|
||||||
|
| `hash.ts` | `computeHash`, `computeSelfHash` |
|
||||||
|
| `cbor.ts` | Deterministic CBOR encoding |
|
||||||
|
| `bootstrap-capable.ts` | `BOOTSTRAP_STORE` symbol and capability check |
|
||||||
|
| `bootstrap.ts` | Meta-schema seed and `bootstrap()` |
|
||||||
|
| `store.ts` | `createMemoryStore()` |
|
||||||
|
| `mem-store.ts` | Alternate in-memory store (tests only; not exported) |
|
||||||
|
| `schema.ts` | Schema put/get/validate, `refs`, `walk` |
|
||||||
|
| `verify.ts` | Node integrity verification |
|
||||||
|
| `index.ts` | Public exports |
|
||||||
|
|
||||||
|
Tests live in `src/*.test.ts` and `tests/`.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@uncaged/json-cas",
|
"name": "@uncaged/json-cas",
|
||||||
"version": "0.5.0",
|
"version": "0.5.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"src"
|
"src"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun test"
|
"test": "bun test",
|
||||||
|
"prepublishOnly": "echo '请用 bun run release 从根目录发版' && exit 1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.20.0",
|
"ajv": "^8.20.0",
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ const BOOTSTRAP_PAYLOAD = {
|
|||||||
type: "array",
|
type: "array",
|
||||||
items: { type: "object", additionalProperties: false },
|
items: { type: "object", additionalProperties: false },
|
||||||
},
|
},
|
||||||
|
oneOf: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "object", additionalProperties: false },
|
||||||
|
},
|
||||||
items: { type: "object", additionalProperties: false },
|
items: { type: "object", additionalProperties: false },
|
||||||
format: { type: "string" },
|
format: { type: "string" },
|
||||||
title: { type: "string" },
|
title: { type: "string" },
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const ALLOWED_SCHEMA_KEYS = new Set([
|
|||||||
"required",
|
"required",
|
||||||
"additionalProperties",
|
"additionalProperties",
|
||||||
"anyOf",
|
"anyOf",
|
||||||
|
"oneOf",
|
||||||
"items",
|
"items",
|
||||||
"format",
|
"format",
|
||||||
"title",
|
"title",
|
||||||
@@ -108,6 +109,13 @@ function isValidSchema(value: unknown): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("oneOf" in schema) {
|
||||||
|
if (!Array.isArray(schema.oneOf) || schema.oneOf.length === 0) return false;
|
||||||
|
for (const entry of schema.oneOf) {
|
||||||
|
if (!isValidSchema(entry)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("items" in schema && !isValidSchema(schema.items)) return false;
|
if ("items" in schema && !isValidSchema(schema.items)) return false;
|
||||||
if ("format" in schema && typeof schema.format !== "string") return false;
|
if ("format" in schema && typeof schema.format !== "string") return false;
|
||||||
if ("title" in schema && typeof schema.title !== "string") return false;
|
if ("title" in schema && typeof schema.title !== "string") return false;
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ describe("Test Suite 1: Meta-Schema Structure and Self-Validation", () => {
|
|||||||
expect(properties).not.toHaveProperty("$id");
|
expect(properties).not.toHaveProperty("$id");
|
||||||
expect(properties).not.toHaveProperty("$defs");
|
expect(properties).not.toHaveProperty("$defs");
|
||||||
expect(properties).not.toHaveProperty("allOf");
|
expect(properties).not.toHaveProperty("allOf");
|
||||||
expect(properties).not.toHaveProperty("oneOf");
|
|
||||||
expect(properties).not.toHaveProperty("not");
|
expect(properties).not.toHaveProperty("not");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -148,6 +147,18 @@ describe("Test Suite 2: putSchema Validation - Valid Schemas", () => {
|
|||||||
expect(hash).toBeTruthy();
|
expect(hash).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("2.7b: Accept schema with oneOf", async () => {
|
||||||
|
const store = new MemStore();
|
||||||
|
await bootstrap(store);
|
||||||
|
const hash = await putSchema(store, {
|
||||||
|
oneOf: [
|
||||||
|
{ properties: { status: { const: "ready" } }, required: ["status"] },
|
||||||
|
{ properties: { status: { const: "failed" } }, required: ["status"] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(hash).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
test("2.8: Accept schema with array items", async () => {
|
test("2.8: Accept schema with array items", async () => {
|
||||||
const store = new MemStore();
|
const store = new MemStore();
|
||||||
await bootstrap(store);
|
await bootstrap(store);
|
||||||
|
|||||||
+1
-4
@@ -12,10 +12,7 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@uncaged/json-cas": ["./packages/json-cas/src/index.ts"],
|
"@uncaged/json-cas": ["./packages/json-cas/src/index.ts"],
|
||||||
"@uncaged/json-cas-fs": ["./packages/json-cas-fs/src/index.ts"],
|
"@uncaged/json-cas-fs": ["./packages/json-cas-fs/src/index.ts"]
|
||||||
"@uncaged/json-cas-workflow": [
|
|
||||||
"./packages/json-cas-workflow/src/index.ts"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user