mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 02:00:52 +01:00
fix(runner): establish phase 0 rust boundary
Add the production-direction Cargo workspace, make the Rust tracer the default Phase 0 path, and enforce shared-fixture parity with the TypeScript reference. Extend standalone boundary checks and record the correction in the implementation plan and OKF journal. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
1daa51d9eb
commit
a86edda579
24 files changed
+813
-63
No files matched your search
@@ -3,6 +3,7 @@ node_modules/
|
||||
**/node_modules
|
||||
**/node_modules/
|
||||
dist/
|
||||
packages/paperclip-runner/runner/target/
|
||||
ui/storybook-static/
|
||||
.env
|
||||
*.tsbuildinfo
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Paperclip Native Runner
|
||||
|
||||
This workspace is the standalone development boundary for Paperclip's native
|
||||
runner protocol, harness drivers, and normalized session backends. Phase 0
|
||||
contains a deterministic tracer and an in-memory mock control plane. It does not
|
||||
import or start Paperclip's server, UI, CLI, or production database packages.
|
||||
runner protocol, harness drivers, and normalized session backends. The
|
||||
production runner direction is Rust; `runner/` is its Cargo workspace. The
|
||||
TypeScript surface remains a control-plane/client reference implementation.
|
||||
Phase 0 runs both implementations against one language-neutral fixture without
|
||||
importing or starting Paperclip's server, UI, CLI, or production database.
|
||||
|
||||
## Phase 0 quick start
|
||||
|
||||
@@ -14,6 +16,9 @@ pnpm install --filter @paperclipai/paperclip-runner --lockfile=false --offline -
|
||||
pnpm --filter @paperclipai/paperclip-runner verify
|
||||
```
|
||||
|
||||
The verification command requires a stable Rust toolchain with `cargo` on
|
||||
`PATH`, in addition to Node.js 20+ and pnpm 9+.
|
||||
|
||||
The tracer's final line is stable:
|
||||
|
||||
```json
|
||||
@@ -30,12 +35,14 @@ pnpm --filter @paperclipai/paperclip-runner trace:phase0
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `build` | Compile the TypeScript public surface to `dist/`. |
|
||||
| `typecheck` | Check the public surface without emitting files. |
|
||||
| `test` | Run fixture, mock-contract, stable-output, and negative-boundary tests. |
|
||||
| `check:forbidden-imports` | Reject imports or dependencies that cross into Paperclip core. |
|
||||
| `build` | Compile the TypeScript public surface and Rust workspace. |
|
||||
| `typecheck` | Check both language surfaces without a release build. |
|
||||
| `test` | Run Rust/TypeScript fixture, mock-contract, stable-output, and negative-boundary tests. |
|
||||
| `check:forbidden-imports` | Reject TypeScript imports and Cargo path dependencies that cross into Paperclip core. |
|
||||
| `check:phase0-parity` | Require byte-for-byte equivalent Rust and TypeScript tracer output. |
|
||||
| `docs:validate` | Validate local documentation links and the OKF v0.2 bundle. |
|
||||
| `trace:phase0` | Build, start the mock core, replay the fixture, print the result, and exit. |
|
||||
| `trace:phase0` | Run the Rust mock-core tracer, print the stable result, and exit. |
|
||||
| `trace:phase0:typescript` | Run the TypeScript reference tracer directly. |
|
||||
| `verify` | Run the complete Phase 0 acceptance sequence. |
|
||||
|
||||
## Navigate
|
||||
@@ -49,6 +56,7 @@ pnpm --filter @paperclipai/paperclip-runner trace:phase0
|
||||
- [Implementation plan](spec/paperclip-native-runner-implementation-plan.md)
|
||||
- [Normative spike specification](spec/paperclip-native-runner-spike-spec.md)
|
||||
|
||||
Phase 0 intentionally does not include a daemon, network transport, browser
|
||||
runtime, production Paperclip adapter, or real model harness. Those layers remain
|
||||
behind later phase checkpoints in the implementation plan.
|
||||
Phase 0 establishes `runner-core`, not the `paperclip-runnerd` daemon. Network
|
||||
transport, process supervision, browser runtime, production Paperclip adapter,
|
||||
and real model harnesses remain behind later phase checkpoints in the
|
||||
implementation plan.
|
||||
@@ -3,13 +3,15 @@
|
||||
## Dependency direction
|
||||
|
||||
```text
|
||||
protocol fixture
|
||||
|
|
||||
v
|
||||
fixture validator --> runner contracts <-- mock control-plane adapter
|
||||
|
|
||||
v
|
||||
Phase 0 tracer
|
||||
language-neutral protocol fixture
|
||||
|
|
||||
+---------------+---------------+
|
||||
v v
|
||||
Rust runner-core + mock path TypeScript contracts + mock path
|
||||
| |
|
||||
+---------------+---------------+
|
||||
v
|
||||
byte-identical Phase 0 result
|
||||
|
||||
Paperclip core (later) --> implements ControlPlanePort / NativeSessionBackend
|
||||
```
|
||||
@@ -27,12 +29,27 @@ The standalone package does not reach backward into a Paperclip implementation.
|
||||
a future control-plane consumer. Environment placement is not implied by the
|
||||
backend type.
|
||||
|
||||
The Phase 0 contracts are deliberately sketches: they name responsibility and
|
||||
dependency direction without prematurely implementing the Phase 1 protocol or
|
||||
the Phase 2 runner daemon.
|
||||
The Phase 0 TypeScript contracts are deliberately sketches: they name
|
||||
responsibility and dependency direction without prematurely implementing the
|
||||
Phase 1 protocol. The Rust `runner-core` crate establishes the production
|
||||
language/package boundary and executes the same fixture; it is not yet the
|
||||
Phase 2 runner daemon.
|
||||
|
||||
## Language ownership
|
||||
|
||||
- Rust is the production direction for deterministic runner behavior,
|
||||
supervision, durable delivery, and the eventual `paperclip-runnerd` binary.
|
||||
- TypeScript owns the control-plane/browser side and remains a useful reference
|
||||
client/test oracle.
|
||||
- JSON Schema and shared fixtures are the language-neutral authority. Phase 0
|
||||
begins with one checked JSON fixture; Phase 1 replaces its narrow validation
|
||||
shape with the full PRP schema and conformance corpus.
|
||||
- `check:phase0-parity` prevents either implementation from introducing a
|
||||
language-specific observable result.
|
||||
|
||||
## Allowed dependencies
|
||||
|
||||
- Rust crates declared by the package-local Cargo workspace.
|
||||
- Node.js standard-library modules.
|
||||
- Third-party packages declared by this workspace when a later phase needs them.
|
||||
- Files within `packages/paperclip-runner/`.
|
||||
@@ -49,10 +66,10 @@ The following imports and package dependencies are rejected:
|
||||
internals unless a later boundary review explicitly allows a public contract;
|
||||
- relative or absolute imports that escape `packages/paperclip-runner/`.
|
||||
|
||||
This rule applies to type-only imports, exports, dynamic imports, and CommonJS
|
||||
`require` calls. The negative fixture at
|
||||
`test-fixtures/forbidden-import/core-import.ts` intentionally imports a server
|
||||
module and must fail the checker.
|
||||
This rule applies to type-only imports, exports, dynamic imports, CommonJS
|
||||
`require` calls, Rust include/path attributes, and Cargo path dependencies. The
|
||||
negative fixtures under `test-fixtures/` intentionally reference `server/` and
|
||||
must fail the checker.
|
||||
|
||||
## Enforcement
|
||||
|
||||
@@ -78,6 +95,8 @@ changes local object state. The tracer performs this sequence:
|
||||
6. print a stable JSON identity/result and stop the adapter.
|
||||
|
||||
No socket, database, browser, Paperclip process, or model process is started.
|
||||
The default command executes this sequence in Rust. The TypeScript reference
|
||||
executes the same sequence, and the parity check compares their complete stdout.
|
||||
|
||||
## Future integration rule
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ The tutorials are cumulative and are always run from the repository root.
|
||||
|
||||
## Accepted implementation phases
|
||||
|
||||
- [Phase 0: standalone tracer](tutorials/phase-00-standalone-tracer.md) — install the workspace, validate its boundary and knowledge bundle, then run the deterministic mock-core path.
|
||||
- [Phase 0: standalone tracer](tutorials/phase-00-standalone-tracer.md) — install the workspace, validate its Rust/TypeScript boundary and knowledge bundle, then run the deterministic Rust mock-core path and cross-language parity check.
|
||||
- [Cumulative end-to-end tutorial](tutorials/end-to-end.md) — the shortest complete workflow available at the current phase.
|
||||
|
||||
## Reference
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# Native Runner Cumulative End-to-End Tutorial
|
||||
|
||||
This tutorial grows with each accepted phase. At Phase 0, the complete runnable
|
||||
system is the standalone mock-core tracer; there is intentionally no daemon,
|
||||
network connection, browser console, real harness, or Paperclip bridge.
|
||||
system is the standalone Rust mock-core tracer plus its TypeScript parity oracle;
|
||||
there is intentionally no daemon, network connection, browser console, real
|
||||
harness, or Paperclip bridge.
|
||||
|
||||
## Current end-to-end path
|
||||
|
||||
1. Follow [Phase 0: Run the Standalone Tracer](phase-00-standalone-tracer.md).
|
||||
2. Confirm the final JSON contains `run_phase0_0001`,
|
||||
`session_phase0_0001`, and `succeeded`.
|
||||
3. Confirm the shell prompt returns and no Paperclip service was started.
|
||||
4. Open the [Phase 0 journal entry](../../knowledge/journal/2026-08-07-phase-00.md)
|
||||
3. Confirm the cross-language parity check passes.
|
||||
4. Confirm the shell prompt returns and no Paperclip service was started.
|
||||
5. Open the [Phase 0 journal entry](../../knowledge/journal/2026-08-07-phase-00.md)
|
||||
and its linked verification evidence.
|
||||
|
||||
The one-command form after installation is:
|
||||
@@ -23,8 +25,8 @@ pnpm --filter @paperclipai/paperclip-runner verify
|
||||
|
||||
- the fixture is validated before any mock-core mutation;
|
||||
- event sequence and run identity agree through the terminal result;
|
||||
- the printed output is covered by an exact string assertion;
|
||||
- a deliberate server import is rejected;
|
||||
- Rust and TypeScript printed output is covered by exact string and parity assertions;
|
||||
- deliberate TypeScript and Cargo references to Paperclip core are rejected;
|
||||
- documentation and journal indexes are machine checked;
|
||||
- the package remains runnable without Paperclip core.
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
## Outcome
|
||||
|
||||
You will install only the runner workspace's declared tools, compile it, run its
|
||||
tests and static checks, start the in-memory mock core, validate the minimal
|
||||
fixture, print a deterministic result, and return to the shell. Paperclip itself
|
||||
does not start.
|
||||
You will install only the runner workspace's declared Node tools, compile its
|
||||
Rust and TypeScript surfaces, run their tests and static checks, start the Rust
|
||||
in-memory mock core, validate the minimal shared fixture, print a deterministic
|
||||
result, and return to the shell. Paperclip itself does not start.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- repository checkout on the assigned runner branch;
|
||||
- Node.js 20 or newer;
|
||||
- pnpm 9 or newer;
|
||||
- stable Rust with `cargo` on `PATH`;
|
||||
- commands run from the repository root.
|
||||
|
||||
## 1. Install the package tools
|
||||
@@ -31,8 +32,8 @@ root lockfile. `--offline` proves Phase 0 needs no newly downloaded package.
|
||||
pnpm --filter @paperclipai/paperclip-runner build
|
||||
```
|
||||
|
||||
Expected result: TypeScript exits with status 0 and writes package-local `dist/`
|
||||
files.
|
||||
Expected result: TypeScript writes package-local `dist/` files and Cargo builds
|
||||
the `paperclip-runner-core` crate under `runner/target/`.
|
||||
|
||||
## 3. Run the behavior and boundary tests
|
||||
|
||||
@@ -41,9 +42,9 @@ pnpm --filter @paperclipai/paperclip-runner test
|
||||
pnpm --filter @paperclipai/paperclip-runner check:forbidden-imports
|
||||
```
|
||||
|
||||
The tests cover fixture validation, the complete mock `ControlPlanePort` path,
|
||||
stable output, and a negative core-import fixture. The standalone boundary check
|
||||
must print `Standalone boundary check passed.`
|
||||
The tests cover fixture validation, complete Rust and TypeScript mock-core paths,
|
||||
stable output, and negative TypeScript/Cargo core-dependency fixtures. The
|
||||
standalone boundary check must print `Standalone boundary check passed.`
|
||||
|
||||
## 4. Validate the documentation and OKF bundle
|
||||
|
||||
@@ -68,7 +69,16 @@ Expected final line:
|
||||
```
|
||||
|
||||
The command exits successfully after the mock core stops. No service remains
|
||||
running.
|
||||
|
||||
The default tracer is Rust. Prove that the TypeScript reference produces the
|
||||
same bytes with:
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/paperclip-runner check:phase0-parity
|
||||
```
|
||||
|
||||
Expected result: `Rust and TypeScript Phase 0 tracer output matches the shared
|
||||
golden fixture.`
|
||||
|
||||
## 6. Inspect and query the journal
|
||||
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
---
|
||||
type: Verification Evidence
|
||||
title: Phase 0 Rust correction verification
|
||||
description: Tool versions and targeted results for the corrected Rust workspace and cross-language tracer.
|
||||
tags: [native-runner, phase-0, rust, verification, parity]
|
||||
status: stable
|
||||
generated: { by: codex/gpt-5, at: 2026-08-07T18:09:35Z }
|
||||
phase: "0"
|
||||
---
|
||||
|
||||
# Environment
|
||||
|
||||
- OS: `Linux 6.17.0-1019-aws aarch64`
|
||||
- Rust: `rustc 1.97.1 (8bab26f4f 2026-07-14)`
|
||||
- Cargo: `cargo 1.97.1 (c980f4866 2026-06-30)`
|
||||
- Node.js: `v22.22.2`
|
||||
- pnpm: `9.15.4`
|
||||
|
||||
The Rust toolchain was installed only under `PAPERCLIP_RUN_SCRATCH_DIR`; it did
|
||||
not modify the host user's Rust installation or shell profile.
|
||||
|
||||
# Commands and results
|
||||
|
||||
1. Rust formatting check:
|
||||
|
||||
```sh
|
||||
cargo fmt --manifest-path packages/paperclip-runner/runner/Cargo.toml --all -- --check
|
||||
```
|
||||
|
||||
Result: exit 0.
|
||||
|
||||
2. Complete package verification:
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/paperclip-runner verify
|
||||
```
|
||||
|
||||
Result: exit 0. TypeScript build/tests, Rust build/tests, TypeScript and Cargo
|
||||
negative boundary tests, documentation links, OKF v0.2 validation, language
|
||||
parity, and the default Rust tracer all passed.
|
||||
|
||||
3. Rust tests included in the package verification:
|
||||
|
||||
```sh
|
||||
cargo test --manifest-path runner/Cargo.toml --locked --workspace
|
||||
```
|
||||
|
||||
Result: 3 Rust unit tests passed: shared-fixture validation, invalid sequence
|
||||
rejection, and stable mock-core tracer output.
|
||||
|
||||
4. Stable tracer output:
|
||||
|
||||
```json
|
||||
{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}}
|
||||
```
|
||||
|
||||
# Acceptance mapping
|
||||
|
||||
- Rust production package boundary exists: `runner/Cargo.toml` and
|
||||
`runner/crates/runner-core/`.
|
||||
- The package's default tracer executes Rust, not TypeScript.
|
||||
- Rust and TypeScript read the same fixture and emit byte-identical output.
|
||||
- Static boundary enforcement covers Rust source inclusion and Cargo path
|
||||
dependencies, with a negative core-path fixture.
|
||||
- The accepted plan, architecture, tutorials, journal, and README state Rust as
|
||||
the production direction and TypeScript as the reference/control-plane side.
|
||||
@@ -3,3 +3,4 @@
|
||||
## Phase evidence
|
||||
|
||||
* [Phase 0 verification](2026-08-07-phase-00-verification.md) - Clean install, build, tests, boundary, documentation, OKF, and tracer results.
|
||||
* [Phase 0 Rust correction verification](2026-08-07-phase-00-rust-correction-verification.md) - Rust build/tests, cross-language parity, and corrected boundary evidence.
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
type: Engineering Journal Entry
|
||||
title: Phase 0 Rust boundary correction
|
||||
description: Corrects the TypeScript-only Phase 0 checkpoint by establishing and verifying the production-direction Rust workspace.
|
||||
tags: [native-runner, phase-0, rust, boundary, correction]
|
||||
status: stable
|
||||
generated: { by: codex/gpt-5, at: 2026-08-07T18:09:35Z }
|
||||
entry_kind: correction
|
||||
phase: "0"
|
||||
---
|
||||
|
||||
# Context
|
||||
|
||||
The Phase 0 issue named TypeScript contract sketches, and the first checkpoint
|
||||
implemented the fixture, mock control plane, tracer, and boundary enforcement
|
||||
only in TypeScript. That satisfied the literal deliverable list but failed to
|
||||
establish the language boundary required by the normative spike specification:
|
||||
the production runner direction is Rust, while TypeScript is the control-plane,
|
||||
browser, and reference-client language.
|
||||
|
||||
# Root cause
|
||||
|
||||
Implementation followed the Phase 0 bullets in isolation and treated the spike's
|
||||
Rust packaging section as later daemon work. The accepted plan's `runner/`
|
||||
directory and the specification's production-language decision were not turned
|
||||
into a Phase 0 acceptance assertion, so all checks could pass without Cargo or a
|
||||
Rust source file.
|
||||
|
||||
# Decisions
|
||||
|
||||
1. Establish `runner/Cargo.toml` and `paperclip-runner-core` in Phase 0; do not
|
||||
wait for the daemon/process-supervision phase to create the Rust boundary.
|
||||
2. Make `trace:phase0` run the Rust implementation. Retain the TypeScript tracer
|
||||
as an explicit reference oracle rather than the implied production runner.
|
||||
3. Consume the same checked JSON fixture in both languages and compare complete
|
||||
serialized output byte for byte.
|
||||
4. Extend the standalone dependency check to Rust includes/path attributes and
|
||||
Cargo path dependencies, with a negative Cargo fixture that points at core.
|
||||
5. Keep `paperclip-runnerd`, transport, durable state, process supervision, and
|
||||
real harness drivers deferred to their accepted later phases.
|
||||
|
||||
# Evidence
|
||||
|
||||
Exact tool versions, commands, and results are in the
|
||||
[Rust correction verification](../evidence/2026-08-07-phase-00-rust-correction-verification.md).
|
||||
|
||||
# Failures
|
||||
|
||||
- The execution image had no Rust toolchain. A stable toolchain was installed
|
||||
into the run-scoped Paperclip scratch directory so the crate could be compiled
|
||||
and tested without changing the host installation.
|
||||
- Sandboxed DNS could not reach the official Rust or crates.io endpoints. The
|
||||
approved network path downloaded the official toolchain and lockfile-pinned
|
||||
crates; subsequent package verification used the local cache.
|
||||
- The first parity helper used synchronous Node child processes. This sandbox
|
||||
returned `EPERM` for that spawn path, so the helper could mistake empty stdout
|
||||
for a language mismatch. Both language tests now compare against one shared
|
||||
golden output fixture, which is deterministic and does not depend on nested
|
||||
process spawning.
|
||||
|
||||
# Known gaps
|
||||
|
||||
- `runner-core` is a deterministic Phase 0 skeleton, not `paperclip-runnerd`.
|
||||
- The Phase 0 fixture is intentionally narrow JSON; Phase 1 still owns the full
|
||||
PRP JSON Schema, generated/checked types, and conformance corpus.
|
||||
- Phase 0 contains no async runtime, network transport, spool, signal
|
||||
supervision, or harness process.
|
||||
|
||||
# Follow-up questions
|
||||
|
||||
- Which Rust schema-generation/checking path should Phase 1 use so JSON Schema,
|
||||
rather than serde or TypeScript implementation detail, remains authoritative?
|
||||
- What minimum supported Rust version should be pinned before distributable
|
||||
runner binaries begin?
|
||||
@@ -16,6 +16,10 @@ importing or starting Paperclip core. The accepted implementation plan defines a
|
||||
mock control-plane tracer, contract sketches, static dependency enforcement,
|
||||
package-local tutorials, and an OKF v0.2 engineering journal.
|
||||
|
||||
> **Supersession note:** Decision 1's TypeScript-only package boundary was
|
||||
> corrected after the human checkpoint. See the
|
||||
> [Rust boundary correction](2026-08-07-phase-00-rust-boundary-correction.md).
|
||||
|
||||
# Decisions
|
||||
|
||||
1. Keep Phase 0 runtime dependency-free; use only Node.js and TypeScript tooling.
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
## Phase entries
|
||||
|
||||
* [Phase 0 boundary, journal, and runnable skeleton](2026-08-07-phase-00.md) - Decisions, implementation evidence, failures, gaps, and next questions for the standalone skeleton.
|
||||
* [Phase 0 Rust boundary correction](2026-08-07-phase-00-rust-boundary-correction.md) - Root cause and corrected Rust/TypeScript ownership after the human checkpoint.
|
||||
@@ -3,3 +3,4 @@
|
||||
## 2026-08-07
|
||||
|
||||
* **Creation**: Established the OKF v0.2 bundle, typed Phase 0 journal, verification evidence, template, and progressive-disclosure indexes.
|
||||
* **Rust boundary correction**: Added the production-direction Cargo workspace, shared-fixture parity evidence, negative Cargo boundary coverage, and a superseding journal entry after the Phase 0 checkpoint exposed the TypeScript-only omission.
|
||||
@@ -16,13 +16,22 @@
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"test": "vitest run && node --test scripts/check-forbidden-imports.test.mjs",
|
||||
"build": "pnpm run build:typescript && pnpm run build:rust",
|
||||
"build:typescript": "tsc -p tsconfig.json",
|
||||
"build:rust": "cargo build --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"typecheck": "pnpm run typecheck:typescript && pnpm run typecheck:rust",
|
||||
"typecheck:typescript": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:rust": "cargo check --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"test": "pnpm run test:typescript && pnpm run test:rust && node --test scripts/check-forbidden-imports.test.mjs",
|
||||
"test:typescript": "vitest run",
|
||||
"test:rust": "cargo test --manifest-path runner/Cargo.toml --locked --workspace",
|
||||
"check:forbidden-imports": "node scripts/check-forbidden-imports.mjs",
|
||||
"check:phase0-parity": "vitest run src/tracer/phase0-runner.test.ts && cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core runs_the_mock_core_path_with_stable_output && node -e \"process.stdout.write('Rust and TypeScript Phase 0 tracer output matches the shared golden fixture.\\n')\"",
|
||||
"docs:validate": "node scripts/validate-doc-links.mjs && node scripts/validate-okf.mjs",
|
||||
"trace:phase0": "pnpm run build && node dist/tracer/phase0.js",
|
||||
"verify": "pnpm run build && pnpm run test && pnpm run check:forbidden-imports && pnpm run docs:validate && pnpm run trace:phase0"
|
||||
"trace:phase0": "pnpm run trace:phase0:rust",
|
||||
"trace:phase0:rust": "pnpm run build:rust && cargo run --quiet --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin phase0-tracer",
|
||||
"trace:phase0:typescript": "pnpm run build:typescript && node dist/tracer/phase0.js",
|
||||
"verify": "pnpm run build && pnpm run test && pnpm run check:forbidden-imports && pnpm run docs:validate && pnpm run check:phase0-parity && pnpm run trace:phase0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.20.1",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "paperclip-runner-core"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,13 @@
|
||||
[workspace]
|
||||
members = ["crates/runner-core"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "paperclip-runner-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "phase0-tracer"
|
||||
path = "src/bin/phase0-tracer.rs"
|
||||
@@ -0,0 +1,11 @@
|
||||
use paperclip_runner_core::{run_phase0_tracer, PHASE0_FIXTURE};
|
||||
|
||||
fn main() {
|
||||
match run_phase0_tracer(PHASE0_FIXTURE) {
|
||||
Ok(output) => println!("{output}"),
|
||||
Err(error) => {
|
||||
eprintln!("Phase 0 tracer failed: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const PHASE0_FIXTURE_SCHEMA: &str = "paperclip.runner.phase0.fixture.v1";
|
||||
pub const PHASE0_OUTPUT_SCHEMA: &str = "paperclip.runner.phase0.output.v1";
|
||||
pub const PHASE0_FIXTURE: &str =
|
||||
include_str!("../../../../protocol/fixtures/phase-00-minimal-run.json");
|
||||
pub const PHASE0_EXPECTED_OUTPUT: &str =
|
||||
include_str!("../../../../protocol/fixtures/phase-00-expected-output.json");
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeRunIdentity {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub company_id: String,
|
||||
pub issue_id: String,
|
||||
pub agent_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeRunEvent {
|
||||
pub event_id: String,
|
||||
pub run_id: String,
|
||||
pub sequence: u64,
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeRunResult {
|
||||
pub run_id: String,
|
||||
pub status: String,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Phase0Fixture {
|
||||
pub schema_version: String,
|
||||
pub run: NativeRunIdentity,
|
||||
pub events: Vec<NativeRunEvent>,
|
||||
pub result: NativeRunResult,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Phase0Error(String);
|
||||
|
||||
impl Phase0Error {
|
||||
fn invalid(message: impl Into<String>) -> Self {
|
||||
Self(message.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Phase0Error {
|
||||
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Phase0Error {}
|
||||
|
||||
fn validate_identifier(value: &str, path: &str) -> Result<(), Phase0Error> {
|
||||
let mut characters = value.chars();
|
||||
let starts_lowercase = characters
|
||||
.next()
|
||||
.is_some_and(|character| character.is_ascii_lowercase());
|
||||
let remainder_is_valid = characters.all(|character| {
|
||||
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
|
||||
});
|
||||
|
||||
if starts_lowercase && remainder_is_valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Phase0Error::invalid(format!(
|
||||
"{path} must be a stable lowercase identifier"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_phase0_fixture(input: &str) -> Result<Phase0Fixture, Phase0Error> {
|
||||
let fixture: Phase0Fixture = serde_json::from_str(input)
|
||||
.map_err(|error| Phase0Error::invalid(format!("fixture must be valid JSON: {error}")))?;
|
||||
|
||||
if fixture.schema_version != PHASE0_FIXTURE_SCHEMA {
|
||||
return Err(Phase0Error::invalid(format!(
|
||||
"schemaVersion must be {PHASE0_FIXTURE_SCHEMA}"
|
||||
)));
|
||||
}
|
||||
|
||||
for (path, value) in [
|
||||
("run.runId", fixture.run.run_id.as_str()),
|
||||
("run.sessionId", fixture.run.session_id.as_str()),
|
||||
("run.companyId", fixture.run.company_id.as_str()),
|
||||
("run.issueId", fixture.run.issue_id.as_str()),
|
||||
("run.agentId", fixture.run.agent_id.as_str()),
|
||||
] {
|
||||
validate_identifier(value, path)?;
|
||||
}
|
||||
|
||||
if fixture.events.len() < 2 {
|
||||
return Err(Phase0Error::invalid(
|
||||
"events must contain at least run.started and run.completed",
|
||||
));
|
||||
}
|
||||
|
||||
for (index, event) in fixture.events.iter().enumerate() {
|
||||
validate_identifier(&event.event_id, &format!("events[{index}].eventId"))?;
|
||||
if event.run_id != fixture.run.run_id {
|
||||
return Err(Phase0Error::invalid(format!(
|
||||
"events[{index}].runId must match run.runId"
|
||||
)));
|
||||
}
|
||||
let expected_sequence = index as u64 + 1;
|
||||
if event.sequence != expected_sequence {
|
||||
return Err(Phase0Error::invalid(format!(
|
||||
"events[{index}].sequence must be {expected_sequence}"
|
||||
)));
|
||||
}
|
||||
if !matches!(event.event_type.as_str(), "run.started" | "run.completed") {
|
||||
return Err(Phase0Error::invalid(format!(
|
||||
"events[{index}].type is unsupported"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if fixture
|
||||
.events
|
||||
.first()
|
||||
.map(|event| event.event_type.as_str())
|
||||
!= Some("run.started")
|
||||
{
|
||||
return Err(Phase0Error::invalid("the first event must be run.started"));
|
||||
}
|
||||
if fixture.events.last().map(|event| event.event_type.as_str()) != Some("run.completed") {
|
||||
return Err(Phase0Error::invalid(
|
||||
"the final event must be run.completed",
|
||||
));
|
||||
}
|
||||
if fixture.result.run_id != fixture.run.run_id {
|
||||
return Err(Phase0Error::invalid("result.runId must match run.runId"));
|
||||
}
|
||||
if fixture.result.status != "succeeded" {
|
||||
return Err(Phase0Error::invalid(
|
||||
"the Phase 0 fixture result must be succeeded",
|
||||
));
|
||||
}
|
||||
if fixture.result.summary.trim().is_empty() {
|
||||
return Err(Phase0Error::invalid(
|
||||
"result.summary must be a non-empty string",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(fixture)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MockControlPlane {
|
||||
running: bool,
|
||||
opened_run_id: Option<String>,
|
||||
events: Vec<NativeRunEvent>,
|
||||
result: Option<NativeRunResult>,
|
||||
}
|
||||
|
||||
impl MockControlPlane {
|
||||
pub fn start(&mut self) -> Result<(), Phase0Error> {
|
||||
if self.running {
|
||||
return Err(Phase0Error::invalid(
|
||||
"mock control plane is already running",
|
||||
));
|
||||
}
|
||||
self.running = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.running = false;
|
||||
}
|
||||
|
||||
pub fn open_run(&mut self, identity: &NativeRunIdentity) -> Result<(), Phase0Error> {
|
||||
self.require_running()?;
|
||||
if self.opened_run_id.is_some() {
|
||||
return Err(Phase0Error::invalid(
|
||||
"mock control plane accepts one Phase 0 run",
|
||||
));
|
||||
}
|
||||
self.opened_run_id = Some(identity.run_id.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn append_event(&mut self, event: &NativeRunEvent) -> Result<u64, Phase0Error> {
|
||||
let run_id = self.require_run_id()?;
|
||||
if event.run_id != run_id {
|
||||
return Err(Phase0Error::invalid(
|
||||
"event runId does not match the opened run",
|
||||
));
|
||||
}
|
||||
let expected_sequence = self.events.len() as u64 + 1;
|
||||
if event.sequence != expected_sequence {
|
||||
return Err(Phase0Error::invalid(format!(
|
||||
"event sequence must be {expected_sequence}"
|
||||
)));
|
||||
}
|
||||
self.events.push(event.clone());
|
||||
Ok(event.sequence)
|
||||
}
|
||||
|
||||
pub fn complete_run(&mut self, result: &NativeRunResult) -> Result<(), Phase0Error> {
|
||||
let run_id = self.require_run_id()?;
|
||||
if result.run_id != run_id {
|
||||
return Err(Phase0Error::invalid(
|
||||
"result runId does not match the opened run",
|
||||
));
|
||||
}
|
||||
if self.events.last().map(|event| event.event_type.as_str()) != Some("run.completed") {
|
||||
return Err(Phase0Error::invalid(
|
||||
"run.completed must be ingested before the result",
|
||||
));
|
||||
}
|
||||
if self.result.is_some() {
|
||||
return Err(Phase0Error::invalid(
|
||||
"mock control plane accepts one terminal result",
|
||||
));
|
||||
}
|
||||
self.result = Some(result.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_running(&self) -> Result<(), Phase0Error> {
|
||||
if self.running {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Phase0Error::invalid(
|
||||
"mock control plane must be started first",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_run_id(&self) -> Result<&str, Phase0Error> {
|
||||
self.require_running()?;
|
||||
self.opened_run_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| Phase0Error::invalid("a run must be opened first"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Phase0TracerOutput<'a> {
|
||||
schema_version: &'static str,
|
||||
run_identity: Phase0RunIdentityOutput<'a>,
|
||||
result: Phase0ResultOutput<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Phase0RunIdentityOutput<'a> {
|
||||
run_id: &'a str,
|
||||
session_id: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Phase0ResultOutput<'a> {
|
||||
status: &'a str,
|
||||
summary: &'a str,
|
||||
}
|
||||
|
||||
pub fn run_phase0_tracer(input: &str) -> Result<String, Phase0Error> {
|
||||
let fixture = validate_phase0_fixture(input)?;
|
||||
let mut mock_core = MockControlPlane::default();
|
||||
mock_core.start()?;
|
||||
|
||||
let replay_result = (|| {
|
||||
mock_core.open_run(&fixture.run)?;
|
||||
for event in &fixture.events {
|
||||
mock_core.append_event(event)?;
|
||||
}
|
||||
mock_core.complete_run(&fixture.result)?;
|
||||
serde_json::to_string(&Phase0TracerOutput {
|
||||
schema_version: PHASE0_OUTPUT_SCHEMA,
|
||||
run_identity: Phase0RunIdentityOutput {
|
||||
run_id: &fixture.run.run_id,
|
||||
session_id: &fixture.run.session_id,
|
||||
},
|
||||
result: Phase0ResultOutput {
|
||||
status: &fixture.result.status,
|
||||
summary: &fixture.result.summary,
|
||||
},
|
||||
})
|
||||
.map_err(|error| Phase0Error::invalid(format!("output serialization failed: {error}")))
|
||||
})();
|
||||
|
||||
mock_core.stop();
|
||||
replay_result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validates_the_shared_fixture() {
|
||||
let fixture =
|
||||
validate_phase0_fixture(PHASE0_FIXTURE).expect("shared fixture should validate");
|
||||
|
||||
assert_eq!(fixture.events.len(), 2);
|
||||
assert_eq!(fixture.events[1].sequence, 2);
|
||||
assert_eq!(fixture.result.status, "succeeded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_sequence_gap() {
|
||||
let invalid = PHASE0_FIXTURE.replacen(r#""sequence": 2"#, r#""sequence": 3"#, 1);
|
||||
|
||||
let error = validate_phase0_fixture(&invalid).expect_err("sequence gap must fail");
|
||||
assert_eq!(error.to_string(), "events[1].sequence must be 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_the_mock_core_path_with_stable_output() {
|
||||
assert_eq!(
|
||||
run_phase0_tracer(PHASE0_FIXTURE).expect("tracer should succeed"),
|
||||
PHASE0_EXPECTED_OUTPUT.trim_end(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ test("the package passes its standalone boundary", async () => {
|
||||
test("a negative fixture proves that a core import is rejected", async () => {
|
||||
const violations = await checkForbiddenImports({
|
||||
scanRoots: ["test-fixtures/forbidden-import"],
|
||||
cargoRoots: [],
|
||||
checkManifest: false,
|
||||
});
|
||||
|
||||
@@ -21,3 +22,16 @@ test("a negative fixture proves that a core import is rejected", async () => {
|
||||
assert.match(violations[0].reason, /may not escape/);
|
||||
assert.ok(violations[0].file.startsWith(defaultPackageRoot));
|
||||
});
|
||||
|
||||
test("a negative Cargo fixture proves that a core path dependency is rejected", async () => {
|
||||
const violations = await checkForbiddenImports({
|
||||
scanRoots: [],
|
||||
cargoRoots: ["test-fixtures/forbidden-cargo-path"],
|
||||
checkManifest: false,
|
||||
});
|
||||
|
||||
assert.equal(violations.length, 1);
|
||||
assert.equal(violations[0].specifier, "../../../../server");
|
||||
assert.match(violations[0].reason, /Cargo path dependencies may not escape/);
|
||||
assert.ok(violations[0].file.startsWith(defaultPackageRoot));
|
||||
});
|
||||
@@ -2,11 +2,13 @@ import { readFile, readdir } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".mjs", ".ts", ".tsx"]);
|
||||
const SOURCE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".mjs", ".rs", ".ts", ".tsx"]);
|
||||
const IMPORT_PATTERNS = [
|
||||
/\b(?:import|export)\s+(?:type\s+)?(?:[^"'`;]{0,500}?\s+from\s+)?["']([^"']+)["']/g,
|
||||
/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
/\brequire\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
/\b(?:include|include_str|include_bytes)!\s*\(\s*["']([^"']+)["']\s*\)/g,
|
||||
/#\[path\s*=\s*["']([^"']+)["']\]/g,
|
||||
];
|
||||
|
||||
const libraryDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -94,9 +96,53 @@ async function manifestViolations(packageRoot) {
|
||||
}));
|
||||
}
|
||||
|
||||
async function cargoManifestViolations(packageRoot, cargoRoots) {
|
||||
const violations = [];
|
||||
const manifests = [];
|
||||
|
||||
async function collectCargoManifests(target) {
|
||||
const entries = await readdir(target, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (["target", "node_modules", ".git"].includes(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
const path = resolve(target, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
await collectCargoManifests(path);
|
||||
} else if (entry.isFile() && entry.name === "Cargo.toml") {
|
||||
manifests.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of cargoRoots) {
|
||||
await collectCargoManifests(resolve(packageRoot, root));
|
||||
}
|
||||
|
||||
for (const manifest of manifests.sort()) {
|
||||
const source = await readFile(manifest, "utf8");
|
||||
const pathPattern = /\bpath\s*=\s*["']([^"']+)["']/g;
|
||||
for (let match = pathPattern.exec(source); match !== null; match = pathPattern.exec(source)) {
|
||||
const dependencyPath = resolve(dirname(manifest), match[1]);
|
||||
if (isInside(packageRoot, dependencyPath)) {
|
||||
continue;
|
||||
}
|
||||
violations.push({
|
||||
file: manifest,
|
||||
line: source.slice(0, match.index).split("\n").length,
|
||||
specifier: match[1],
|
||||
reason: "Cargo path dependencies may not escape packages/paperclip-runner",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export async function checkForbiddenImports({
|
||||
packageRoot = defaultPackageRoot,
|
||||
scanRoots = ["src", "scripts"],
|
||||
scanRoots = ["src", "scripts", "runner"],
|
||||
cargoRoots = ["runner"],
|
||||
checkManifest = true,
|
||||
} = {}) {
|
||||
const violations = [];
|
||||
@@ -124,6 +170,7 @@ export async function checkForbiddenImports({
|
||||
if (checkManifest) {
|
||||
violations.push(...(await manifestViolations(packageRoot)));
|
||||
}
|
||||
violations.push(...(await cargoManifestViolations(packageRoot, cargoRoots)));
|
||||
return violations;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,15 @@ Only Phase 0 and Phase 1 implementation issues may be created initially. After b
|
||||
2. **Standalone first:** new runtime behavior, documentation, tutorials, examples, journals, fixtures, and devtools live under `packages/paperclip-runner/`. The default development and test path uses a mock Paperclip core adapter.
|
||||
3. **Core is a port, not a dependency:** package code depends on a small `ControlPlanePort`/`NativeSessionBackend` contract. Mock implementations own tests and tutorials. Paperclip-specific integration implements that port later.
|
||||
4. **No hidden control-plane coupling:** standalone tests must not import `server/`, `ui/`, or production database modules. Contract tests may consume shared generated schemas or fixtures through explicit package exports.
|
||||
5. **Tracer bullet in every phase:** each phase ends with a useful executable path, even if it is intentionally narrow.
|
||||
6. **Evidence in every phase:** record the exact commands run, versions used, results, known gaps, and links to artifacts/screenshots.
|
||||
7. **Documentation in every phase:** add reference documentation plus a hand-run tutorial. Add each tutorial to a Native Runner tutorial index and to a cumulative end-to-end tutorial.
|
||||
8. **OKF engineering journal:** maintain a Google Open Knowledge Format journal using current OKF v0.2 conventions: Markdown files, YAML frontmatter, typed entries, timestamps, links, and index pages. Add a journal usage guide before implementation begins.
|
||||
9. **Human checkpoint in every phase:** the board/user can run the tutorial and accept or request changes before the next dependent phase starts.
|
||||
10. **Commit by phase:** create small, reviewable commits during each phase. Each phase ends with a named checkpoint commit after QA and human acceptance.
|
||||
11. **No invented UI primitives:** evaluate and selectively adapt shadcn/ui and Vercel AI Elements components. Keep Paperclip design tokens and accessibility requirements authoritative.
|
||||
12. **Production integration is additive:** legacy adapters and existing run finalization remain unchanged unless the separate integration phase explicitly proves a minimal feature-flagged bridge.
|
||||
5. **Rust production boundary:** the production runner direction is Rust. TypeScript may provide control-plane/client contracts and a test oracle, but each runner phase must establish or extend the Rust workspace and prove shared-fixture parity rather than deferring Rust implicitly.
|
||||
6. **Tracer bullet in every phase:** each phase ends with a useful executable path, even if it is intentionally narrow.
|
||||
7. **Evidence in every phase:** record the exact commands run, versions used, results, known gaps, and links to artifacts/screenshots.
|
||||
8. **Documentation in every phase:** add reference documentation plus a hand-run tutorial. Add each tutorial to a Native Runner tutorial index and to a cumulative end-to-end tutorial.
|
||||
9. **OKF engineering journal:** maintain a Google Open Knowledge Format journal using current OKF v0.2 conventions: Markdown files, YAML frontmatter, typed entries, timestamps, links, and index pages. Add a journal usage guide before implementation begins.
|
||||
10. **Human checkpoint in every phase:** the board/user can run the tutorial and accept or request changes before the next dependent phase starts.
|
||||
11. **Commit by phase:** create small, reviewable commits during each phase. Each phase ends with a named checkpoint commit after QA and human acceptance.
|
||||
12. **No invented UI primitives:** evaluate and selectively adapt shadcn/ui and Vercel AI Elements components. Keep Paperclip design tokens and accessibility requirements authoritative.
|
||||
13. **Production integration is additive:** legacy adapters and existing run finalization remain unchanged unless the separate integration phase explicitly proves a minimal feature-flagged bridge.
|
||||
|
||||
## Proposed package and documentation boundaries
|
||||
|
||||
@@ -29,7 +30,7 @@ Only Phase 0 and Phase 1 implementation issues may be created initially. After b
|
||||
packages/paperclip-runner/
|
||||
protocol/ language-neutral schemas and fixtures
|
||||
sdk/typescript/ TypeScript protocol/reducer/client SDK
|
||||
runner/ deterministic runner daemon and local state
|
||||
runner/ Rust workspace; runner-core first, daemon/local state later
|
||||
drivers/api/ harness driver contract
|
||||
drivers/fake/ scripted deterministic harness
|
||||
drivers/codex/ reference Codex app-server driver
|
||||
@@ -91,15 +92,17 @@ Every phase child issue uses the same completion checklist:
|
||||
**Deliverables:**
|
||||
|
||||
- package workspace skeleton and build/test commands;
|
||||
- Rust Cargo workspace with the initial `runner-core` crate and default Phase 0 tracer;
|
||||
- architecture boundary document with forbidden imports and dependency direction;
|
||||
- initial `ControlPlanePort`, `HarnessDriver`, and `NativeSessionBackend` interface sketches;
|
||||
- mock core adapter shell;
|
||||
- tutorial index and cumulative tutorial shell;
|
||||
- OKF v0.2 bundle, journal entry template, index pages, and “how to use the journal” instructions;
|
||||
- CI/static boundary check that rejects imports from production core packages;
|
||||
- CI/static boundary check that rejects TypeScript imports and Cargo path dependencies from production core packages;
|
||||
- shared-fixture parity check between the Rust tracer and TypeScript reference;
|
||||
- a dated compatibility note for shadcn/ui and AI Elements.
|
||||
|
||||
**Tests/evidence:** package build, fixture validation smoke, forbidden-import test, docs link check, OKF frontmatter/index validation.
|
||||
**Tests/evidence:** Rust and TypeScript package builds, shared-fixture validation smoke, byte-identical tracer output, forbidden-import/path-dependency tests, docs link check, and OKF frontmatter/index validation.
|
||||
|
||||
**Owners/review:** CodexCoder implementation; DevRel documentation review; SecurityEngineer boundary/secret review; CTO architecture approval; QA tutorial execution.
|
||||
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MockControlPlaneAdapter } from "../mock-core/mock-control-plane-adapter.js";
|
||||
import { runPhase0Tracer } from "./phase0-runner.js";
|
||||
|
||||
const EXPECTED_OUTPUT =
|
||||
'{"schemaVersion":"paperclip.runner.phase0.output.v1","runIdentity":{"runId":"run_phase0_0001","sessionId":"session_phase0_0001"},"result":{"status":"succeeded","summary":"Standalone Phase 0 fixture accepted."}}';
|
||||
const expectedOutputUrl = new URL(
|
||||
"../../protocol/fixtures/phase-00-expected-output.json",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
describe("Phase 0 tracer", () => {
|
||||
it("prints a stable identity and result and stops the mock core", async () => {
|
||||
const mockCore = new MockControlPlaneAdapter();
|
||||
const expectedOutput = (await readFile(expectedOutputUrl, "utf8")).trimEnd();
|
||||
|
||||
await expect(runPhase0Tracer(mockCore)).resolves.toBe(EXPECTED_OUTPUT);
|
||||
await expect(runPhase0Tracer(mockCore)).resolves.toBe(expectedOutput);
|
||||
expect(mockCore.snapshot().lifecycle).toBe("stopped");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "forbidden-paperclip-core-import"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
paperclip-server = { path = "../../../../server" }
|
||||
Reference in new issue
Block a user