diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e27b2ce..9789919 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,14 @@ updates: labels: - "dependencies" + - package-ecosystem: "npm" + directory: "/packages/core" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/deploy-use-cases-site.yml b/.github/workflows/deploy-use-cases-site.yml index 42a4a0e..33dc2f8 100644 --- a/.github/workflows/deploy-use-cases-site.yml +++ b/.github/workflows/deploy-use-cases-site.yml @@ -12,7 +12,7 @@ on: - "apps/use-cases-site/**" - "package.json" - "package-lock.json" - - "samples/**" + - "packages/core/**" push: branches: - main @@ -21,7 +21,7 @@ on: - "apps/use-cases-site/**" - "package.json" - "package-lock.json" - - "samples/**" + - "packages/core/**" permissions: contents: read diff --git a/.github/workflows/release-provenance.yml b/.github/workflows/release-provenance.yml index 2c6c946..260b10c 100644 --- a/.github/workflows/release-provenance.yml +++ b/.github/workflows/release-provenance.yml @@ -60,12 +60,12 @@ jobs: - name: Publish dry run if: inputs.dry_run == 'true' - run: npm publish --provenance --access public --dry-run + run: npm publish --workspace @workit/core --provenance --access public --dry-run env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Publish if: inputs.dry_run == 'false' - run: npm publish --provenance --access public + run: npm publish --workspace @workit/core --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 5debc4a..6cef8d7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,8 @@ out/ lib/ !benchmarks/articles/lib/ !benchmarks/articles/lib/** +!packages/core/benchmarks/articles/lib/ +!packages/core/benchmarks/articles/lib/** coverage/ .nyc_output/ mnt/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0aaad6b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ + + +# Changelog + +## 0.1.5 + +Move `@workit/core` to `packages/core` monorepo layout. No public API changes. + +This release prepares the repository for future WorkIt extensions while keeping +the published package contract unchanged. + +- `@workit/core` source, tests, samples, benchmarks, evidence, and release + scripts now live under `packages/core`. +- Root package is now a private workspace coordinator. +- Existing install and import paths remain unchanged. +- Existing package exports remain unchanged. +- Root runtime bundle size remains unchanged. +- Release provenance workflow now publishes the workspace package. +- Use-cases site now resolves the real local package from `packages/core`. + +This is an infrastructure release only. New runtime capabilities are planned for +the next minor line after the monorepo layout is validated in CI and npm. diff --git a/README.md b/README.md index 0440c4c..aa796e3 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,22 @@ Author: Admilson B. F. Cossa SPDX-License-Identifier: Apache-2.0 --> -# WorkIt +

+ WorkIt +

-Structured concurrency for TypeScript systems that need owned async work: -bounded parallelism, cancellation, cleanup, retries, timeouts, budgets, -backpressure, worker offload, and observable task lifecycles. +# WorkIt -Native `Promise` is still the right tool for one asynchronous value. WorkIt is -for the next step: a request, batch, agent run, provider race, stream, or -background operation where related async tasks must live, fail, cancel, and -clean up together. +WorkIt is a TypeScript structured concurrency runtime for Node.js server +runtimes. [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) -[![Node](https://img.shields.io/badge/node-%3E%3D20.11-brightgreen)](package.json) -[![Runtime deps](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)](package.json) -[![Verification](https://img.shields.io/badge/verify-green-brightgreen)](#verified-evidence) -[![Article benches](https://img.shields.io/badge/article%20benches-19%2F19-brightgreen)](benchmarks/articles/) +[![npm](https://img.shields.io/npm/v/@workit/core?label=npm)](https://www.npmjs.com/package/@workit/core) +[![Node](https://img.shields.io/badge/node-%3E%3D20.11-brightgreen)](packages/core/package.json) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12807/badge)](https://www.bestpractices.dev/projects/12807) -Live examples: -npm package: +WorkIt owns related async work through scope, cancellation, cleanup, context, +events, and child task lifecycles. ## Install @@ -30,526 +26,92 @@ npm package: npm install @workit/core ``` -WorkIt targets Node.js server runtimes today. Browser and edge runtimes resolve -to an explicit unsupported-runtime boundary. - -## Quick Start - ```ts -import { work } from "@workit/core"; - -const doubled = await work([1, 2, 3]) - .inParallel(2) - .do(async (value, _ctx) => value * 2); +import { run, work } from "@workit/core"; ``` -The context parameter is available when the task needs cancellation, progress, -budgets, or scoped resources. It can be ignored for plain transformations. - -## Why Ownership Matters - -Consider this batch helper: - -```ts -type BatchEvent = - | { type: "item:started"; item: T; attempt: number } - | { type: "item:retried"; item: T; attempt: number; error: unknown } - | { type: "item:completed"; item: T }; - -type BatchOptions = { - concurrency: number; - retries: number; - timeoutMs: number; - signal: AbortSignal; - events: { emit(event: BatchEvent): void }; - run: (item: T, options: { signal: AbortSignal }) => Promise; -}; - -function backoffMs(attempt: number): number { - return Math.min(1000 * 2 ** (attempt - 1), 10_000); -} - -function sleep(ms: number, signal?: AbortSignal): Promise { - if (signal?.aborted) return Promise.reject(signal.reason); - - return new Promise((resolve, reject) => { - let settled = false; - - const finish = (): void => { - if (settled) return; - settled = true; - signal?.removeEventListener("abort", abort); - resolve(); - }; - - const abort = (): void => { - if (settled) return; - settled = true; - clearTimeout(timer); - reject(signal?.reason); - }; +WorkIt is Apache-2.0 licensed. Contributions are welcome through issues and +pull requests; please follow [`CONTRIBUTING.md`](CONTRIBUTING.md) and +[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md). - const timer = setTimeout(finish, ms); - signal?.addEventListener("abort", abort, { once: true }); - }); -} - -async function runBatch( - items: readonly T[], - options: BatchOptions -): Promise { - const results = new Array(items.length); - let nextIndex = 0; - - async function worker(): Promise { - while (!options.signal.aborted) { - const index = nextIndex++; - if (index >= items.length) return; - - const item = items[index]; - for (let attempt = 1; attempt <= options.retries + 1; attempt++) { - const timeout = AbortSignal.timeout(options.timeoutMs); - const signal = AbortSignal.any([options.signal, timeout]); - - try { - options.events.emit({ type: "item:started", item, attempt }); - results[index] = await options.run(item, { signal }); - options.events.emit({ type: "item:completed", item }); - break; - } catch (error) { - if (attempt > options.retries || options.signal.aborted) throw error; - options.events.emit({ type: "item:retried", item, attempt, error }); - await sleep(backoffMs(attempt), options.signal); - } - } - } - } +Live examples: - await Promise.all( - Array.from({ length: options.concurrency }, () => worker()) - ); +npm package: - return results; +Changelog: [CHANGELOG.md](CHANGELOG.md) + +## Versioning + +WorkIt follows semver with a stricter release discipline: + +- Patch releases, such as `0.1.x`, are for fixes, build/release hardening, + layout migrations, documentation, and evidence updates. They must not add new + public runtime APIs. +- Minor releases, such as `0.2.0`, may add new subpaths or feature families when + they are backed by tests, evidence, package-consumer checks, and + documentation. +- The root `@workit/core` import remains size-disciplined. New heavier + capabilities should live in subpaths or companion packages. +- `1.0.0` will mark a frozen public API and long-term compatibility policy, not + a shortcut for credibility. Current `0.x` releases are validated and usable, + with changes managed through semver and release notes. + +## Citation + +If you use WorkIt in research, benchmarks, or reproducible artifacts, please +cite the software release you used: + +```bibtex +@software{workit2026, + author = {Admilson B. F. Cossa}, + title = {WorkIt: A TypeScript Structured Concurrency Runtime for Node.js Server Runtimes}, + year = {2026}, + url = {https://github.com/WorkRuntime/workit}, + version = {0.1.5}, + license = {Apache-2.0} } ``` -It covers bounded parallelism, timeout, parent cancellation, ordered results, -typed events, and retry backoff. The lifecycle is still split across the queue, -timeout signals, retry loop, event sink, and caller. Adding sibling -cancellation, cleanup, budgets, partial results, or diagnostics extends the -same ownership protocol in several places. - -With WorkIt, the ownership boundary is the API: - -```ts -import { work } from "@workit/core"; - -const results = await work(items) - .inParallel(8) - .withRetry(3) - .withTimeout("5s") - .do(async (item, ctx) => { - ctx.report({ message: `processing ${item.id}` }); - return apiCall(item, { signal: ctx.signal }); - }); -``` - -This gives the batch one lifecycle contract: - -- at most 8 items run at once -- transient failures retry with cancel-aware backoff -- each item has a 5 second timeout -- every task receives the same cancellation model through `ctx.signal` -- progress is a typed runtime event -- queued and active work stop together when the owner is cancelled - -## What WorkIt Replaces - -WorkIt does not replace promises as values. It replaces repeated lifecycle -orchestration around promises. - -| Existing pattern | Real limitation | Ownership contract | WorkIt primitive | -|---|---|---:|---| -| Hand-written concurrency queue | Queue, retry, timeout, and caller cancellation each own part of the lifecycle | no single owner | `work().inParallel()` / `run.pool()` | -| Manual scope object with cancellation tokens | Works until every new feature must reimplement the same lifecycle rules | local convention | `group()` / `run.*` | -| Provider race with `Promise.race()` | Losing calls keep running unless each branch is wired to cancellation | no | `run.race()` | -| Retry loop with delayed backoff | Cancellation has to be remembered in every sleep and retry branch | no | `run.retry()` | -| Request fan-out with `Promise.all()` | Sibling cancellation and cleanup are not part of the value contract | no | `group()` / `run.all()` | -| Manual `try/finally` cleanup | Cleanup can hang or obscure the original failure without an explicit policy | partial | `run.bracket()` | -| Async iterator prefetch | Producer control and consumer demand are easy to separate accidentally | partial | `work().stream()` | -| Ad hoc token or cost counters | Nested work can charge the wrong owner without a shared context contract | partial | context budgets | -| CPU loop with `AbortController` | Cooperative signals cannot preempt the main thread | no | `offload()` | +## Repository Layout -WorkIt's ownership contract is the combination of scope, cancellation reason, -child task set, defer stack, context, and event stream. +This repository uses a monorepo layout. The published package contract is still +owned by `packages/core`. -## Mental Model - -WorkIt creates a scope tree. A scope owns its tasks. When a foreground task -fails, times out, or is cancelled, the scope cancels sibling work, waits for -owned children, runs cleanup, emits lifecycle events, and then settles. - -```mermaid -flowchart TD - A[scope created] --> B[foreground tasks start] - A --> C[background tasks start] - B --> D{failure, timeout, or cancel} - D -- no --> E[foreground values settle] - D -- yes --> F[cancel siblings with typed reason] - E --> G[await owned background tasks] - F --> G - G --> H[run defer stack and bracket cleanup] - H --> I[scope closes] - A -. explicit escape .-> J[detached task] - J -. not awaited by scope .-> K[external owner required] -``` - -Rules: - -1. Every task runs inside exactly one scope. -2. A scope owns cancellation, cleanup, context, child tasks, and events. -3. Cancelling a scope aborts its signal and propagates a typed reason. -4. A scope cannot close while non-detached children are still pending. -5. `background` is owned and delays closure. -6. `detached` is explicit and transfers ownership to the caller. - -## Core API - -| Need | Use | +| Path | Purpose | |---|---| -| One owned operation with child tasks | `group(async (task) => ...)` | -| Batch work over items | `work(items)` | -| Bounded parallel task functions | `run.pool(concurrency, tasks)` | -| Safer `Promise.all` / `race` / `any` | `run.all()`, `run.race()`, `run.any()` | -| Retry, timeout, fallback, hedge | `run.retry()`, `run.timeout()`, `run.fallback()`, `run.hedge()` | -| Resource safety | `run.bracket()` | -| Critical sections | `run.uncancellable()` | -| Backpressured streams | `work(items).stream()` | -| Producer-consumer coordination | `@workit/core/channel` | -| Worker-thread hard boundary | `@workit/core/worker` | -| Diagnostics and snapshots | `@workit/core/diagnostics` | -| OpenTelemetry bridge | `@workit/core/otel` | -| Agent helper contracts | `@workit/core/ai` | - -## Common Use Cases - -These are short entry points. The full narrative and benchmark discussion live -in [`articles/`](articles/). - -### Owned Request Fan-Out - -```ts -import { group } from "@workit/core"; - -const response = await group(async (task) => { - const profile = task((ctx) => fetchProfile({ signal: ctx.signal })); - const account = task((ctx) => fetchAccount({ signal: ctx.signal })); - - task.background(async (ctx) => { - ctx.defer(() => flushAuditBuffer()); - await writeAuditEvent({ signal: ctx.signal }); - }); - - return { profile: await profile, account: await account }; -}); -``` - -If `profile` fails, the account and audit tasks are cancelled. The audit cleanup -runs before the scope closes. +| `packages/core` | Source, tests, samples, evidence, benchmarks, and release scripts for `@workit/core`. | +| `apps/use-cases-site` | GitHub Pages site with executable WorkIt examples. | +| `articles` | Public article drafts and released article materials. | -### Provider Race +## Package Contract -```ts -import { run } from "@workit/core"; - -const result = await run.race([ - run.timeout((ctx) => primary.generate({ signal: ctx.signal }), "5s"), - run.timeout((ctx) => backup.generate({ signal: ctx.signal }), "5s"), -]); -``` +The monorepo layout must not change how users install or import WorkIt. -The first success wins. Losing branches receive `CancelReason { kind: -"race_lost" }`. +Stable consumer paths for this release line: -### Retry With Timeout - -```ts -import { run } from "@workit/core"; - -const receipt = await run.retry( - (ctx) => - run.timeout( - (timeoutCtx) => - chargeCustomer(invoice, { - signal: AbortSignal.any([ctx.signal, timeoutCtx.signal]), - }), - "5s" - ), - { retries: 3 } -); +```txt +@workit/core +@workit/core/ai +@workit/core/channel +@workit/core/diagnostics +@workit/core/observability +@workit/core/otel +@workit/core/worker ``` -The retry policy, timeout, and caller cancellation share one owned execution -path instead of living in separate helper layers. - -### Backpressured Stream - -```ts -import { work } from "@workit/core"; +## Verification -for await (const summary of work(documents()) - .inParallel(8) - .map((doc, ctx) => summarize(doc, { signal: ctx.signal })) - .stream()) { - if (summary.enough) break; -} -``` - -Breaking the loop cancels remaining in-flight work and stops pulling from the -producer. - -### Budgeted Agent Work - -```ts -import { CostBudget, TokenBudget, run, work } from "@workit/core"; - -await run.context.with(CostBudget, { spent: 0, limit: 0.50, unit: "USD" }, () => - run.context.with(TokenBudget, { spent: 0, limit: 100_000, unit: "tokens" }, () => - work(chunks).inParallel(8).do(async (chunk, ctx) => { - ctx.consume(TokenBudget, chunk.estimatedTokens); - ctx.consume(CostBudget, chunk.estimatedCost); - return embed(chunk, { signal: ctx.signal }); - }) - ) -); -``` - -Budget overrun cancels the scope that installed the budget. - -### Worker Boundary - -```ts -import { offload } from "@workit/core/worker"; - -const result = await offload( - new URL("./cpu-worker.js", import.meta.url), - "compute", - input, - { timeout: "2s" } -); -``` - -`AbortController` cannot preempt a tight CPU loop on the main thread. Worker -offload gives CPU-bound or plugin-like work a hard timeout boundary. - -## Worker Offload Boundary - -`offload()` is an execution boundary and a structured-clone boundary. - -Accepted worker inputs include primitives, arrays, plain objects, `Map`, `Set`, -dates, regexps, buffers, and typed arrays. - -Rejected worker inputs include class instances, functions, symbols, custom -prototype objects, inline or remote module URLs, and parent directory segments -in worker paths. - -When `timeout` fires, WorkIt terminates the worker thread. This is different -from cooperative `AbortSignal` cancellation inside the main JavaScript thread. - -## Runnable Samples - -| Sample | What it demonstrates | -|---|---| -| [`samples/progress-parallel.sample.js`](samples/progress-parallel.sample.js) | progress events during bounded parallel work | -| [`samples/race-providers.sample.js`](samples/race-providers.sample.js) | provider race with loser cancellation | -| [`samples/no-orphan.sample.js`](samples/no-orphan.sample.js) | owned background work waits before scope close | -| [`samples/streaming-summarizer.sample.js`](samples/streaming-summarizer.sample.js) | streaming summarization with early stop | -| [`samples/embed-bisection.sample.js`](samples/embed-bisection.sample.js) | bad-batch bisection for embedding pipelines | -| [`samples/supervision.sample.js`](samples/supervision.sample.js) | supervised long-lived work | -| [`samples/worker-offload.sample.js`](samples/worker-offload.sample.js) | worker timeout against non-cooperative CPU work | -| [`samples/budget-rag.sample.js`](samples/budget-rag.sample.js) | request-scoped cost budget | -| [`samples/logging-otel-bridge.sample.js`](samples/logging-otel-bridge.sample.js) | local events bridged to telemetry | - -## Verified Evidence - -WorkIt claims are tied to executable gates. The benchmark timings below are -representative captured runs; the gates assert semantic invariants and budget -thresholds, not exact milliseconds. - -| Evidence | Current result | -|---|---:| -| Unit tests | 214 passing | -| Coverage gate | 100% statements, branches, functions, lines | -| Runtime dependencies | 0 | -| Article benchmark suite | 19/19 passing | -| Core group import | 14,175 B minified / 4,835 B gzip | -| Public bundle | 29,255 B minified / 9,694 B gzip | -| Stream gate | 1,000,000 logical items with bounded producer growth | -| Soak gate | 100,000 logical tasks with bounded concurrency | -| Exporter stress | 100,000 events with bounded queue | - -Representative article-benchmark results: - -| Benchmark | Baseline | WorkIt | -|---|---:|---:| -| Provider race losers after winner | losers continue until their sleeps finish | losers cancelled in scope close | -| Retry after cancellation | 7 extra attempts, 622 ms latency | 0 extra attempts, 1 ms latency | -| Context `.with()` over 5,000 keys | 31.68 ms | 0.014 ms | -| 1B-row source, take 25 | 281 items pulled | 40 items pulled | -| Sampling volume | 1,300 events | 36 events | - -Run the main verification gate: +Run the core gates from the repository root: ```sh npm run verify +npm run test:coverage +npm run check:size +npm run check:package-consumer ``` -`npm run verify` runs type-checking, header and test hygiene, unit tests, -security checks, vulnerability audit, SBOM validation, API and bundle-size -locks, runtime benchmarks, stream and soak gates, exporter stress, -package-consumer fixtures, public-proof validation, worker-contract checks, -release-policy checks, and `npm pack --dry-run`. - -Run the article benchmark suite: - -```sh -npm run bench:articles -``` - -Run the curated publication evidence suite: - -```sh -npm run test:evidence -``` - -Run verification commands sequentially when they depend on `dist/`. Some gates, -including `npm run test:coverage` and `npm run verify`, rebuild or clean the -compiled artifacts. Running them in parallel with `npm run bench:articles` can -delete `dist/` while benchmark processes are importing it. - -Machine-readable reviewer evidence lives in -[`benchmarks/public-proof.json`](benchmarks/public-proof.json), the article -benchmark capture lives in -[`benchmarks/results/articles.latest.json`](benchmarks/results/articles.latest.json), -and the public claim ledger lives in -[`evidence/claims.json`](evidence/claims.json). - -## Security And Release Integrity - -| Guarantee | Enforcement | -|---|---| -| Runtime core has no production dependencies | package metadata and security gate | -| Core does not import networking modules | static no-network gate | -| Published package includes an SBOM | CycloneDX SBOM generation and validation | -| Release workflow uses provenance controls | release-policy gate | -| Public API and bundle size are locked | API and size gates | -| Consumer fixtures install the package artifact | package-consumer gate | - -Additional repository controls include pinned dev dependencies, vulnerability -audit, SHA-pinned GitHub Actions, OSSF Scorecard workflow, CODEOWNERS, -Dependabot, and signed release tag policy. - -Security reports should follow [`SECURITY.md`](SECURITY.md). - -## Runtime Support - -Supported: - -- Node.js `>=20.11` -- ESM consumers -- CommonJS consumers -- strict TypeScript consumers -- AWS Lambda-shaped handlers -- Azure Functions-shaped handlers -- Next.js route-shaped handlers -- Express, Fastify, tRPC, and Vercel AI SDK fixtures - -Unsupported today: - -- browser client runtime -- Cloudflare Workers -- Next.js Edge / Vercel Edge - -Unsupported runtimes resolve to an explicit unsupported boundary. An edge-safe -context runtime is future work. - -## When To Use Alternatives - -| Tool | Prefer it when | Prefer WorkIt when | -|---|---|---| -| Native `Promise` | One async value is enough | Work needs ownership, cancellation, cleanup, or diagnostics | -| Manual scope object | The lifecycle is local and small enough to audit in one file | The lifecycle becomes a reusable cross-module contract | -| `p-limit` | You only need a tiny semaphore | Bounded work also needs lifecycle semantics | -| `p-map` | You need a focused concurrent map | Mapping needs retry, timeout, stream policy, or partial results | -| RxJS | You are modelling rich event streams | You are modelling owned async task lifecycles | -| Bottleneck | You need distributed reservoirs or rate limits | You need local structured concurrency | -| Effection | You want structured concurrency via operations/generators | You want plain `async`/`await` task functions | -| Effect-TS | You want a full effect system | You want owned async work without migrating to a DSL | - -These comparisons are about ownership and composition. Some libraries expose -cancellation hooks or queue controls; WorkIt's claim is that cancellation, -cleanup, retry, timeout, budgets, backpressure, and diagnostics compose under -one owner. - -## Migration Notes - -These are orientation notes, not codemods. Keep the old tool when it owns the -problem better. - -### From p-limit - -Use `run.pool()` or `work(items).inParallel(n)` when the semaphore also needs -sibling cancellation, retry, timeout, cleanup, progress, or partial-result -policy under one owner. - -### From p-map - -Use `work(items).inParallel(n).do(fn)` for concurrent maps that need the same -lifecycle semantics as the caller. Keep `p-map` for a small one-file map where -concurrency is the only concern. - -### From RxJS - -Keep RxJS for rich observable graphs. Use WorkIt when the problem is owned task -lifecycle: request fan-out, provider racing, agent tools, bounded streams, or -cleanup around async work. - -### From Bottleneck - -Keep Bottleneck for distributed reservoirs and external rate-limit state. Use -WorkIt for local process ownership where bounded concurrency must compose with -cancel, retry, timeout, budgets, and diagnostics. - -## Documentation - -| Resource | Purpose | -|---|---| -| [`articles/`](articles/) | Narrative articles with examples and benchmark discussion | -| [`benchmarks/articles/`](benchmarks/articles/) | Reproducible article benchmark suite | -| [`evidence/`](evidence/) | Machine-readable claim ledger and evidence policy | -| [`tests/evidence/`](tests/evidence/) | Curated publication evidence proofs | -| [`samples/`](samples/) | Runnable examples against the compiled package | -| [`SECURITY.md`](SECURITY.md) | Security reporting and release integrity policy | - -## Contributing - -Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a pull request. - -Before submitting code: +Run the site gates from the repository root: ```sh -npm run verify -npm run test:coverage -npm run bench:articles -npm run test:evidence +npm run site:build ``` - -Run these commands sequentially. Several verification commands clean and rebuild -`dist/`, while article benchmarks import the compiled package artifact. - -Bug reports should include the WorkIt version, Node.js version, reproduction -code, and whether the failure occurs from source or the installed package. - -## License - -Apache-2.0. See [`LICENSE`](LICENSE). diff --git a/apps/use-cases-site/package-lock.json b/apps/use-cases-site/package-lock.json index c3b1a1f..d2ecf01 100644 --- a/apps/use-cases-site/package-lock.json +++ b/apps/use-cases-site/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@workit/core": "file:../..", + "@workit/core": "file:../../packages/core", "clsx": "2.1.1", "lucide-react": "1.17.0", "react": "19.2.7", @@ -29,9 +29,9 @@ "@rollup/rollup-win32-x64-msvc": "4.61.0" } }, - "../..": { + "../../packages/core": { "name": "@workit/core", - "version": "0.1.3", + "version": "0.1.5", "license": "Apache-2.0", "devDependencies": { "@opentelemetry/api": "1.9.1", @@ -1617,7 +1617,7 @@ } }, "node_modules/@workit/core": { - "resolved": "../..", + "resolved": "../../packages/core", "link": true }, "node_modules/baseline-browser-mapping": { diff --git a/apps/use-cases-site/package.json b/apps/use-cases-site/package.json index 5231b02..1bc94f0 100644 --- a/apps/use-cases-site/package.json +++ b/apps/use-cases-site/package.json @@ -18,7 +18,7 @@ "preview": "vite preview" }, "dependencies": { - "@workit/core": "file:../..", + "@workit/core": "file:../../packages/core", "clsx": "2.1.1", "lucide-react": "1.17.0", "react": "19.2.7", diff --git a/apps/use-cases-site/scripts/generate-evidence.mjs b/apps/use-cases-site/scripts/generate-evidence.mjs index 38f7027..2e21334 100644 --- a/apps/use-cases-site/scripts/generate-evidence.mjs +++ b/apps/use-cases-site/scripts/generate-evidence.mjs @@ -16,10 +16,10 @@ const repoRoot = resolve(siteRoot, "..", ".."); const outputPath = resolve(siteRoot, "src", "data", "generated", "evidence-snapshots.json"); const samples = [ - { id: "agent-tree-cancel", path: "samples/agent-tree-cancel.sample.js" }, - { id: "conversation-agent", path: "samples/conversation-agent.sample.js" }, - { id: "race-providers", path: "samples/race-providers.sample.js" }, - { id: "budget-rag", path: "samples/budget-rag.sample.js" }, + { id: "agent-tree-cancel", path: "packages/core/samples/agent-tree-cancel.sample.js" }, + { id: "conversation-agent", path: "packages/core/samples/conversation-agent.sample.js" }, + { id: "race-providers", path: "packages/core/samples/race-providers.sample.js" }, + { id: "budget-rag", path: "packages/core/samples/budget-rag.sample.js" }, ]; if (process.platform === "win32") { diff --git a/apps/use-cases-site/scripts/test-data-contract.mjs b/apps/use-cases-site/scripts/test-data-contract.mjs index b76161e..c50c861 100644 --- a/apps/use-cases-site/scripts/test-data-contract.mjs +++ b/apps/use-cases-site/scripts/test-data-contract.mjs @@ -23,7 +23,7 @@ const exampleContracts = [ { id: "vibe-coding-agent", sampleId: "agent-tree-cancel", - samplePath: "samples/agent-tree-cancel.sample.js", + samplePath: "packages/core/samples/agent-tree-cancel.sample.js", liveReceipt: [ "runtime: @workit/core", "sample: agent-tree-cancel", @@ -37,7 +37,7 @@ const exampleContracts = [ { id: "conversation-agent", sampleId: "conversation-agent", - samplePath: "samples/conversation-agent.sample.js", + samplePath: "packages/core/samples/conversation-agent.sample.js", liveReceipt: [ "runtime: @workit/core", "sample: conversation-agent", @@ -51,7 +51,7 @@ const exampleContracts = [ { id: "provider-fallback", sampleId: "race-providers", - samplePath: "samples/race-providers.sample.js", + samplePath: "packages/core/samples/race-providers.sample.js", liveReceipt: [ "runtime: @workit/core", "sample: race-providers", @@ -63,7 +63,7 @@ const exampleContracts = [ { id: "rag-pipeline", sampleId: "budget-rag", - samplePath: "samples/budget-rag.sample.js", + samplePath: "packages/core/samples/budget-rag.sample.js", liveReceipt: [ "runtime: @workit/core", "sample: budget-rag", diff --git a/apps/use-cases-site/server/runners.mjs b/apps/use-cases-site/server/runners.mjs index 0821872..57c503c 100644 --- a/apps/use-cases-site/server/runners.mjs +++ b/apps/use-cases-site/server/runners.mjs @@ -59,7 +59,7 @@ async function runAgentTree() { `reason.tag: ${reason.tag}`, `cleanups: ${cleanups.sort().join(", ")}`, ], - code: await readSample("samples/agent-tree-cancel.sample.js"), + code: await readSample("packages/core/samples/agent-tree-cancel.sample.js"), }; } @@ -87,7 +87,7 @@ async function runProviderFallback() { `winner: ${result.provider}`, `cancelledProviders: ${cancelledProviders.sort().join(", ")}`, ], - code: await readSample("samples/race-providers.sample.js"), + code: await readSample("packages/core/samples/race-providers.sample.js"), }; } @@ -153,7 +153,7 @@ async function runRagPipeline() { `limit: ${finalBudget.limit}`, `audit.sources: ${audits[0]?.sources ?? 0}`, ], - code: await readSample("samples/budget-rag.sample.js"), + code: await readSample("packages/core/samples/budget-rag.sample.js"), }; } @@ -209,7 +209,7 @@ async function runConversationAgent() { `memoryWrites: ${memoryWrites}`, `cleanups: ${cleanups.sort().join(", ")}`, ], - code: await readSample("samples/conversation-agent.sample.js"), + code: await readSample("packages/core/samples/conversation-agent.sample.js"), }; } diff --git a/apps/use-cases-site/src/data/generated/evidence-snapshots.json b/apps/use-cases-site/src/data/generated/evidence-snapshots.json index cd30c45..4513cdd 100644 --- a/apps/use-cases-site/src/data/generated/evidence-snapshots.json +++ b/apps/use-cases-site/src/data/generated/evidence-snapshots.json @@ -3,7 +3,7 @@ "generatedBy": "apps/use-cases-site/scripts/generate-evidence.mjs", "samples": { "agent-tree-cancel": { - "path": "samples/agent-tree-cancel.sample.js", + "path": "packages/core/samples/agent-tree-cancel.sample.js", "result": { "sample": "agent-tree-cancel", "cancelled": [ @@ -24,7 +24,7 @@ "source": "/**\n * Agent tree cancellation sample.\n *\n * @author Admilson B. F. Cossa\n * SPDX-License-Identifier: Apache-2.0\n *\n * Demonstrates parent-scope cancellation across multiple in-flight tool tasks.\n * Each task receives the same typed cancellation reason and runs cleanup.\n */\n\nimport assert from \"node:assert/strict\";\nimport { CancellationError, run } from \"../dist/index.js\";\n\nconst cancelled = [];\nconst cleanups = [];\n\nawait run.scope(async (scope) => {\n const tools = [\"search\", \"browser\", \"code\"];\n const handles = tools.map((name) => scope.spawn(async (ctx) => {\n ctx.defer(() => cleanups.push(name));\n try {\n await sleep(1_000, ctx.signal);\n return name;\n } catch (err) {\n if (err instanceof CancellationError) {\n cancelled.push({ name, reason: err.reason });\n }\n throw err;\n }\n }, { name: `tool.${name}`, kind: \"tool\" }));\n\n await sleep(5, scope.signal);\n scope.cancel({ kind: \"manual\", tag: \"user_stopped_agent\" });\n await Promise.allSettled(handles);\n}, { name: \"agent.tree\" });\n\nassert.deepEqual(cancelled.map((item) => item.name).sort(), [\"browser\", \"code\", \"search\"]);\nassert.deepEqual(cleanups.sort(), [\"browser\", \"code\", \"search\"]);\nassert.ok(cancelled.every((item) => item.reason.tag === \"user_stopped_agent\"));\n\nprocess.stdout.write(`${JSON.stringify({\n sample: \"agent-tree-cancel\",\n cancelled: cancelled.map((item) => item.name).sort(),\n reason: cancelled[0]?.reason,\n cleanups: cleanups.sort(),\n})}\\n`);\n\nfunction sleep(ms, signal) {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n reject(signal.reason);\n }, { once: true });\n });\n}\n" }, "conversation-agent": { - "path": "samples/conversation-agent.sample.js", + "path": "packages/core/samples/conversation-agent.sample.js", "result": { "sample": "conversation-agent", "tokens": [ @@ -47,7 +47,7 @@ "source": "/**\n * Conversation agent sample.\n *\n * @author Admilson B. F. Cossa\n * SPDX-License-Identifier: Apache-2.0\n *\n * Runs a chat turn with streaming, tool calls, memory write, and cleanup under\n * one WorkIt scope.\n */\n\nimport assert from \"node:assert/strict\";\nimport { run } from \"../dist/index.js\";\n\nconst tokens = [];\nconst cleanups = [];\nlet memoryWrites = 0;\n\nconst toolResults = await run.scope(async (scope) => {\n const stream = scope.spawn(async (ctx) => {\n ctx.defer(() => cleanups.push(\"stream\"));\n for (const token of [\"plan\", \"search\", \"edit\", \"reply\"]) {\n await sleep(1, ctx.signal);\n tokens.push(token);\n }\n return tokens.length;\n }, { name: \"llm.stream\", kind: \"llm\" });\n\n const search = scope.spawn(async (ctx) => {\n ctx.defer(() => cleanups.push(\"tools\"));\n await sleep(2, ctx.signal);\n return \"search:2\";\n }, { name: \"tool.search\", kind: \"tool\" });\n\n const repo = scope.spawn(async (ctx) => {\n await sleep(3, ctx.signal);\n return \"repo:clean\";\n }, { name: \"tool.repo\", kind: \"tool\" });\n\n const memory = scope.spawn(async (ctx) => {\n ctx.defer(() => cleanups.push(\"memory\"));\n await sleep(4, ctx.signal);\n memoryWrites++;\n return memoryWrites;\n }, { name: \"memory.write\", kind: \"io\" });\n\n const results = await Promise.all([search, repo]);\n await Promise.all([stream, memory]);\n return results;\n}, { name: \"chat.turn\" });\n\nassert.deepEqual(tokens, [\"plan\", \"search\", \"edit\", \"reply\"]);\nassert.deepEqual(toolResults, [\"search:2\", \"repo:clean\"]);\nassert.equal(memoryWrites, 1);\nassert.deepEqual(cleanups.sort(), [\"memory\", \"stream\", \"tools\"]);\n\nprocess.stdout.write(`${JSON.stringify({\n sample: \"conversation-agent\",\n tokens,\n toolResults,\n memoryWrites,\n cleanups: cleanups.sort(),\n})}\\n`);\n\nfunction sleep(ms, signal) {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n reject(signal.reason);\n }, { once: true });\n });\n}\n" }, "race-providers": { - "path": "samples/race-providers.sample.js", + "path": "packages/core/samples/race-providers.sample.js", "result": { "sample": "race-providers", "winner": "anthropic", @@ -59,7 +59,7 @@ "source": "/**\n * Provider race sample.\n *\n * @author Admilson B. F. Cossa\n * SPDX-License-Identifier: Apache-2.0\n *\n * Races three provider calls and cancels the losing requests through the shared\n * task signal.\n */\n\nimport assert from \"node:assert/strict\";\nimport { CancellationError, run } from \"../dist/index.js\";\n\nconst cancelledProviders = [];\n\nconst winner = await run.race([\n provider(\"openai\", 50),\n provider(\"anthropic\", 10),\n provider(\"gemini\", 80),\n]);\n\nassert.equal(winner.provider, \"anthropic\");\nassert.deepEqual(cancelledProviders.sort(), [\"gemini\", \"openai\"]);\n\nprocess.stdout.write(`${JSON.stringify({\n sample: \"race-providers\",\n winner: winner.provider,\n cancelledProviders: cancelledProviders.sort(),\n})}\\n`);\n\nfunction provider(name, latencyMs) {\n return async (ctx) => {\n try {\n await sleep(latencyMs, ctx.signal);\n return { provider: name, text: `${name}:ok` };\n } catch (err) {\n if (err instanceof CancellationError) cancelledProviders.push(name);\n throw err;\n }\n };\n}\n\nfunction sleep(ms, signal) {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal.addEventListener(\"abort\", () => {\n clearTimeout(timer);\n reject(signal.reason);\n }, { once: true });\n });\n}\n" }, "budget-rag": { - "path": "samples/budget-rag.sample.js", + "path": "packages/core/samples/budget-rag.sample.js", "result": { "sample": "budget-rag", "answer": "answer:keyword:structured concurrency", diff --git a/apps/use-cases-site/src/data/useCases.ts b/apps/use-cases-site/src/data/useCases.ts index bc3bb20..99799a2 100644 --- a/apps/use-cases-site/src/data/useCases.ts +++ b/apps/use-cases-site/src/data/useCases.ts @@ -169,13 +169,13 @@ export const useCases: UseCase[] = [ evidence: [ { claim: "Parent cancellation reaches in-flight tool work.", - path: "samples/agent-tree-cancel.sample.js", + path: "packages/core/samples/agent-tree-cancel.sample.js", invariant: "search, browser, and code tasks all cancel with the same reason.", status: "tracked", }, { claim: "Cleanup runs after cancellation.", - path: "tests/unit/sanity.test.js", + path: "packages/core/tests/unit/sanity.test.js", invariant: "deferred cleanup executes for cancelled scope work.", status: "tracked", }, @@ -282,13 +282,13 @@ export const useCases: UseCase[] = [ evidence: [ { claim: "Streaming work can be scoped with the same owner as tool work.", - path: "samples/conversation-agent.sample.js", + path: "packages/core/samples/conversation-agent.sample.js", invariant: "stream, tool, memory, and cleanup work stay under one runtime owner.", status: "tracked", }, { claim: "Agent trees expose cancellable child lifecycles.", - path: "samples/agent-tree-cancel.sample.js", + path: "packages/core/samples/agent-tree-cancel.sample.js", invariant: "in-flight child tasks receive the parent cancellation reason.", status: "tracked", }, @@ -379,7 +379,7 @@ export const useCases: UseCase[] = [ evidence: [ { claim: "Provider race cancels losing requests.", - path: "samples/race-providers.sample.js", + path: "packages/core/samples/race-providers.sample.js", invariant: "anthropic wins and openai/gemini are cancelled.", status: "tracked", }, @@ -486,7 +486,7 @@ export const useCases: UseCase[] = [ evidence: [ { claim: "RAG budget and background audit work remain attached to the request owner.", - path: "samples/budget-rag.sample.js", + path: "packages/core/samples/budget-rag.sample.js", invariant: "answer is produced, budget spent is 8, audit record is written.", status: "tracked", }, diff --git a/package-lock.json b/package-lock.json index b8ab358..9f87e42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,34 +1,16 @@ { - "name": "@workit/core", - "version": "0.1.4", + "name": "workit", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@workit/core", - "version": "0.1.4", + "name": "workit", + "version": "0.1.5", "license": "Apache-2.0", - "devDependencies": { - "@opentelemetry/api": "1.9.1", - "@types/node": "25.6.1", - "@vitest/coverage-v8": "4.1.5", - "esbuild": "0.28.0", - "fast-check": "4.7.0", - "typescript": "6.0.3", - "vitest": "4.1.5", - "wrangler": "4.89.1" - }, - "engines": { - "node": ">=20.11" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.1" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - } - } + "workspaces": [ + "packages/core" + ] }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", @@ -225,40 +207,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", @@ -1784,6 +1732,10 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@workit/core": { + "resolved": "packages/core", + "link": true + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3498,6 +3450,32 @@ "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } + }, + "packages/core": { + "name": "@workit/core", + "version": "0.1.5", + "license": "Apache-2.0", + "devDependencies": { + "@opentelemetry/api": "1.9.1", + "@types/node": "25.6.1", + "@vitest/coverage-v8": "4.1.5", + "esbuild": "0.28.0", + "fast-check": "4.7.0", + "typescript": "6.0.3", + "vitest": "4.1.5", + "wrangler": "4.89.1" + }, + "engines": { + "node": ">=20.11" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.1" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 978f93f..e23ab24 100644 --- a/package.json +++ b/package.json @@ -1,157 +1,73 @@ { - "name": "@workit/core", - "version": "0.1.4", - "description": "Structured concurrency runtime for TypeScript.", + "name": "workit", + "version": "0.1.5", + "private": true, + "description": "WorkIt monorepo.", "type": "module", - "private": false, "license": "Apache-2.0", "author": "Admilson B. F. Cossa", - "repository": { - "type": "git", - "url": "https://github.com/WorkRuntime/workit" - }, - "bugs": { - "url": "https://github.com/WorkRuntime/workit/issues" - }, - "homepage": "https://github.com/WorkRuntime/workit#readme", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "node": { - "import": "./dist/index.js", - "require": "./dist-cjs/index.cjs" - }, - "default": "./dist/runtime/unsupported.js" - }, - "./ai": { - "types": "./dist/ai/index.d.ts", - "node": { - "import": "./dist/ai/index.js", - "require": "./dist-cjs/ai/index.cjs" - }, - "default": "./dist/runtime/unsupported.js" - }, - "./channel": { - "types": "./dist/channel/index.d.ts", - "import": "./dist/channel/index.js", - "require": "./dist-cjs/channel/index.cjs" - }, - "./observability": { - "types": "./dist/observability/index.d.ts", - "import": "./dist/observability/index.js", - "require": "./dist-cjs/observability/index.cjs" - }, - "./diagnostics": { - "types": "./dist/diagnostics/index.d.ts", - "import": "./dist/diagnostics/index.js", - "require": "./dist-cjs/diagnostics/index.cjs" - }, - "./otel": { - "types": "./dist/otel/index.d.ts", - "import": "./dist/otel/index.js", - "require": "./dist-cjs/otel/index.cjs" - }, - "./worker": { - "types": "./dist/worker/index.d.ts", - "node": { - "import": "./dist/worker/index.js" - }, - "default": "./dist/runtime/unsupported.js" - } - }, - "sideEffects": false, - "publishConfig": { - "access": "public" - }, - "files": [ - "CODE_OF_CONDUCT.md", - "CONTRIBUTING.md", - "dist", - "dist-cjs", - "SECURITY.md" + "workspaces": [ + "packages/core" ], "scripts": { - "clean": "node -e \"const fs=require('node:fs'); for (const p of ['dist','dist-cjs','coverage']) fs.rmSync(p,{recursive:true,force:true});\"", - "build": "npm run clean && tsc && node scripts/build-cjs.mjs && node scripts/generate-sbom.mjs", - "typecheck": "tsc --noEmit", - "check:no-network": "node scripts/check-no-network.mjs", - "check:headers": "node scripts/check-file-headers.mjs", - "check:security": "node scripts/check-security.mjs", - "check:vulnerabilities": "node scripts/check-vulnerabilities.mjs", - "check:sbom": "npm run build && node scripts/check-sbom.mjs", - "check:tests": "node scripts/check-tests.mjs", - "check:api": "npm run build && node scripts/check-api-surface.mjs", - "check:size": "npm run build && node scripts/check-bundle-size.mjs", - "report:size": "npm run build && node scripts/report-bundle-size.mjs", - "check:benchmark": "npm run build && node scripts/check-benchmark.mjs", - "check:context-performance": "npm run build && node scripts/check-context-performance.mjs", - "check:1b": "npm run build && node scripts/check-1b-benchmark.mjs", - "check:leak": "npm run build && node --expose-gc scripts/check-leak.mjs", - "check:stream-memory": "npm run build && node --expose-gc scripts/check-stream-memory.mjs", - "check:soak": "npm run build && node --expose-gc scripts/check-soak.mjs", - "check:exporter-stress": "npm run build && node --expose-gc scripts/check-exporter-stress.mjs", - "check:package-consumer": "npm run build && node scripts/check-package-consumer.mjs", - "check:claims": "npm run build && node scripts/check-claim-fixtures.mjs", - "check:public-proof": "node scripts/check-public-proof.mjs", - "check:worker-contract": "node scripts/check-worker-contract-docs.mjs", - "check:release-policy": "npm run build && node scripts/check-release-provenance.mjs", - "check:release": "npm run build && node scripts/check-release-provenance.mjs --registry-dry-run", - "bench:articles": "node benchmarks/articles/run-all.mjs", - "bench:articles:repeated": "node benchmarks/articles/run-repeated.mjs", + "clean": "npm --workspace @workit/core run clean", + "build": "npm --workspace @workit/core run build", + "typecheck": "npm --workspace @workit/core run typecheck", + "check:no-network": "npm --workspace @workit/core run check:no-network", + "check:headers": "npm --workspace @workit/core run check:headers", + "check:security": "npm --workspace @workit/core run check:security", + "check:vulnerabilities": "npm --workspace @workit/core run check:vulnerabilities", + "check:sbom": "npm --workspace @workit/core run check:sbom", + "check:tests": "npm --workspace @workit/core run check:tests", + "check:api": "npm --workspace @workit/core run check:api", + "check:size": "npm --workspace @workit/core run check:size", + "report:size": "npm --workspace @workit/core run report:size", + "check:benchmark": "npm --workspace @workit/core run check:benchmark", + "check:context-performance": "npm --workspace @workit/core run check:context-performance", + "check:1b": "npm --workspace @workit/core run check:1b", + "check:leak": "npm --workspace @workit/core run check:leak", + "check:stream-memory": "npm --workspace @workit/core run check:stream-memory", + "check:soak": "npm --workspace @workit/core run check:soak", + "check:exporter-stress": "npm --workspace @workit/core run check:exporter-stress", + "check:package-consumer": "npm --workspace @workit/core run check:package-consumer", + "check:claims": "npm --workspace @workit/core run check:claims", + "check:public-proof": "npm --workspace @workit/core run check:public-proof", + "check:worker-contract": "npm --workspace @workit/core run check:worker-contract", + "check:release-policy": "npm --workspace @workit/core run check:release-policy", + "check:release": "npm --workspace @workit/core run check:release", + "bench:articles": "npm --workspace @workit/core run bench:articles", + "bench:articles:repeated": "npm --workspace @workit/core run bench:articles:repeated", "site:dev": "npm --prefix apps/use-cases-site run dev", "site:build": "npm --prefix apps/use-cases-site run build", "site:preview": "npm --prefix apps/use-cases-site run preview", - "test:evidence": "node tests/evidence/run-all.mjs", - "test:property": "npm run build && vitest run tests/property", - "pack:dry": "npm pack --dry-run --json", - "sample:1b": "npm run build && node samples/1b-stream.sample.js", - "sample:concurrency": "npm run build && node samples/concurrency-budget.sample.js", - "sample:progress": "npm run build && node samples/progress-parallel.sample.js", - "sample:cancel": "npm run build && node samples/cancel-reason.sample.js", - "sample:timeout": "npm run build && node samples/timeout-stop.sample.js", - "sample:no-orphan": "npm run build && node samples/no-orphan.sample.js", - "sample:all": "npm run build && node samples/safer-promise-all.sample.js", - "sample:agent": "npm run build && node samples/agent-tree-cancel.sample.js", - "sample:race": "npm run build && node samples/race-providers.sample.js", - "sample:rag": "npm run build && node samples/budget-rag.sample.js", - "sample:batch": "npm run build && node samples/batch-upload.sample.js", - "sample:stream": "npm run build && node samples/streaming-summarizer.sample.js", - "sample:embed100k": "npm run build && node samples/embed-100k.sample.js", - "sample:bisection": "npm run build && node samples/embed-bisection.sample.js", - "sample:stt-disconnect": "npm run build && node samples/stt-disconnect.sample.js", - "sample:supervise": "npm run build && node samples/supervision.sample.js", - "sample:worker": "npm run build && node samples/worker-offload.sample.js", - "sample:aws": "npm run build && node samples/aws-lambda-handler.sample.js", - "sample:azure": "npm run build && node samples/azure-functions-handler.sample.js", - "sample:next": "npm run build && node samples/next-server-route.sample.js", - "sample:otel": "npm run build && node samples/otel-adapter.sample.js", - "sample:logging": "npm run build && node samples/logging-otel-bridge.sample.js", - "soak:24h": "npm run build && node --expose-gc scripts/soak-24h.mjs", - "test": "npm run build && vitest run", - "test:coverage": "npm run build && vitest run --coverage", - "verify": "npm run typecheck && npm run check:no-network && npm run check:headers && npm run check:tests && npm test && npm run check:security && npm run check:vulnerabilities && npm run check:sbom && npm run check:api && npm run check:size && npm run check:benchmark && npm run check:context-performance && npm run check:1b && npm run check:leak && npm run check:stream-memory && npm run check:soak && npm run check:exporter-stress && npm run check:package-consumer && npm run check:claims && npm run check:public-proof && npm run check:worker-contract && npm run check:release-policy && npm run pack:dry" - }, - "engines": { - "node": ">=20.11" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.1" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - } - }, - "devDependencies": { - "@opentelemetry/api": "1.9.1", - "@types/node": "25.6.1", - "@vitest/coverage-v8": "4.1.5", - "esbuild": "0.28.0", - "fast-check": "4.7.0", - "typescript": "6.0.3", - "vitest": "4.1.5", - "wrangler": "4.89.1" + "test:evidence": "npm --workspace @workit/core run test:evidence", + "test:property": "npm --workspace @workit/core run test:property", + "pack:dry": "npm --workspace @workit/core run pack:dry", + "sample:1b": "npm --workspace @workit/core run sample:1b", + "sample:concurrency": "npm --workspace @workit/core run sample:concurrency", + "sample:progress": "npm --workspace @workit/core run sample:progress", + "sample:cancel": "npm --workspace @workit/core run sample:cancel", + "sample:timeout": "npm --workspace @workit/core run sample:timeout", + "sample:no-orphan": "npm --workspace @workit/core run sample:no-orphan", + "sample:all": "npm --workspace @workit/core run sample:all", + "sample:agent": "npm --workspace @workit/core run sample:agent", + "sample:race": "npm --workspace @workit/core run sample:race", + "sample:rag": "npm --workspace @workit/core run sample:rag", + "sample:batch": "npm --workspace @workit/core run sample:batch", + "sample:stream": "npm --workspace @workit/core run sample:stream", + "sample:embed100k": "npm --workspace @workit/core run sample:embed100k", + "sample:bisection": "npm --workspace @workit/core run sample:bisection", + "sample:stt-disconnect": "npm --workspace @workit/core run sample:stt-disconnect", + "sample:supervise": "npm --workspace @workit/core run sample:supervise", + "sample:worker": "npm --workspace @workit/core run sample:worker", + "sample:aws": "npm --workspace @workit/core run sample:aws", + "sample:azure": "npm --workspace @workit/core run sample:azure", + "sample:next": "npm --workspace @workit/core run sample:next", + "sample:otel": "npm --workspace @workit/core run sample:otel", + "sample:logging": "npm --workspace @workit/core run sample:logging", + "soak:24h": "npm --workspace @workit/core run soak:24h", + "test": "npm --workspace @workit/core run test", + "test:coverage": "npm --workspace @workit/core run test:coverage", + "verify": "npm --workspace @workit/core run verify" } } diff --git a/packages/core/CODE_OF_CONDUCT.md b/packages/core/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a75fef8 --- /dev/null +++ b/packages/core/CODE_OF_CONDUCT.md @@ -0,0 +1,80 @@ + + +# WorkIt Code of Conduct + +## Purpose + +WorkIt is built for professional, reliable software work. The project expects +technical disagreement to stay constructive, evidence-based, and respectful. + +## Scope + +This code of conduct applies to project spaces, including the repository, +issues, pull requests, discussions, release channels, community chat, events, +and any other official project forum. + +## Expected Behavior + +- Be respectful and direct. +- Discuss ideas, designs, code, and evidence rather than attacking people. +- Give and receive review feedback with care. +- Assume good intent where reasonable, and ask for clarification before + escalating conflict. +- Respect maintainers' time, project priorities, and release boundaries. +- Disclose conflicts of interest when they affect project decisions. +- Keep private information private. + +## Unacceptable Behavior + +- Harassment, threats, intimidation, stalking, or sustained disruption. +- Discriminatory language or conduct. +- Sexualized language, imagery, or unwanted attention. +- Public or private sharing of another person's private information without + explicit permission. +- Personal attacks, insults, or repeated bad-faith arguments. +- Attempts to pressure maintainers into unsafe releases, hidden behavior, or + unsupported claims. +- Spam, coordinated manipulation, or abuse of project infrastructure. + +## Reporting + +Report concerns through the private contact channel listed by the project +maintainers. If the project is hosted on GitHub and private vulnerability or +maintainer contact features are enabled, use those channels when appropriate. + +Reports should include: + +- the behavior or incident being reported +- where and when it happened +- relevant links or screenshots, if safe to share +- whether immediate action is needed + +Maintainers should handle reports with confidentiality, minimize access to +sensitive details, and avoid public disclosure unless required for safety or +project integrity. + +## Enforcement + +Maintainers may take action proportional to the behavior and risk, including: + +- a private clarification or warning +- moderation of comments or discussions +- temporary or permanent restriction from project spaces +- rejection or closure of issues, pull requests, or discussions +- escalation to platform administrators when required + +Enforcement decisions should be based on observable behavior, project safety, +and the need to keep collaboration productive. + +## No Retaliation + +Retaliation against a person who reports a concern in good faith is not +acceptable. Maintainers should treat retaliation as a separate conduct issue. + +## Maintainer Responsibility + +Maintainers are responsible for applying this code consistently and for +correcting their own behavior when they fall short of it. diff --git a/packages/core/CONTRIBUTING.md b/packages/core/CONTRIBUTING.md new file mode 100644 index 0000000..5715117 --- /dev/null +++ b/packages/core/CONTRIBUTING.md @@ -0,0 +1,43 @@ + +# Contributing + +WorkIt accepts changes only when they preserve the runtime invariants and pass +the full verification gate. + +## Development Contract + +- Keep the core package local-first: no network clients in core. +- Keep runtime dependencies at zero. +- Add or update tests before changing behavior. +- Keep public API changes intentional and reflected in the API-surface gate. +- Do not commit temporary tests, debug output, generated coverage, or private docs. +- Do not claim browser, edge, or hosted cloud support unless an executable + fixture proves the real runtime path. + +## Verification + +Run before proposing a release-quality change: + +```sh +npm run test:coverage +npm run verify +``` + +Coverage must remain at 100% for statements, branches, functions, and lines. + +## Commit Style + +Use small scoped commits. Prefer prefixes such as: + +- `runtime:` +- `observability:` +- `ai:` +- `samples:` +- `tests:` +- `release:` +- `security:` + +Each commit should contain one coherent change and the tests needed to prove it. diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 0000000..f450318 --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,183 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work by +the copyright owner or by an individual or Legal Entity authorized to submit on +behalf of the copyright owner. For the purposes of this definition, "submitted" +means any form of electronic, verbal, or written communication sent to the +Licensor or its representatives, including but not limited to communication on +electronic mailing lists, source code control systems, and issue tracking systems +that are managed by, or on behalf of, the Licensor for the purpose of discussing +and improving the Work, but excluding communication that is conspicuously marked +or otherwise designated in writing by the copyright owner as "Not a +Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this +License, each Contributor hereby grants to You a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright license to +reproduce, prepare Derivative Works of, publicly display, publicly perform, +sublicense, and distribute the Work and such Derivative Works in Source or Object +form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, +each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise +transfer the Work, where such license applies only to those patent claims +licensable by such Contributor that are necessarily infringed by their +Contribution(s) alone or by combination of their Contribution(s) with the Work to +which such Contribution(s) was submitted. If You institute patent litigation +against any entity (including a cross-claim or counterclaim in a lawsuit) +alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses +granted to You under this License for that Work shall terminate as of the date +such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or +Derivative Works thereof in any medium, with or without modifications, and in +Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy of +this License; and + +(b) You must cause any modified files to carry prominent notices stating that You +changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You +distribute, all copyright, patent, trademark, and attribution notices from the +Source form of the Work, excluding those notices that do not pertain to any part +of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, then +any Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the Derivative +Works; within the Source form or documentation, if provided along with the +Derivative Works; or, within a display generated by the Derivative Works, if and +wherever such third-party notices normally appear. + +The contents of the NOTICE file are for informational purposes only and do not +modify the License. You may add Your own attribution notices within Derivative +Works that You distribute, alongside or as an addendum to the NOTICE text from +the Work, provided that such additional attribution notices cannot be construed +as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any +Contribution intentionally submitted for inclusion in the Work by You to the +Licensor shall be under the terms and conditions of this License, without any +additional terms or conditions. Notwithstanding the above, nothing herein shall +supersede or modify the terms of any separate license agreement you may have +executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, +trademarks, service marks, or product names of the Licensor, except as required +for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in +writing, Licensor provides the Work (and each Contributor provides its +Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied, including, without limitation, any warranties or +conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks +associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in +tort (including negligence), contract, or otherwise, unless required by +applicable law (such as deliberate and grossly negligent acts) or agreed to in +writing, shall any Contributor be liable to You for damages, including any +direct, indirect, special, incidental, or consequential damages of any character +arising as a result of this License or out of the use or inability to use the +Work (including but not limited to damages for loss of goodwill, work stoppage, +computer failure or malfunction, or any and all other commercial damages or +losses), even if such Contributor has been advised of the possibility of such +damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or +Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations and/or +rights consistent with this License. However, in accepting such obligations, You +may act only on Your own behalf and on Your sole responsibility, not on behalf of +any other Contributor, and only if You agree to indemnify, defend, and hold each +Contributor harmless for any liability incurred by, or claims asserted against, +such Contributor by reason of your accepting any such warranty or additional +liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + +Copyright 2026 Admilson B. F. Cossa + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..f93f089 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,588 @@ + + +# WorkIt + +[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) +[![Node](https://img.shields.io/badge/node-%3E%3D20.11-brightgreen)](package.json) +[![Runtime deps](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)](package.json) +[![Verification](https://img.shields.io/badge/verify-green-brightgreen)](#verified-evidence) +[![Article benches](https://img.shields.io/badge/article%20benches-19%2F19-brightgreen)](benchmarks/articles/) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12807/badge)](https://www.bestpractices.dev/projects/12807) + +Structured concurrency for TypeScript systems that need owned async work: +bounded parallelism, cancellation, cleanup, retries, timeouts, budgets, +backpressure, worker offload, and observable task lifecycles. + +Native `Promise` is still the right tool for one asynchronous value. WorkIt is +for the next step: a request, batch, agent run, provider race, stream, or +background operation where related async tasks must live, fail, cancel, and +clean up together. + +## Install + +```sh +npm install @workit/core +``` + +WorkIt targets Node.js server runtimes today. Browser and edge runtimes resolve +to an explicit unsupported-runtime boundary. + +Live examples: +npm package: +Changelog: + +## Quick Start + +```ts +import { work } from "@workit/core"; + +const doubled = await work([1, 2, 3]) + .inParallel(2) + .do(async (value, _ctx) => value * 2); +``` + +The context parameter is available when the task needs cancellation, progress, +budgets, or scoped resources. It can be ignored for plain transformations. + +## Why Ownership Matters + +Consider this batch helper: + +```ts +type BatchEvent = + | { type: "item:started"; item: T; attempt: number } + | { type: "item:retried"; item: T; attempt: number; error: unknown } + | { type: "item:completed"; item: T }; + +type BatchOptions = { + concurrency: number; + retries: number; + timeoutMs: number; + signal: AbortSignal; + events: { emit(event: BatchEvent): void }; + run: (item: T, options: { signal: AbortSignal }) => Promise; +}; + +function backoffMs(attempt: number): number { + return Math.min(1000 * 2 ** (attempt - 1), 10_000); +} + +function sleep(ms: number, signal?: AbortSignal): Promise { + if (signal?.aborted) return Promise.reject(signal.reason); + + return new Promise((resolve, reject) => { + let settled = false; + + const finish = (): void => { + if (settled) return; + settled = true; + signal?.removeEventListener("abort", abort); + resolve(); + }; + + const abort = (): void => { + if (settled) return; + settled = true; + clearTimeout(timer); + reject(signal?.reason); + }; + + const timer = setTimeout(finish, ms); + signal?.addEventListener("abort", abort, { once: true }); + }); +} + +async function runBatch( + items: readonly T[], + options: BatchOptions +): Promise { + const results = new Array(items.length); + let nextIndex = 0; + + async function worker(): Promise { + while (!options.signal.aborted) { + const index = nextIndex++; + if (index >= items.length) return; + + const item = items[index]; + for (let attempt = 1; attempt <= options.retries + 1; attempt++) { + const timeout = AbortSignal.timeout(options.timeoutMs); + const signal = AbortSignal.any([options.signal, timeout]); + + try { + options.events.emit({ type: "item:started", item, attempt }); + results[index] = await options.run(item, { signal }); + options.events.emit({ type: "item:completed", item }); + break; + } catch (error) { + if (attempt > options.retries || options.signal.aborted) throw error; + options.events.emit({ type: "item:retried", item, attempt, error }); + await sleep(backoffMs(attempt), options.signal); + } + } + } + } + + await Promise.all( + Array.from({ length: options.concurrency }, () => worker()) + ); + + return results; +} +``` + +It covers bounded parallelism, timeout, parent cancellation, ordered results, +typed events, and retry backoff. The lifecycle is still split across the queue, +timeout signals, retry loop, event sink, and caller. Adding sibling +cancellation, cleanup, budgets, partial results, or diagnostics extends the +same ownership protocol in several places. + +With WorkIt, the ownership boundary is the API: + +```ts +import { work } from "@workit/core"; + +const results = await work(items) + .inParallel(8) + .withRetry(3) + .withTimeout("5s") + .do(async (item, ctx) => { + ctx.report({ message: `processing ${item.id}` }); + return apiCall(item, { signal: ctx.signal }); + }); +``` + +This gives the batch one lifecycle contract: + +- at most 8 items run at once +- transient failures retry with cancel-aware backoff +- each item has a 5 second timeout +- every task receives the same cancellation model through `ctx.signal` +- progress is a typed runtime event +- queued and active work stop together when the owner is cancelled + +## What WorkIt Replaces + +WorkIt does not replace promises as values. It replaces repeated lifecycle +orchestration around promises. + +| Existing pattern | Real limitation | Ownership contract | WorkIt primitive | +|---|---|---:|---| +| Hand-written concurrency queue | Queue, retry, timeout, and caller cancellation each own part of the lifecycle | no single owner | `work().inParallel()` / `run.pool()` | +| Manual scope object with cancellation tokens | Works until every new feature must reimplement the same lifecycle rules | local convention | `group()` / `run.*` | +| Provider race with `Promise.race()` | Losing calls keep running unless each branch is wired to cancellation | no | `run.race()` | +| Retry loop with delayed backoff | Cancellation has to be remembered in every sleep and retry branch | no | `run.retry()` | +| Request fan-out with `Promise.all()` | Sibling cancellation and cleanup are not part of the value contract | no | `group()` / `run.all()` | +| Manual `try/finally` cleanup | Cleanup can hang or obscure the original failure without an explicit policy | partial | `run.bracket()` | +| Async iterator prefetch | Producer control and consumer demand are easy to separate accidentally | partial | `work().stream()` | +| Ad hoc token or cost counters | Nested work can charge the wrong owner without a shared context contract | partial | context budgets | +| CPU loop with `AbortController` | Cooperative signals cannot preempt the main thread | no | `offload()` | + +WorkIt's ownership contract is the combination of scope, cancellation reason, +child task set, defer stack, context, and event stream. + +## Mental Model + +WorkIt creates a scope tree. A scope owns its tasks. When a foreground task +fails, times out, or is cancelled, the scope cancels sibling work, waits for +owned children, runs cleanup, emits lifecycle events, and then settles. + +```mermaid +flowchart TD + A[scope created] --> B[foreground tasks start] + A --> C[background tasks start] + B --> D{failure, timeout, or cancel} + D -- no --> E[foreground values settle] + D -- yes --> F[cancel siblings with typed reason] + E --> G[await owned background tasks] + F --> G + G --> H[run defer stack and bracket cleanup] + H --> I[scope closes] + A -. explicit escape .-> J[detached task] + J -. not awaited by scope .-> K[external owner required] +``` + +Rules: + +1. Every task runs inside exactly one scope. +2. A scope owns cancellation, cleanup, context, child tasks, and events. +3. Cancelling a scope aborts its signal and propagates a typed reason. +4. A scope cannot close while non-detached children are still pending. +5. `background` is owned and delays closure. +6. `detached` is explicit and transfers ownership to the caller. + +## Core API + +| Need | Use | +|---|---| +| One owned operation with child tasks | `group(async (task) => ...)` | +| Batch work over items | `work(items)` | +| Bounded parallel task functions | `run.pool(concurrency, tasks)` | +| Safer `Promise.all` / `race` / `any` | `run.all()`, `run.race()`, `run.any()` | +| Retry, timeout, fallback, hedge | `run.retry()`, `run.timeout()`, `run.fallback()`, `run.hedge()` | +| Resource safety | `run.bracket()` | +| Critical sections | `run.uncancellable()` | +| Backpressured streams | `work(items).stream()` | +| Producer-consumer coordination | `@workit/core/channel` | +| Worker-thread hard boundary | `@workit/core/worker` | +| Diagnostics and snapshots | `@workit/core/diagnostics` | +| OpenTelemetry bridge | `@workit/core/otel` | +| Agent helper contracts | `@workit/core/ai` | + +## Common Use Cases + +These are short entry points. The full narrative and benchmark discussion live +in the repository [`articles/`](https://github.com/WorkRuntime/workit/tree/main/articles). + +### Owned Request Fan-Out + +```ts +import { group } from "@workit/core"; + +const response = await group(async (task) => { + const profile = task((ctx) => fetchProfile({ signal: ctx.signal })); + const account = task((ctx) => fetchAccount({ signal: ctx.signal })); + + task.background(async (ctx) => { + ctx.defer(() => flushAuditBuffer()); + await writeAuditEvent({ signal: ctx.signal }); + }); + + return { profile: await profile, account: await account }; +}); +``` + +If `profile` fails, the account and audit tasks are cancelled. The audit cleanup +runs before the scope closes. + +### Provider Race + +```ts +import { run } from "@workit/core"; + +const result = await run.race([ + run.timeout((ctx) => primary.generate({ signal: ctx.signal }), "5s"), + run.timeout((ctx) => backup.generate({ signal: ctx.signal }), "5s"), +]); +``` + +The first success wins. Losing branches receive `CancelReason { kind: +"race_lost" }`. + +### Retry With Timeout + +```ts +import { run } from "@workit/core"; + +const receipt = await run.retry( + (ctx) => + run.timeout( + (timeoutCtx) => + chargeCustomer(invoice, { + signal: AbortSignal.any([ctx.signal, timeoutCtx.signal]), + }), + "5s" + ), + { retries: 3 } +); +``` + +The retry policy, timeout, and caller cancellation share one owned execution +path instead of living in separate helper layers. + +### Backpressured Stream + +```ts +import { work } from "@workit/core"; + +for await (const summary of work(documents()) + .inParallel(8) + .map((doc, ctx) => summarize(doc, { signal: ctx.signal })) + .stream()) { + if (summary.enough) break; +} +``` + +Breaking the loop cancels remaining in-flight work and stops pulling from the +producer. + +### Budgeted Agent Work + +```ts +import { CostBudget, TokenBudget, run, work } from "@workit/core"; + +await run.context.with(CostBudget, { spent: 0, limit: 0.50, unit: "USD" }, () => + run.context.with(TokenBudget, { spent: 0, limit: 100_000, unit: "tokens" }, () => + work(chunks).inParallel(8).do(async (chunk, ctx) => { + ctx.consume(TokenBudget, chunk.estimatedTokens); + ctx.consume(CostBudget, chunk.estimatedCost); + return embed(chunk, { signal: ctx.signal }); + }) + ) +); +``` + +Budget overrun cancels the scope that installed the budget. + +### Worker Boundary + +```ts +import { offload } from "@workit/core/worker"; + +const result = await offload( + new URL("./cpu-worker.js", import.meta.url), + "compute", + input, + { timeout: "2s" } +); +``` + +`AbortController` cannot preempt a tight CPU loop on the main thread. Worker +offload gives CPU-bound or plugin-like work a hard timeout boundary. + +## Worker Offload Boundary + +`offload()` is an execution boundary and a structured-clone boundary. + +Accepted worker inputs include primitives, arrays, plain objects, `Map`, `Set`, +dates, regexps, buffers, and typed arrays. + +Rejected worker inputs include class instances, functions, symbols, custom +prototype objects, inline or remote module URLs, and parent directory segments +in worker paths. + +When `timeout` fires, WorkIt terminates the worker thread. This is different +from cooperative `AbortSignal` cancellation inside the main JavaScript thread. + +## Runnable Samples + +| Sample | What it demonstrates | +|---|---| +| [`samples/progress-parallel.sample.js`](samples/progress-parallel.sample.js) | progress events during bounded parallel work | +| [`samples/race-providers.sample.js`](samples/race-providers.sample.js) | provider race with loser cancellation | +| [`samples/no-orphan.sample.js`](samples/no-orphan.sample.js) | owned background work waits before scope close | +| [`samples/streaming-summarizer.sample.js`](samples/streaming-summarizer.sample.js) | streaming summarization with early stop | +| [`samples/embed-bisection.sample.js`](samples/embed-bisection.sample.js) | bad-batch bisection for embedding pipelines | +| [`samples/supervision.sample.js`](samples/supervision.sample.js) | supervised long-lived work | +| [`samples/worker-offload.sample.js`](samples/worker-offload.sample.js) | worker timeout against non-cooperative CPU work | +| [`samples/budget-rag.sample.js`](samples/budget-rag.sample.js) | request-scoped cost budget | +| [`samples/logging-otel-bridge.sample.js`](samples/logging-otel-bridge.sample.js) | local events bridged to telemetry | + +## Verified Evidence + +WorkIt claims are tied to executable gates. The benchmark timings below are +representative captured runs; the gates assert semantic invariants and budget +thresholds, not exact milliseconds. + +| Evidence | Current result | +|---|---:| +| Unit tests | 221 passing | +| Coverage gate | 100% statements, branches, functions, lines | +| Runtime dependencies | 0 | +| Article benchmark suite | 19/19 passing | +| Core group import | 14,175 B minified / 4,835 B gzip | +| Public bundle | 29,255 B minified / 9,694 B gzip | +| Stream gate | 1,000,000 logical items with bounded producer growth | +| Soak gate | 100,000 logical tasks with bounded concurrency | +| Exporter stress | 100,000 events with bounded queue | + +Representative article-benchmark results: + +| Benchmark | Baseline | WorkIt | +|---|---:|---:| +| Provider race losers after winner | losers continue until their sleeps finish | losers cancelled in scope close | +| Retry after cancellation | 7 extra attempts, 622 ms latency | 0 extra attempts, 1 ms latency | +| Context `.with()` over 5,000 keys | 31.68 ms | 0.014 ms | +| 1B-row source, take 25 | 281 items pulled | 40 items pulled | +| Sampling volume | 1,300 events | 36 events | + +Run the main verification gate: + +```sh +npm run verify +``` + +`npm run verify` runs type-checking, header and test hygiene, unit tests, +security checks, vulnerability audit, SBOM validation, API and bundle-size +locks, runtime benchmarks, stream and soak gates, exporter stress, +package-consumer fixtures, public-proof validation, worker-contract checks, +release-policy checks, and `npm pack --dry-run`. + +Run the article benchmark suite: + +```sh +npm run bench:articles +``` + +Run the curated publication evidence suite: + +```sh +npm run test:evidence +``` + +Run verification commands sequentially when they depend on `dist/`. Some gates, +including `npm run test:coverage` and `npm run verify`, rebuild or clean the +compiled artifacts. Running them in parallel with `npm run bench:articles` can +delete `dist/` while benchmark processes are importing it. + +Machine-readable reviewer evidence lives in +[`benchmarks/public-proof.json`](benchmarks/public-proof.json), the article +benchmark capture lives in +[`benchmarks/results/articles.latest.json`](benchmarks/results/articles.latest.json), +and the public claim ledger lives in +[`evidence/claims.json`](evidence/claims.json). + +## Security And Release Integrity + +| Guarantee | Enforcement | +|---|---| +| Runtime core has no production dependencies | package metadata and security gate | +| Core does not import networking modules | static no-network gate | +| Published package includes an SBOM | CycloneDX SBOM generation and validation | +| Release workflow uses provenance controls | release-policy gate | +| Public API and bundle size are locked | API and size gates | +| Consumer fixtures install the package artifact | package-consumer gate | + +Additional repository controls include pinned dev dependencies, vulnerability +audit, SHA-pinned GitHub Actions, OSSF Scorecard workflow, CODEOWNERS, +Dependabot, and signed release tag policy. + +Security reports should follow [`SECURITY.md`](SECURITY.md). + +## Runtime Support + +Supported: + +- Node.js `>=20.11` +- ESM consumers +- CommonJS consumers +- strict TypeScript consumers +- AWS Lambda-shaped handlers +- Azure Functions-shaped handlers +- Next.js route-shaped handlers +- Express, Fastify, tRPC, and Vercel AI SDK fixtures + +Unsupported today: + +- browser client runtime +- Cloudflare Workers +- Next.js Edge / Vercel Edge + +Unsupported runtimes resolve to an explicit unsupported boundary. An edge-safe +context runtime is future work. + +## When To Use Alternatives + +| Tool | Prefer it when | Prefer WorkIt when | +|---|---|---| +| Native `Promise` | One async value is enough | Work needs ownership, cancellation, cleanup, or diagnostics | +| Manual scope object | The lifecycle is local and small enough to audit in one file | The lifecycle becomes a reusable cross-module contract | +| `p-limit` | You only need a tiny semaphore | Bounded work also needs lifecycle semantics | +| `p-map` | You need a focused concurrent map | Mapping needs retry, timeout, stream policy, or partial results | +| RxJS | You are modelling rich event streams | You are modelling owned async task lifecycles | +| Bottleneck | You need distributed reservoirs or rate limits | You need local structured concurrency | +| Effection | You want structured concurrency via operations/generators | You want plain `async`/`await` task functions | +| Effect-TS | You want a full effect system | You want owned async work without migrating to a DSL | + +These comparisons are about ownership and composition. Some libraries expose +cancellation hooks or queue controls; WorkIt's claim is that cancellation, +cleanup, retry, timeout, budgets, backpressure, and diagnostics compose under +one owner. + +## Migration Notes + +These are orientation notes, not codemods. Keep the old tool when it owns the +problem better. + +### From p-limit + +Use `run.pool()` or `work(items).inParallel(n)` when the semaphore also needs +sibling cancellation, retry, timeout, cleanup, progress, or partial-result +policy under one owner. + +### From p-map + +Use `work(items).inParallel(n).do(fn)` for concurrent maps that need the same +lifecycle semantics as the caller. Keep `p-map` for a small one-file map where +concurrency is the only concern. + +### From RxJS + +Keep RxJS for rich observable graphs. Use WorkIt when the problem is owned task +lifecycle: request fan-out, provider racing, agent tools, bounded streams, or +cleanup around async work. + +### From Bottleneck + +Keep Bottleneck for distributed reservoirs and external rate-limit state. Use +WorkIt for local process ownership where bounded concurrency must compose with +cancel, retry, timeout, budgets, and diagnostics. + +## Documentation + +| Resource | Purpose | +|---|---| +| [`articles/`](https://github.com/WorkRuntime/workit/tree/main/articles) | Narrative articles with examples and benchmark discussion | +| [`benchmarks/articles/`](benchmarks/articles/) | Reproducible article benchmark suite | +| [`evidence/`](evidence/) | Machine-readable claim ledger and evidence policy | +| [`tests/evidence/`](tests/evidence/) | Curated publication evidence proofs | +| [`samples/`](samples/) | Runnable examples against the compiled package | +| [`SECURITY.md`](SECURITY.md) | Security reporting and release integrity policy | + +## Versioning + +WorkIt follows semver with a stricter release discipline: + +- Patch releases, such as `0.1.x`, are for fixes, build/release hardening, + layout migrations, documentation, and evidence updates. They must not add new + public runtime APIs. +- Minor releases, such as `0.2.0`, may add new subpaths or feature families when + they are backed by tests, evidence, package-consumer checks, and + documentation. +- The root `@workit/core` import remains size-disciplined. New heavier + capabilities should live in subpaths or companion packages. +- `1.0.0` will mark a frozen public API and long-term compatibility policy, not + a shortcut for credibility. Current `0.x` releases are validated and usable, + with changes managed through semver and release notes. + +## Citation + +If you use WorkIt in research, benchmarks, or reproducible artifacts, please +cite the software release you used: + +```bibtex +@software{workit2026, + author = {Admilson B. F. Cossa}, + title = {WorkIt: A TypeScript Structured Concurrency Runtime for Node.js Server Runtimes}, + year = {2026}, + url = {https://github.com/WorkRuntime/workit}, + version = {0.1.5}, + license = {Apache-2.0} +} +``` + +## Contributing + +Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a pull request. + +Before submitting code: + +```sh +npm run verify +npm run test:coverage +npm run bench:articles +npm run test:evidence +``` + +Run these commands sequentially. Several verification commands clean and rebuild +`dist/`, while article benchmarks import the compiled package artifact. + +Bug reports should include the WorkIt version, Node.js version, reproduction +code, and whether the failure occurs from source or the installed package. + +## License + +Apache-2.0. See [`LICENSE`](LICENSE). diff --git a/packages/core/SECURITY.md b/packages/core/SECURITY.md new file mode 100644 index 0000000..7c796bf --- /dev/null +++ b/packages/core/SECURITY.md @@ -0,0 +1,130 @@ + +# Security Policy + +## Supported Versions + +WorkIt is pre-release software. Security fixes apply to the current `0.x` +development line until a stable support policy is published. + +## Reporting A Vulnerability + +Do not open a public issue for suspected vulnerabilities. + +Security contact: admilsoncossa@gmail.com + +PGP encryption: request the maintainer's current public key through the security +contact before sending secrets, exploit details that include credentials, or +tenant-sensitive material. Do not attach secrets to an unencrypted first report. + +Send a private report to the project maintainer with: + +- affected version or commit +- operating system and Node.js version +- minimal reproduction +- impact assessment +- whether secrets, tenant data, billing controls, or cancellation guarantees are affected + +The maintainer should acknowledge valid reports within 72 hours and publish a +fix, mitigation, or status update as soon as practical. + +## Security Boundary + +WorkIt is a local structured-concurrency runtime. It does not authenticate +users, authorize actions, encrypt payloads, or provide a durable workflow +ledger. Applications remain responsible for tenant isolation, provider +credentials, authorization, persistence, and external network policy. + +The core package must keep these guarantees: + +- zero runtime dependencies +- no core networking imports or remote telemetry clients +- bounded exporter queues for opt-in telemetry bridges +- caller-owned telemetry sanitizers before events leave the process +- no skipped or focused tests in release verification +- 100% statement, branch, function, and line coverage +- CycloneDX SBOM generation and validation +- production dependency vulnerability audit +- package dry-run inspection before publication + +## Release Provenance + +Public releases must be built from a clean worktree and published with npm +provenance enabled. A release is not approved unless these commands pass: + +```sh +npm run verify +npm run test:coverage +npm run check:vulnerabilities +npm run check:sbom +npm pack --dry-run --json +``` + +The provenance workflow is defined in `.github/workflows/release-provenance.yml`. +Registry dry-runs and real publication must be triggered only from a signed +release tag after the scoped release commit is clean and verified. The publish +step runs: + +```sh +npm publish --provenance --access public --dry-run +``` + +for dry runs, and: + +```sh +npm publish --provenance --access public +``` + +for an approved release. The package must not publish source maps, local docs, +tests, secrets, temporary files, debug output, or private agent instructions. + +Release tags must be signed. The release operator must create the version tag +only after the scoped release commit is clean and verified: + +```sh +git tag -s vX.Y.Z -m "Release vX.Y.Z" +git tag -v vX.Y.Z +``` + +Unsigned release tags are not valid release evidence. + +## OpenSSF Best Practices + +The OpenSSF Best Practices badge is tracked as a public supply-chain hygiene +process, not as a marketing badge. The project must not claim a passing badge +until the external OpenSSF checklist is completed and the project entry exists. + +The process is documented in `OPENSSF-BEST-PRACTICES.md`. + +## Responsible Disclosure Scope + +Reports are in scope when they affect: + +- cancellation integrity +- no-orphan guarantees +- budget accounting or cost-cap bypass +- context isolation across requests +- telemetry exporter isolation +- package contents or supply-chain integrity +- worker-thread offload boundaries + +Worker-thread offload is an explicit local execution boundary. `offload()` +accepts only local file URLs or paths controlled by the application; inline and remote module URLs +are rejected before import, and parent directory traversal is +rejected before the worker starts. Worker input must be plain structured-clone data. +Primitives, arrays, plain objects, `Map`, `Set`, dates, regexps, buffers, +and typed arrays are accepted. Functions, symbols, class instances, and objects +with custom prototypes are rejected before worker startup. When `offload()` is +given a timeout, WorkIt terminates the worker thread on timeout so +non-cooperative worker code cannot keep running in-process. In-process helpers +such as `run.uncancellable()` remain cooperative shields; JavaScript code that +ignores abort signals cannot be forcibly stopped without a worker/process +boundary. + +Out of scope: + +- vulnerabilities in downstream application code +- denial-of-service claims that require intentionally unbounded user code inside a task +- unsupported browser, edge, or Cloudflare Worker execution diff --git a/benchmarks/articles/01-run-all-vs-promise-all.mjs b/packages/core/benchmarks/articles/01-run-all-vs-promise-all.mjs similarity index 100% rename from benchmarks/articles/01-run-all-vs-promise-all.mjs rename to packages/core/benchmarks/articles/01-run-all-vs-promise-all.mjs diff --git a/benchmarks/articles/02-run-race-vs-promise-race.mjs b/packages/core/benchmarks/articles/02-run-race-vs-promise-race.mjs similarity index 100% rename from benchmarks/articles/02-run-race-vs-promise-race.mjs rename to packages/core/benchmarks/articles/02-run-race-vs-promise-race.mjs diff --git a/benchmarks/articles/03-run-any-vs-promise-any.mjs b/packages/core/benchmarks/articles/03-run-any-vs-promise-any.mjs similarity index 100% rename from benchmarks/articles/03-run-any-vs-promise-any.mjs rename to packages/core/benchmarks/articles/03-run-any-vs-promise-any.mjs diff --git a/benchmarks/articles/04-pool-vs-semaphore.mjs b/packages/core/benchmarks/articles/04-pool-vs-semaphore.mjs similarity index 100% rename from benchmarks/articles/04-pool-vs-semaphore.mjs rename to packages/core/benchmarks/articles/04-pool-vs-semaphore.mjs diff --git a/benchmarks/articles/05-retry-on-cancel.mjs b/packages/core/benchmarks/articles/05-retry-on-cancel.mjs similarity index 100% rename from benchmarks/articles/05-retry-on-cancel.mjs rename to packages/core/benchmarks/articles/05-retry-on-cancel.mjs diff --git a/benchmarks/articles/06-hedge-tied-requests.mjs b/packages/core/benchmarks/articles/06-hedge-tied-requests.mjs similarity index 100% rename from benchmarks/articles/06-hedge-tied-requests.mjs rename to packages/core/benchmarks/articles/06-hedge-tied-requests.mjs diff --git a/benchmarks/articles/07-worker-hard-kill.mjs b/packages/core/benchmarks/articles/07-worker-hard-kill.mjs similarity index 100% rename from benchmarks/articles/07-worker-hard-kill.mjs rename to packages/core/benchmarks/articles/07-worker-hard-kill.mjs diff --git a/benchmarks/articles/08-uncancellable-shield.mjs b/packages/core/benchmarks/articles/08-uncancellable-shield.mjs similarity index 100% rename from benchmarks/articles/08-uncancellable-shield.mjs rename to packages/core/benchmarks/articles/08-uncancellable-shield.mjs diff --git a/benchmarks/articles/09-stream-1b-lazy.mjs b/packages/core/benchmarks/articles/09-stream-1b-lazy.mjs similarity index 100% rename from benchmarks/articles/09-stream-1b-lazy.mjs rename to packages/core/benchmarks/articles/09-stream-1b-lazy.mjs diff --git a/benchmarks/articles/10-stream-slow-consumer.mjs b/packages/core/benchmarks/articles/10-stream-slow-consumer.mjs similarity index 100% rename from benchmarks/articles/10-stream-slow-consumer.mjs rename to packages/core/benchmarks/articles/10-stream-slow-consumer.mjs diff --git a/benchmarks/articles/11-channel-contract.mjs b/packages/core/benchmarks/articles/11-channel-contract.mjs similarity index 100% rename from benchmarks/articles/11-channel-contract.mjs rename to packages/core/benchmarks/articles/11-channel-contract.mjs diff --git a/benchmarks/articles/12-bracket-vs-try-finally.mjs b/packages/core/benchmarks/articles/12-bracket-vs-try-finally.mjs similarity index 100% rename from benchmarks/articles/12-bracket-vs-try-finally.mjs rename to packages/core/benchmarks/articles/12-bracket-vs-try-finally.mjs diff --git a/benchmarks/articles/13-budget-atomicity-and-cancel.mjs b/packages/core/benchmarks/articles/13-budget-atomicity-and-cancel.mjs similarity index 100% rename from benchmarks/articles/13-budget-atomicity-and-cancel.mjs rename to packages/core/benchmarks/articles/13-budget-atomicity-and-cancel.mjs diff --git a/benchmarks/articles/14-context-overlay-perf.mjs b/packages/core/benchmarks/articles/14-context-overlay-perf.mjs similarity index 100% rename from benchmarks/articles/14-context-overlay-perf.mjs rename to packages/core/benchmarks/articles/14-context-overlay-perf.mjs diff --git a/benchmarks/articles/15-core-zero-network.mjs b/packages/core/benchmarks/articles/15-core-zero-network.mjs similarity index 100% rename from benchmarks/articles/15-core-zero-network.mjs rename to packages/core/benchmarks/articles/15-core-zero-network.mjs diff --git a/benchmarks/articles/16-sampling-and-aggregation.mjs b/packages/core/benchmarks/articles/16-sampling-and-aggregation.mjs similarity index 100% rename from benchmarks/articles/16-sampling-and-aggregation.mjs rename to packages/core/benchmarks/articles/16-sampling-and-aggregation.mjs diff --git a/benchmarks/articles/17-cardinality-safe-metrics.mjs b/packages/core/benchmarks/articles/17-cardinality-safe-metrics.mjs similarity index 100% rename from benchmarks/articles/17-cardinality-safe-metrics.mjs rename to packages/core/benchmarks/articles/17-cardinality-safe-metrics.mjs diff --git a/benchmarks/articles/18-diagnostics-finding-codes.mjs b/packages/core/benchmarks/articles/18-diagnostics-finding-codes.mjs similarity index 100% rename from benchmarks/articles/18-diagnostics-finding-codes.mjs rename to packages/core/benchmarks/articles/18-diagnostics-finding-codes.mjs diff --git a/benchmarks/articles/19-agent-scope.mjs b/packages/core/benchmarks/articles/19-agent-scope.mjs similarity index 100% rename from benchmarks/articles/19-agent-scope.mjs rename to packages/core/benchmarks/articles/19-agent-scope.mjs diff --git a/benchmarks/articles/README.md b/packages/core/benchmarks/articles/README.md similarity index 100% rename from benchmarks/articles/README.md rename to packages/core/benchmarks/articles/README.md diff --git a/benchmarks/articles/lib/baselines.mjs b/packages/core/benchmarks/articles/lib/baselines.mjs similarity index 100% rename from benchmarks/articles/lib/baselines.mjs rename to packages/core/benchmarks/articles/lib/baselines.mjs diff --git a/benchmarks/articles/lib/spinner.mjs b/packages/core/benchmarks/articles/lib/spinner.mjs similarity index 100% rename from benchmarks/articles/lib/spinner.mjs rename to packages/core/benchmarks/articles/lib/spinner.mjs diff --git a/benchmarks/articles/package.json b/packages/core/benchmarks/articles/package.json similarity index 100% rename from benchmarks/articles/package.json rename to packages/core/benchmarks/articles/package.json diff --git a/benchmarks/articles/run-all.mjs b/packages/core/benchmarks/articles/run-all.mjs similarity index 100% rename from benchmarks/articles/run-all.mjs rename to packages/core/benchmarks/articles/run-all.mjs diff --git a/benchmarks/articles/run-repeated.mjs b/packages/core/benchmarks/articles/run-repeated.mjs similarity index 100% rename from benchmarks/articles/run-repeated.mjs rename to packages/core/benchmarks/articles/run-repeated.mjs diff --git a/benchmarks/public-proof.json b/packages/core/benchmarks/public-proof.json similarity index 100% rename from benchmarks/public-proof.json rename to packages/core/benchmarks/public-proof.json diff --git a/benchmarks/results/articles.latest.json b/packages/core/benchmarks/results/articles.latest.json similarity index 100% rename from benchmarks/results/articles.latest.json rename to packages/core/benchmarks/results/articles.latest.json diff --git a/benchmarks/results/articles.repeated.json b/packages/core/benchmarks/results/articles.repeated.json similarity index 100% rename from benchmarks/results/articles.repeated.json rename to packages/core/benchmarks/results/articles.repeated.json diff --git a/evidence/README.md b/packages/core/evidence/README.md similarity index 100% rename from evidence/README.md rename to packages/core/evidence/README.md diff --git a/evidence/claims.json b/packages/core/evidence/claims.json similarity index 100% rename from evidence/claims.json rename to packages/core/evidence/claims.json diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..cdceab0 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,154 @@ +{ + "name": "@workit/core", + "version": "0.1.5", + "description": "Structured concurrency runtime for TypeScript.", + "type": "module", + "private": false, + "license": "Apache-2.0", + "author": "Admilson B. F. Cossa", + "repository": { + "type": "git", + "url": "https://github.com/WorkRuntime/workit" + }, + "bugs": { + "url": "https://github.com/WorkRuntime/workit/issues" + }, + "homepage": "https://github.com/WorkRuntime/workit#readme", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": { + "import": "./dist/index.js", + "require": "./dist-cjs/index.cjs" + }, + "default": "./dist/runtime/unsupported.js" + }, + "./ai": { + "types": "./dist/ai/index.d.ts", + "node": { + "import": "./dist/ai/index.js", + "require": "./dist-cjs/ai/index.cjs" + }, + "default": "./dist/runtime/unsupported.js" + }, + "./channel": { + "types": "./dist/channel/index.d.ts", + "import": "./dist/channel/index.js", + "require": "./dist-cjs/channel/index.cjs" + }, + "./observability": { + "types": "./dist/observability/index.d.ts", + "import": "./dist/observability/index.js", + "require": "./dist-cjs/observability/index.cjs" + }, + "./diagnostics": { + "types": "./dist/diagnostics/index.d.ts", + "import": "./dist/diagnostics/index.js", + "require": "./dist-cjs/diagnostics/index.cjs" + }, + "./otel": { + "types": "./dist/otel/index.d.ts", + "import": "./dist/otel/index.js", + "require": "./dist-cjs/otel/index.cjs" + }, + "./worker": { + "types": "./dist/worker/index.d.ts", + "node": { + "import": "./dist/worker/index.js" + }, + "default": "./dist/runtime/unsupported.js" + } + }, + "sideEffects": false, + "publishConfig": { + "access": "public" + }, + "files": [ + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "dist", + "dist-cjs", + "SECURITY.md" + ], + "scripts": { + "clean": "node -e \"const fs=require('node:fs'); for (const p of ['dist','dist-cjs','coverage']) fs.rmSync(p,{recursive:true,force:true});\"", + "build": "npm run clean && tsc && node scripts/build-cjs.mjs && node scripts/generate-sbom.mjs", + "typecheck": "tsc --noEmit", + "check:no-network": "node scripts/check-no-network.mjs", + "check:headers": "node scripts/check-file-headers.mjs", + "check:security": "node scripts/check-security.mjs", + "check:vulnerabilities": "node scripts/check-vulnerabilities.mjs", + "check:sbom": "npm run build && node scripts/check-sbom.mjs", + "check:tests": "node scripts/check-tests.mjs", + "check:api": "npm run build && node scripts/check-api-surface.mjs", + "check:size": "npm run build && node scripts/check-bundle-size.mjs", + "report:size": "npm run build && node scripts/report-bundle-size.mjs", + "check:benchmark": "npm run build && node scripts/check-benchmark.mjs", + "check:context-performance": "npm run build && node scripts/check-context-performance.mjs", + "check:1b": "npm run build && node scripts/check-1b-benchmark.mjs", + "check:leak": "npm run build && node --expose-gc scripts/check-leak.mjs", + "check:stream-memory": "npm run build && node --expose-gc scripts/check-stream-memory.mjs", + "check:soak": "npm run build && node --expose-gc scripts/check-soak.mjs", + "check:exporter-stress": "npm run build && node --expose-gc scripts/check-exporter-stress.mjs", + "check:package-consumer": "npm run build && node scripts/check-package-consumer.mjs", + "check:claims": "npm run build && node scripts/check-claim-fixtures.mjs", + "check:public-proof": "node scripts/check-public-proof.mjs", + "check:worker-contract": "node scripts/check-worker-contract-docs.mjs", + "check:release-policy": "npm run build && node scripts/check-release-provenance.mjs", + "check:release": "npm run build && node scripts/check-release-provenance.mjs --registry-dry-run", + "bench:articles": "node benchmarks/articles/run-all.mjs", + "bench:articles:repeated": "node benchmarks/articles/run-repeated.mjs", + "test:evidence": "node tests/evidence/run-all.mjs", + "test:property": "npm run build && vitest run tests/property", + "pack:dry": "npm run build && npm pack --dry-run --json", + "sample:1b": "npm run build && node samples/1b-stream.sample.js", + "sample:concurrency": "npm run build && node samples/concurrency-budget.sample.js", + "sample:progress": "npm run build && node samples/progress-parallel.sample.js", + "sample:cancel": "npm run build && node samples/cancel-reason.sample.js", + "sample:timeout": "npm run build && node samples/timeout-stop.sample.js", + "sample:no-orphan": "npm run build && node samples/no-orphan.sample.js", + "sample:all": "npm run build && node samples/safer-promise-all.sample.js", + "sample:agent": "npm run build && node samples/agent-tree-cancel.sample.js", + "sample:race": "npm run build && node samples/race-providers.sample.js", + "sample:rag": "npm run build && node samples/budget-rag.sample.js", + "sample:batch": "npm run build && node samples/batch-upload.sample.js", + "sample:stream": "npm run build && node samples/streaming-summarizer.sample.js", + "sample:embed100k": "npm run build && node samples/embed-100k.sample.js", + "sample:bisection": "npm run build && node samples/embed-bisection.sample.js", + "sample:stt-disconnect": "npm run build && node samples/stt-disconnect.sample.js", + "sample:supervise": "npm run build && node samples/supervision.sample.js", + "sample:worker": "npm run build && node samples/worker-offload.sample.js", + "sample:aws": "npm run build && node samples/aws-lambda-handler.sample.js", + "sample:azure": "npm run build && node samples/azure-functions-handler.sample.js", + "sample:next": "npm run build && node samples/next-server-route.sample.js", + "sample:otel": "npm run build && node samples/otel-adapter.sample.js", + "sample:logging": "npm run build && node samples/logging-otel-bridge.sample.js", + "soak:24h": "npm run build && node --expose-gc scripts/soak-24h.mjs", + "test": "npm run build && vitest run", + "test:coverage": "npm run build && vitest run --coverage", + "verify": "npm run typecheck && npm run check:no-network && npm run check:headers && npm run check:tests && npm test && npm run check:security && npm run check:vulnerabilities && npm run check:sbom && npm run check:api && npm run check:size && npm run check:benchmark && npm run check:context-performance && npm run check:1b && npm run check:leak && npm run check:stream-memory && npm run check:soak && npm run check:exporter-stress && npm run check:package-consumer && npm run check:claims && npm run check:public-proof && npm run check:worker-contract && npm run check:release-policy && npm run pack:dry" + }, + "engines": { + "node": ">=20.11" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.1" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + }, + "devDependencies": { + "@opentelemetry/api": "1.9.1", + "@types/node": "25.6.1", + "@vitest/coverage-v8": "4.1.5", + "esbuild": "0.28.0", + "fast-check": "4.7.0", + "typescript": "6.0.3", + "vitest": "4.1.5", + "wrangler": "4.89.1" + } +} diff --git a/samples/1b-stream.sample.js b/packages/core/samples/1b-stream.sample.js similarity index 100% rename from samples/1b-stream.sample.js rename to packages/core/samples/1b-stream.sample.js diff --git a/samples/agent-tree-cancel.sample.js b/packages/core/samples/agent-tree-cancel.sample.js similarity index 100% rename from samples/agent-tree-cancel.sample.js rename to packages/core/samples/agent-tree-cancel.sample.js diff --git a/samples/aws-lambda-handler.sample.js b/packages/core/samples/aws-lambda-handler.sample.js similarity index 100% rename from samples/aws-lambda-handler.sample.js rename to packages/core/samples/aws-lambda-handler.sample.js diff --git a/samples/azure-functions-handler.sample.js b/packages/core/samples/azure-functions-handler.sample.js similarity index 100% rename from samples/azure-functions-handler.sample.js rename to packages/core/samples/azure-functions-handler.sample.js diff --git a/samples/batch-upload.sample.js b/packages/core/samples/batch-upload.sample.js similarity index 100% rename from samples/batch-upload.sample.js rename to packages/core/samples/batch-upload.sample.js diff --git a/samples/budget-rag.sample.js b/packages/core/samples/budget-rag.sample.js similarity index 100% rename from samples/budget-rag.sample.js rename to packages/core/samples/budget-rag.sample.js diff --git a/samples/cancel-reason.sample.js b/packages/core/samples/cancel-reason.sample.js similarity index 100% rename from samples/cancel-reason.sample.js rename to packages/core/samples/cancel-reason.sample.js diff --git a/samples/claim-fixtures.mjs b/packages/core/samples/claim-fixtures.mjs similarity index 100% rename from samples/claim-fixtures.mjs rename to packages/core/samples/claim-fixtures.mjs diff --git a/samples/concurrency-budget.sample.js b/packages/core/samples/concurrency-budget.sample.js similarity index 100% rename from samples/concurrency-budget.sample.js rename to packages/core/samples/concurrency-budget.sample.js diff --git a/samples/conversation-agent.sample.js b/packages/core/samples/conversation-agent.sample.js similarity index 100% rename from samples/conversation-agent.sample.js rename to packages/core/samples/conversation-agent.sample.js diff --git a/samples/cpu-worker.sample-worker.js b/packages/core/samples/cpu-worker.sample-worker.js similarity index 100% rename from samples/cpu-worker.sample-worker.js rename to packages/core/samples/cpu-worker.sample-worker.js diff --git a/samples/embed-100k.sample.js b/packages/core/samples/embed-100k.sample.js similarity index 100% rename from samples/embed-100k.sample.js rename to packages/core/samples/embed-100k.sample.js diff --git a/samples/embed-bisection.sample.js b/packages/core/samples/embed-bisection.sample.js similarity index 100% rename from samples/embed-bisection.sample.js rename to packages/core/samples/embed-bisection.sample.js diff --git a/samples/logging-otel-bridge.sample.js b/packages/core/samples/logging-otel-bridge.sample.js similarity index 100% rename from samples/logging-otel-bridge.sample.js rename to packages/core/samples/logging-otel-bridge.sample.js diff --git a/samples/next-server-route.sample.js b/packages/core/samples/next-server-route.sample.js similarity index 100% rename from samples/next-server-route.sample.js rename to packages/core/samples/next-server-route.sample.js diff --git a/samples/no-orphan.sample.js b/packages/core/samples/no-orphan.sample.js similarity index 100% rename from samples/no-orphan.sample.js rename to packages/core/samples/no-orphan.sample.js diff --git a/samples/otel-adapter.sample.js b/packages/core/samples/otel-adapter.sample.js similarity index 100% rename from samples/otel-adapter.sample.js rename to packages/core/samples/otel-adapter.sample.js diff --git a/samples/progress-parallel.sample.js b/packages/core/samples/progress-parallel.sample.js similarity index 100% rename from samples/progress-parallel.sample.js rename to packages/core/samples/progress-parallel.sample.js diff --git a/samples/race-providers.sample.js b/packages/core/samples/race-providers.sample.js similarity index 100% rename from samples/race-providers.sample.js rename to packages/core/samples/race-providers.sample.js diff --git a/samples/safer-promise-all.sample.js b/packages/core/samples/safer-promise-all.sample.js similarity index 100% rename from samples/safer-promise-all.sample.js rename to packages/core/samples/safer-promise-all.sample.js diff --git a/samples/streaming-summarizer.sample.js b/packages/core/samples/streaming-summarizer.sample.js similarity index 100% rename from samples/streaming-summarizer.sample.js rename to packages/core/samples/streaming-summarizer.sample.js diff --git a/samples/stt-disconnect.sample.js b/packages/core/samples/stt-disconnect.sample.js similarity index 100% rename from samples/stt-disconnect.sample.js rename to packages/core/samples/stt-disconnect.sample.js diff --git a/samples/supervision.sample.js b/packages/core/samples/supervision.sample.js similarity index 100% rename from samples/supervision.sample.js rename to packages/core/samples/supervision.sample.js diff --git a/samples/timeout-stop.sample.js b/packages/core/samples/timeout-stop.sample.js similarity index 100% rename from samples/timeout-stop.sample.js rename to packages/core/samples/timeout-stop.sample.js diff --git a/samples/worker-offload.sample.js b/packages/core/samples/worker-offload.sample.js similarity index 100% rename from samples/worker-offload.sample.js rename to packages/core/samples/worker-offload.sample.js diff --git a/scripts/build-cjs.mjs b/packages/core/scripts/build-cjs.mjs similarity index 100% rename from scripts/build-cjs.mjs rename to packages/core/scripts/build-cjs.mjs diff --git a/scripts/check-1b-benchmark.mjs b/packages/core/scripts/check-1b-benchmark.mjs similarity index 100% rename from scripts/check-1b-benchmark.mjs rename to packages/core/scripts/check-1b-benchmark.mjs diff --git a/scripts/check-api-surface.mjs b/packages/core/scripts/check-api-surface.mjs similarity index 100% rename from scripts/check-api-surface.mjs rename to packages/core/scripts/check-api-surface.mjs diff --git a/scripts/check-benchmark.mjs b/packages/core/scripts/check-benchmark.mjs similarity index 100% rename from scripts/check-benchmark.mjs rename to packages/core/scripts/check-benchmark.mjs diff --git a/scripts/check-bundle-size.mjs b/packages/core/scripts/check-bundle-size.mjs similarity index 100% rename from scripts/check-bundle-size.mjs rename to packages/core/scripts/check-bundle-size.mjs diff --git a/scripts/check-claim-fixtures.mjs b/packages/core/scripts/check-claim-fixtures.mjs similarity index 100% rename from scripts/check-claim-fixtures.mjs rename to packages/core/scripts/check-claim-fixtures.mjs diff --git a/scripts/check-context-performance.mjs b/packages/core/scripts/check-context-performance.mjs similarity index 100% rename from scripts/check-context-performance.mjs rename to packages/core/scripts/check-context-performance.mjs diff --git a/scripts/check-exporter-stress.mjs b/packages/core/scripts/check-exporter-stress.mjs similarity index 100% rename from scripts/check-exporter-stress.mjs rename to packages/core/scripts/check-exporter-stress.mjs diff --git a/scripts/check-file-headers.mjs b/packages/core/scripts/check-file-headers.mjs similarity index 100% rename from scripts/check-file-headers.mjs rename to packages/core/scripts/check-file-headers.mjs diff --git a/scripts/check-leak.mjs b/packages/core/scripts/check-leak.mjs similarity index 100% rename from scripts/check-leak.mjs rename to packages/core/scripts/check-leak.mjs diff --git a/scripts/check-no-network.mjs b/packages/core/scripts/check-no-network.mjs similarity index 100% rename from scripts/check-no-network.mjs rename to packages/core/scripts/check-no-network.mjs diff --git a/scripts/check-package-consumer.mjs b/packages/core/scripts/check-package-consumer.mjs similarity index 98% rename from scripts/check-package-consumer.mjs rename to packages/core/scripts/check-package-consumer.mjs index b983944..ed3f59d 100644 --- a/scripts/check-package-consumer.mjs +++ b/packages/core/scripts/check-package-consumer.mjs @@ -17,14 +17,15 @@ import { promisify } from "node:util"; import { build } from "esbuild"; const execFileAsync = promisify(execFile); -const ROOT = resolve(fileURLToPath(new URL("..", import.meta.url))); -const tscCli = join(ROOT, "node_modules", "typescript", "bin", "tsc"); +const PACKAGE_ROOT = resolve(fileURLToPath(new URL("..", import.meta.url))); +const REPO_ROOT = resolve(fileURLToPath(new URL("../../..", import.meta.url))); +const tscCli = join(REPO_ROOT, "node_modules", "typescript", "bin", "tsc"); const bunCli = await findExecutable(["bun.exe", "bun"], [join(homedir(), ".bun", "bin", "bun.exe")]); const denoCli = await findExecutable(["deno.exe", "deno"], [join(homedir(), ".deno", "bin", "deno.exe")]); const wranglerCli = await findExecutable( ["wrangler.cmd", "wrangler"], [ - join(ROOT, "node_modules", ".bin", "wrangler.cmd"), + join(REPO_ROOT, "node_modules", ".bin", "wrangler.cmd"), join(homedir(), "node_modules", ".bin", "wrangler.cmd"), ] ); @@ -37,7 +38,7 @@ const temp = await mkdtemp(join(tmpdir(), "workit-consumer-")); try { const { stdout } = await runNpm(["pack", "--json", "--pack-destination", temp], { - cwd: ROOT, + cwd: PACKAGE_ROOT, timeout: 120_000, }); const [pack] = JSON.parse(stdout); diff --git a/scripts/check-public-proof.mjs b/packages/core/scripts/check-public-proof.mjs similarity index 100% rename from scripts/check-public-proof.mjs rename to packages/core/scripts/check-public-proof.mjs diff --git a/scripts/check-release-provenance.mjs b/packages/core/scripts/check-release-provenance.mjs similarity index 82% rename from scripts/check-release-provenance.mjs rename to packages/core/scripts/check-release-provenance.mjs index c9456c5..70956c7 100644 --- a/scripts/check-release-provenance.mjs +++ b/packages/core/scripts/check-release-provenance.mjs @@ -13,16 +13,19 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +const packageRoot = new URL("../", import.meta.url); +const repoRoot = new URL("../../../", import.meta.url); const packageJson = JSON.parse(await readFile("package.json", "utf8")); -const workflow = await readFile(".github/workflows/release-provenance.yml", "utf8"); +const workflow = await readFile(new URL(".github/workflows/release-provenance.yml", repoRoot), "utf8"); const security = await readFile("SECURITY.md", "utf8"); -const codeowners = await readRequiredFile(".github/CODEOWNERS"); -const allowedSigners = await readRequiredFile(".github/allowed_signers"); -const dependabot = await readRequiredFile(".github/dependabot.yml"); -const ci = await readRequiredFile(".github/workflows/ci.yml"); -const scorecard = await readRequiredFile(".github/workflows/scorecard.yml"); +const codeowners = await readRequiredFile(new URL(".github/CODEOWNERS", repoRoot)); +const allowedSigners = await readRequiredFile(new URL(".github/allowed_signers", repoRoot)); +const dependabot = await readRequiredFile(new URL(".github/dependabot.yml", repoRoot)); +const ci = await readRequiredFile(new URL(".github/workflows/ci.yml", repoRoot)); +const scorecard = await readRequiredFile(new URL(".github/workflows/scorecard.yml", repoRoot)); const requireRegistryDryRun = process.argv.includes("--registry-dry-run"); const execFileAsync = promisify(execFile); @@ -49,7 +52,7 @@ assert.ok(packageJson.files.includes("SECURITY.md"), "published package must inc assert.ok(packageJson.files.includes("CONTRIBUTING.md"), "published package must include CONTRIBUTING.md"); assert.match(workflow, /id-token:\s*write/u, "release workflow must allow OIDC id-token provenance"); assert.match(workflow, /attestations:\s*write/u, "release workflow must allow GitHub artifact attestations"); -assert.match(workflow, /npm publish --provenance --access public/u, "release workflow must publish with npm provenance"); +assert.match(workflow, /npm publish --workspace @workit\/core --provenance --access public/u, "release workflow must publish @workit/core with npm provenance"); assert.match(workflow, /npm run verify/u, "release workflow must run full verification before publish"); assert.match(workflow, /npm run test:coverage/u, "release workflow must run coverage before publish"); assert.match(workflow, /gpg\.ssh\.allowedSignersFile/u, "release workflow must configure SSH allowed signers before tag verification"); @@ -98,20 +101,21 @@ function assertShaPinnedActions(path, text) { } async function assertExistingTagsAreSigned() { - const { stdout } = await execFileAsync("git", ["tag", "--list"]); + const { stdout } = await execFileAsync("git", ["tag", "--list"], { cwd: fileURLToPath(repoRoot) }); for (const tag of stdout.split(/\r?\n/u).filter(Boolean)) { - await execFileAsync("git", ["tag", "-v", tag]); + await execFileAsync("git", ["tag", "-v", tag], { cwd: fileURLToPath(repoRoot) }); } } async function runNpm(args) { if (process.env.npm_execpath !== undefined) { await execFileAsync(process.execPath, [process.env.npm_execpath, ...args], { + cwd: fileURLToPath(packageRoot), timeout: 120_000, }); return; } const npmExecutable = process.platform === "win32" ? "npm.cmd" : "npm"; - await execFileAsync(npmExecutable, args, { timeout: 120_000 }); + await execFileAsync(npmExecutable, args, { cwd: fileURLToPath(packageRoot), timeout: 120_000 }); } diff --git a/scripts/check-sbom.mjs b/packages/core/scripts/check-sbom.mjs similarity index 100% rename from scripts/check-sbom.mjs rename to packages/core/scripts/check-sbom.mjs diff --git a/scripts/check-security.mjs b/packages/core/scripts/check-security.mjs similarity index 85% rename from scripts/check-security.mjs rename to packages/core/scripts/check-security.mjs index f83309b..4c5d0be 100644 --- a/scripts/check-security.mjs +++ b/packages/core/scripts/check-security.mjs @@ -14,18 +14,18 @@ import { join } from "node:path"; const failures = []; const packageJson = JSON.parse(await readFile("package.json", "utf8")); -const packageLock = JSON.parse(await readFile("package-lock.json", "utf8")); +const packageLock = JSON.parse(await readFile("../../package-lock.json", "utf8")); const tsconfig = JSON.parse(stripJsonComments(await readFile("tsconfig.json", "utf8"))); const securityPolicy = await readFile("SECURITY.md", "utf8"); -const rootLock = packageLock.packages?.[""] ?? {}; +const packageLockEntry = packageLock.packages?.["packages/core"] ?? {}; const runtimeDependencies = Object.keys(packageJson.dependencies ?? {}); if (runtimeDependencies.length > 0) { failures.push(`Runtime dependencies must stay empty: ${runtimeDependencies.join(", ")}`); } -if (packageLock.name !== packageJson.name || rootLock.name !== packageJson.name) { - failures.push("package-lock package name must match package.json"); +if (packageLockEntry.name !== packageJson.name) { + failures.push("package-lock workspace package name must match packages/core/package.json"); } for (const [name, version] of Object.entries(packageJson.devDependencies ?? {})) { @@ -34,12 +34,12 @@ for (const [name, version] of Object.entries(packageJson.devDependencies ?? {})) } } -if (JSON.stringify(rootLock.devDependencies ?? {}) !== JSON.stringify(packageJson.devDependencies ?? {})) { - failures.push("package-lock root devDependencies must match package.json exactly"); +if (JSON.stringify(packageLockEntry.devDependencies ?? {}) !== JSON.stringify(packageJson.devDependencies ?? {})) { + failures.push("package-lock workspace devDependencies must match packages/core/package.json exactly"); } -if (JSON.stringify(rootLock.peerDependencies ?? {}) !== JSON.stringify(packageJson.peerDependencies ?? {})) { - failures.push("package-lock root peerDependencies must match package.json exactly"); +if (JSON.stringify(packageLockEntry.peerDependencies ?? {}) !== JSON.stringify(packageJson.peerDependencies ?? {})) { + failures.push("package-lock workspace peerDependencies must match packages/core/package.json exactly"); } for (const lifecycle of ["preinstall", "install", "postinstall"]) { diff --git a/scripts/check-soak.mjs b/packages/core/scripts/check-soak.mjs similarity index 100% rename from scripts/check-soak.mjs rename to packages/core/scripts/check-soak.mjs diff --git a/scripts/check-stream-memory.mjs b/packages/core/scripts/check-stream-memory.mjs similarity index 100% rename from scripts/check-stream-memory.mjs rename to packages/core/scripts/check-stream-memory.mjs diff --git a/scripts/check-tests.mjs b/packages/core/scripts/check-tests.mjs similarity index 100% rename from scripts/check-tests.mjs rename to packages/core/scripts/check-tests.mjs diff --git a/scripts/check-vulnerabilities.mjs b/packages/core/scripts/check-vulnerabilities.mjs similarity index 100% rename from scripts/check-vulnerabilities.mjs rename to packages/core/scripts/check-vulnerabilities.mjs diff --git a/scripts/check-worker-contract-docs.mjs b/packages/core/scripts/check-worker-contract-docs.mjs similarity index 100% rename from scripts/check-worker-contract-docs.mjs rename to packages/core/scripts/check-worker-contract-docs.mjs diff --git a/scripts/generate-sbom.mjs b/packages/core/scripts/generate-sbom.mjs similarity index 87% rename from scripts/generate-sbom.mjs rename to packages/core/scripts/generate-sbom.mjs index c2ab485..76ffa9b 100644 --- a/scripts/generate-sbom.mjs +++ b/packages/core/scripts/generate-sbom.mjs @@ -14,8 +14,8 @@ import { createHash, randomUUID } from "node:crypto"; import { join } from "node:path"; const packageJson = JSON.parse(await readFile("package.json", "utf8")); -const packageLock = JSON.parse(await readFile("package-lock.json", "utf8")); -const rootLock = packageLock.packages?.[""] ?? {}; +const packageLock = JSON.parse(await readFile("../../package-lock.json", "utf8")); +const packageLockEntry = packageLock.packages?.["packages/core"] ?? {}; const runtimeDependencies = Object.keys(packageJson.dependencies ?? {}); if (runtimeDependencies.length > 0) { @@ -25,10 +25,10 @@ if (runtimeDependencies.length > 0) { const bomRef = packagePurl(packageJson.name, packageJson.version); const lockDigest = createHash("sha256") .update(JSON.stringify({ - name: rootLock.name, - version: rootLock.version, - license: rootLock.license, - peerDependencies: rootLock.peerDependencies ?? {}, + name: packageLockEntry.name, + version: packageLockEntry.version, + license: packageLockEntry.license, + peerDependencies: packageLockEntry.peerDependencies ?? {}, })) .digest("hex"); diff --git a/scripts/report-bundle-size.mjs b/packages/core/scripts/report-bundle-size.mjs similarity index 100% rename from scripts/report-bundle-size.mjs rename to packages/core/scripts/report-bundle-size.mjs diff --git a/scripts/soak-24h.mjs b/packages/core/scripts/soak-24h.mjs similarity index 100% rename from scripts/soak-24h.mjs rename to packages/core/scripts/soak-24h.mjs diff --git a/scripts/soak-runtime.mjs b/packages/core/scripts/soak-runtime.mjs similarity index 100% rename from scripts/soak-runtime.mjs rename to packages/core/scripts/soak-runtime.mjs diff --git a/src/ai/index.ts b/packages/core/src/ai/index.ts similarity index 100% rename from src/ai/index.ts rename to packages/core/src/ai/index.ts diff --git a/src/channel/index.ts b/packages/core/src/channel/index.ts similarity index 100% rename from src/channel/index.ts rename to packages/core/src/channel/index.ts diff --git a/src/diagnostics/index.ts b/packages/core/src/diagnostics/index.ts similarity index 100% rename from src/diagnostics/index.ts rename to packages/core/src/diagnostics/index.ts diff --git a/src/engine/context.ts b/packages/core/src/engine/context.ts similarity index 100% rename from src/engine/context.ts rename to packages/core/src/engine/context.ts diff --git a/src/engine/duration.ts b/packages/core/src/engine/duration.ts similarity index 100% rename from src/engine/duration.ts rename to packages/core/src/engine/duration.ts diff --git a/src/engine/event-bus.ts b/packages/core/src/engine/event-bus.ts similarity index 100% rename from src/engine/event-bus.ts rename to packages/core/src/engine/event-bus.ts diff --git a/src/engine/retry.ts b/packages/core/src/engine/retry.ts similarity index 100% rename from src/engine/retry.ts rename to packages/core/src/engine/retry.ts diff --git a/src/engine/scope.ts b/packages/core/src/engine/scope.ts similarity index 100% rename from src/engine/scope.ts rename to packages/core/src/engine/scope.ts diff --git a/src/engine/tree.ts b/packages/core/src/engine/tree.ts similarity index 100% rename from src/engine/tree.ts rename to packages/core/src/engine/tree.ts diff --git a/src/index.ts b/packages/core/src/index.ts similarity index 100% rename from src/index.ts rename to packages/core/src/index.ts diff --git a/src/observability/index.ts b/packages/core/src/observability/index.ts similarity index 100% rename from src/observability/index.ts rename to packages/core/src/observability/index.ts diff --git a/src/otel/index.ts b/packages/core/src/otel/index.ts similarity index 100% rename from src/otel/index.ts rename to packages/core/src/otel/index.ts diff --git a/src/run/index.ts b/packages/core/src/run/index.ts similarity index 100% rename from src/run/index.ts rename to packages/core/src/run/index.ts diff --git a/src/runtime/unsupported.ts b/packages/core/src/runtime/unsupported.ts similarity index 100% rename from src/runtime/unsupported.ts rename to packages/core/src/runtime/unsupported.ts diff --git a/src/types/index.ts b/packages/core/src/types/index.ts similarity index 100% rename from src/types/index.ts rename to packages/core/src/types/index.ts diff --git a/src/work/index.ts b/packages/core/src/work/index.ts similarity index 100% rename from src/work/index.ts rename to packages/core/src/work/index.ts diff --git a/src/worker/index.ts b/packages/core/src/worker/index.ts similarity index 100% rename from src/worker/index.ts rename to packages/core/src/worker/index.ts diff --git a/src/worker/module-url.ts b/packages/core/src/worker/module-url.ts similarity index 100% rename from src/worker/module-url.ts rename to packages/core/src/worker/module-url.ts diff --git a/src/worker/runner.ts b/packages/core/src/worker/runner.ts similarity index 100% rename from src/worker/runner.ts rename to packages/core/src/worker/runner.ts diff --git a/tests/evidence/correctness/runtime-contracts.mjs b/packages/core/tests/evidence/correctness/runtime-contracts.mjs similarity index 100% rename from tests/evidence/correctness/runtime-contracts.mjs rename to packages/core/tests/evidence/correctness/runtime-contracts.mjs diff --git a/tests/evidence/harness.mjs b/packages/core/tests/evidence/harness.mjs similarity index 100% rename from tests/evidence/harness.mjs rename to packages/core/tests/evidence/harness.mjs diff --git a/tests/evidence/lifecycle/owned-work.mjs b/packages/core/tests/evidence/lifecycle/owned-work.mjs similarity index 100% rename from tests/evidence/lifecycle/owned-work.mjs rename to packages/core/tests/evidence/lifecycle/owned-work.mjs diff --git a/tests/evidence/performance/benchmark-contracts.mjs b/packages/core/tests/evidence/performance/benchmark-contracts.mjs similarity index 100% rename from tests/evidence/performance/benchmark-contracts.mjs rename to packages/core/tests/evidence/performance/benchmark-contracts.mjs diff --git a/tests/evidence/release/release-integrity.mjs b/packages/core/tests/evidence/release/release-integrity.mjs similarity index 100% rename from tests/evidence/release/release-integrity.mjs rename to packages/core/tests/evidence/release/release-integrity.mjs diff --git a/tests/evidence/run-all.mjs b/packages/core/tests/evidence/run-all.mjs similarity index 100% rename from tests/evidence/run-all.mjs rename to packages/core/tests/evidence/run-all.mjs diff --git a/tests/evidence/security/cpu-spinner.mjs b/packages/core/tests/evidence/security/cpu-spinner.mjs similarity index 100% rename from tests/evidence/security/cpu-spinner.mjs rename to packages/core/tests/evidence/security/cpu-spinner.mjs diff --git a/tests/evidence/security/worker-boundary.mjs b/packages/core/tests/evidence/security/worker-boundary.mjs similarity index 100% rename from tests/evidence/security/worker-boundary.mjs rename to packages/core/tests/evidence/security/worker-boundary.mjs diff --git a/tests/property/core-invariants.property.test.js b/packages/core/tests/property/core-invariants.property.test.js similarity index 100% rename from tests/property/core-invariants.property.test.js rename to packages/core/tests/property/core-invariants.property.test.js diff --git a/tests/unit/ai.test.js b/packages/core/tests/unit/ai.test.js similarity index 100% rename from tests/unit/ai.test.js rename to packages/core/tests/unit/ai.test.js diff --git a/tests/unit/channel.test.js b/packages/core/tests/unit/channel.test.js similarity index 100% rename from tests/unit/channel.test.js rename to packages/core/tests/unit/channel.test.js diff --git a/tests/unit/claim-gaps.test.js b/packages/core/tests/unit/claim-gaps.test.js similarity index 100% rename from tests/unit/claim-gaps.test.js rename to packages/core/tests/unit/claim-gaps.test.js diff --git a/tests/unit/contracts.test.js b/packages/core/tests/unit/contracts.test.js similarity index 100% rename from tests/unit/contracts.test.js rename to packages/core/tests/unit/contracts.test.js diff --git a/tests/unit/coverage.test.js b/packages/core/tests/unit/coverage.test.js similarity index 100% rename from tests/unit/coverage.test.js rename to packages/core/tests/unit/coverage.test.js diff --git a/tests/unit/diagnostics.test.js b/packages/core/tests/unit/diagnostics.test.js similarity index 100% rename from tests/unit/diagnostics.test.js rename to packages/core/tests/unit/diagnostics.test.js diff --git a/tests/unit/event-bus.test.js b/packages/core/tests/unit/event-bus.test.js similarity index 100% rename from tests/unit/event-bus.test.js rename to packages/core/tests/unit/event-bus.test.js diff --git a/tests/unit/examples.test.js b/packages/core/tests/unit/examples.test.js similarity index 100% rename from tests/unit/examples.test.js rename to packages/core/tests/unit/examples.test.js diff --git a/tests/unit/invariants.test.js b/packages/core/tests/unit/invariants.test.js similarity index 100% rename from tests/unit/invariants.test.js rename to packages/core/tests/unit/invariants.test.js diff --git a/tests/unit/observability.test.js b/packages/core/tests/unit/observability.test.js similarity index 100% rename from tests/unit/observability.test.js rename to packages/core/tests/unit/observability.test.js diff --git a/tests/unit/otel.test.js b/packages/core/tests/unit/otel.test.js similarity index 100% rename from tests/unit/otel.test.js rename to packages/core/tests/unit/otel.test.js diff --git a/tests/unit/perf-contracts.test.js b/packages/core/tests/unit/perf-contracts.test.js similarity index 100% rename from tests/unit/perf-contracts.test.js rename to packages/core/tests/unit/perf-contracts.test.js diff --git a/tests/unit/run.test.js b/packages/core/tests/unit/run.test.js similarity index 100% rename from tests/unit/run.test.js rename to packages/core/tests/unit/run.test.js diff --git a/tests/unit/samples.test.js b/packages/core/tests/unit/samples.test.js similarity index 100% rename from tests/unit/samples.test.js rename to packages/core/tests/unit/samples.test.js diff --git a/tests/unit/sanity.test.js b/packages/core/tests/unit/sanity.test.js similarity index 100% rename from tests/unit/sanity.test.js rename to packages/core/tests/unit/sanity.test.js diff --git a/tests/unit/tree.test.js b/packages/core/tests/unit/tree.test.js similarity index 100% rename from tests/unit/tree.test.js rename to packages/core/tests/unit/tree.test.js diff --git a/tests/unit/work.test.js b/packages/core/tests/unit/work.test.js similarity index 100% rename from tests/unit/work.test.js rename to packages/core/tests/unit/work.test.js diff --git a/tests/unit/worker.test.js b/packages/core/tests/unit/worker.test.js similarity index 100% rename from tests/unit/worker.test.js rename to packages/core/tests/unit/worker.test.js diff --git a/tsconfig.json b/packages/core/tsconfig.json similarity index 100% rename from tsconfig.json rename to packages/core/tsconfig.json diff --git a/vitest.config.ts b/packages/core/vitest.config.ts similarity index 100% rename from vitest.config.ts rename to packages/core/vitest.config.ts