Add RFCs for the remaining quality-proposal ideas
Eight proposals grouped by category, each with problem statement, concrete plan, and risks: property-based testing over the protocol-shaped core (chunk streams, event logs, schema DSL); mutation testing as the counterweight to the 100%-coverage gate; deterministic tests + a universal replay-invariant fixture + nightly race stress; architectural conformance (dependency-cruiser rules and the LlmAdapter conformance kit); runtime arg validation at the model boundary with a structured error taxonomy and dev-mode invariants; doc-sync enforcement (typechecked doc snippets, API reports); supply-chain checks and nightly vendor-drift verification against the manifest; and deep-readonly public surfaces (logged-vs-in-flight mutability boundary). AGENTS.md points at docs/adr and docs/rfc.
This commit is contained in:
@@ -29,7 +29,9 @@ packages/ Harness packages, all named @deepseek-ai/dsh-<name>:
|
||||
agent-loop/ THE concrete plugin: LoopAgent + the loop driver
|
||||
examples/ Runnable demos (not workspaces). echo-agent = mock model + echo
|
||||
tool + stdio UI + JSONL persistence, wired via cordis.yml.
|
||||
docs/ architecture.md — the design doc.
|
||||
docs/ architecture.md — the design doc. adr/ — decision records (the
|
||||
why behind vendoring, event-sourcing, the schema DSL, …).
|
||||
rfc/ — proposals for substantial future work.
|
||||
scripts/ build.ts — dumble JS bundling for all packages.
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# RFC 001: Property-based testing for protocol-shaped code
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
Example-based tests pin the cases we thought of. The harness's core is
|
||||
protocol-shaped — chunk streams, event logs, schema conversion — where the
|
||||
input space is combinatorial and the interesting bugs live in interleavings
|
||||
nobody wrote an example for (the `streamBlocks` ordering bug survived 100%
|
||||
line coverage of the happy paths).
|
||||
|
||||
## Proposal
|
||||
|
||||
Adopt fast-check (vitest integration) with generators for our vocabulary:
|
||||
|
||||
- **BlockAssembler**: arbitrary chunk sequences (valid and malformed —
|
||||
duplicate indices, stragglers after block-end, missing block-start).
|
||||
Invariants: `flushReady() + flushRemaining() ≡ blocks()` in order;
|
||||
`streamBlocks ≡ generate().message.content`; memory bounded (partials map
|
||||
size ≤ distinct indices); idempotent re-assembly.
|
||||
- **Session**: arbitrary event logs (seeded generators over SessionEventMap).
|
||||
Invariants: `deriveMessages` deterministic; replay-from-seed produces
|
||||
identical derivation; seq strictly monotonic; derived history unaffected by
|
||||
non-message events.
|
||||
- **Schema DSL**: arbitrary SchemaSpecs. Invariants: generated JSON Schema's
|
||||
`required` array equals the `required: true` keys at every nesting level;
|
||||
conversion is total (never throws); generated args satisfying `InferArgs`
|
||||
validate against the generated schema (once RFC 005's validator exists —
|
||||
the two RFCs compose).
|
||||
- **Inbox/loop**: arbitrary send/steer/abort schedules against a scripted
|
||||
adapter. Invariants: no message lost (every send/steer appears in the log
|
||||
exactly once), turn numbers strictly increase, status transitions follow
|
||||
idle→running→idle/disposed.
|
||||
|
||||
## Plan
|
||||
|
||||
One `tests/properties.spec.ts` per package; fast-check as devDependency;
|
||||
numRuns tuned so the suite stays under ~10s locally, with a nightly CI job
|
||||
running 100× the iterations. Failures persist their seed in the report so
|
||||
agents can reproduce deterministically.
|
||||
|
||||
## Risks
|
||||
|
||||
Generator quality determines value — invest in generators that produce
|
||||
*realistic-but-adversarial* streams, not uniform noise. Property flake from
|
||||
timeouts must be treated as a finding, not retried away.
|
||||
@@ -0,0 +1,38 @@
|
||||
# RFC 002: Mutation testing as the coverage counterweight
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The per-file 100% coverage gate (ADR 0007) proves every line *executes* under
|
||||
test — not that any assertion would notice if the line were wrong. Under
|
||||
agent-written tests, coverage pressure can produce execution-without-assertion.
|
||||
Mutation testing measures what coverage cannot: whether the suite *kills*
|
||||
deliberately injected bugs.
|
||||
|
||||
## Proposal
|
||||
|
||||
Stryker (`@stryker-mutator/vitest-runner`) over `packages/*/src`:
|
||||
|
||||
- **PR-scoped incremental runs** (changed files only) as a CI job — fast
|
||||
enough to gate merges once tuned.
|
||||
- **Nightly full runs** with a tracked mutation score; start by recording,
|
||||
then set the threshold at the observed baseline and ratchet upward (same
|
||||
policy as coverage: thresholds only ever tighten).
|
||||
- Surviving mutants are work items: an agent picks a survivor, writes the
|
||||
killing test, repeats — a well-shaped autonomous loop.
|
||||
- Equivalent mutants (provably behavior-preserving) get annotated exclusions
|
||||
with reasons, mirroring the `/* v8 ignore */` policy.
|
||||
|
||||
## Plan
|
||||
|
||||
1. Add Stryker config scoped to one package (llm — smallest, most algorithmic)
|
||||
and measure runtime.
|
||||
2. Expand to all packages; record baseline scores in the config.
|
||||
3. Wire the nightly job; add the incremental PR job once runtime is acceptable.
|
||||
|
||||
## Risks
|
||||
|
||||
Runtime: mutation testing is expensive; per-file 100% coverage helps (every
|
||||
mutant is at least reached). If PR-scoped runs stay too slow, keep them
|
||||
nightly-only and rely on the score ratchet.
|
||||
@@ -0,0 +1,40 @@
|
||||
# RFC 003: Deterministic tests, the replay invariant fixture, and race stress
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
Several loop tests synchronize with `setTimeout(30)` sleeps — flakiness debt
|
||||
that wastes agent cycles on retries and can mask ordering bugs. Separately,
|
||||
our core architectural promise (any session log replays to identical derived
|
||||
history) is asserted in two tests but is cheap to assert *everywhere*. And
|
||||
the inbox wakeup race was verified by hand exactly once; nothing re-verifies
|
||||
it continuously.
|
||||
|
||||
## Proposal
|
||||
|
||||
Three measures:
|
||||
|
||||
1. **No wall-clock sleeps in tests.** Replace `setTimeout(N)` waits with
|
||||
event-driven waits (the existing `waitForIdle` pattern, extended to
|
||||
`waitForStatus`, `waitForEvent(n)`) or vitest fake timers where time
|
||||
itself is under test. Enforce with a lint rule banning `setTimeout` in
|
||||
`packages/*/tests` outside an allowlisted helper module.
|
||||
2. **Universal replay fixture.** A shared test helper wraps the loop harness
|
||||
so that after every test, the agent's session log is replayed into a fresh
|
||||
Session and `deriveMessages()` equality is asserted automatically. The
|
||||
invariant then gets checked hundreds of times per CI run across every
|
||||
scenario the suite produces, not twice.
|
||||
3. **Nightly race stress.** A CI job running the agent-loop and inbox suites
|
||||
with `vitest --repeat=200` (and `--shuffle`) to flush scheduling-dependent
|
||||
failures; any flake found is a bug to fix, never a retry.
|
||||
|
||||
## Plan
|
||||
|
||||
Land 1 and 2 together (they touch the same helpers); add the nightly job
|
||||
after the suite is sleep-free so repeats are fast.
|
||||
|
||||
## Risks
|
||||
|
||||
Fake timers interact subtly with Promise scheduling in the loop — prefer
|
||||
event-driven waits; reserve fake timers for timer-service behavior itself.
|
||||
@@ -0,0 +1,43 @@
|
||||
# RFC 004: Architectural conformance — dependency rules and the adapter kit
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
Two architectural guarantees currently live only in prose: (1) nothing
|
||||
depends on the concrete loop package (the microkernel promise, ADR 0002), and
|
||||
(2) every LlmAdapter speaks the chunk protocol correctly. Both should be
|
||||
mechanical (ADR 0007).
|
||||
|
||||
## Proposal
|
||||
|
||||
**dependency-cruiser** with rules:
|
||||
|
||||
- `packages/*` (except agent-loop's own tests and examples/) must not import
|
||||
`@deepseek-ai/dsh-agent-loop`.
|
||||
- No cross-package deep imports (`@deepseek-ai/dsh-*/src/...` paths) — public
|
||||
entry points only.
|
||||
- No import cycles anywhere in packages/.
|
||||
- `vendor/*` must not import from `packages/*`.
|
||||
- Layering: dsh-llm imports nothing from other dsh packages; dsh-session only
|
||||
dsh-llm; etc. (the dependency table in packages/README.md, enforced).
|
||||
|
||||
**Adapter conformance kit** in dsh-llm (`@deepseek-ai/dsh-llm/conformance`):
|
||||
a reusable vitest suite parameterized by an adapter factory, asserting the
|
||||
chunk-protocol contract — index monotonicity per block, no deltas after
|
||||
`block-end` for an index, exactly one `finish`, usage at most once, every
|
||||
`tool-call-delta` carries the call id, abort honored promptly. Run it against
|
||||
the mocks now; the DeepSeek V4 adapter inherits it on day one. Optionally a
|
||||
dev-mode `strictAdapter()` wrapper enforcing the same at runtime behind a
|
||||
debug flag (pairs with RFC 005's invariants).
|
||||
|
||||
## Plan
|
||||
|
||||
dependency-cruiser config + CI step first (an hour of work, permanent
|
||||
guarantee); the conformance kit lands with its first consumer test against
|
||||
MockAdapter, and is a prerequisite for the V4 adapter phase.
|
||||
|
||||
## Risks
|
||||
|
||||
Dep-cruiser rule maintenance as packages are added — keep rules pattern-based
|
||||
(`dsh-*`) rather than enumerated.
|
||||
@@ -0,0 +1,49 @@
|
||||
# RFC 005: Runtime validation at the model boundary, error taxonomy, dev-mode invariants
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
Three gaps where compile-time guarantees stop:
|
||||
|
||||
1. Tool args are model-generated JSON — `defineTool`'s `InferArgs<S>` claim
|
||||
is only as true as the model's output. Today a malformed call reaches
|
||||
`execute` untyped-in-practice.
|
||||
2. Tool errors flatten to a text block; name/code/stack are lost, so future
|
||||
sandbox/retry plugins can't distinguish ENOENT from EACCES, and the model
|
||||
gets less actionable feedback than it could.
|
||||
3. Loop ordering invariants (seq monotonicity, step/turn event nesting,
|
||||
turn-number continuity) are asserted only where tests look.
|
||||
|
||||
## Proposal
|
||||
|
||||
1. **Schema validation in defineTool**: before `execute`, validate parsed
|
||||
args against the SchemaSpec (the converter already encodes the structure —
|
||||
a small interpreter walks it: presence of required keys, primitive type
|
||||
checks, enum membership, recursion into objects/arrays). On mismatch,
|
||||
return an `isError` ToolExecutionResult describing the violation — the
|
||||
model can self-correct. Raw-registered tools (MCP) keep validating their
|
||||
own input.
|
||||
2. **Structured error taxonomy**: per-package error classes extending a
|
||||
common `HarnessError` (name, `code`, `cause` chaining).
|
||||
`ToolExecutionResult` gains optional `error: { name, code }` alongside the
|
||||
model-facing text. The loop's `errorData` consumes it; session `error`
|
||||
events carry the code. This also properly fixes the non-Error-throw
|
||||
message degradation found in review.
|
||||
3. **Dev-mode invariants**: a `dsh-invariants` debug plugin (everything is a
|
||||
plugin — it's just listeners) asserting, when enabled: session seq strictly
|
||||
increases; `step/start` precedes its chunks; `turn/start`/`turn/end` pair
|
||||
and nest; tool/call has a matching tool/result; status transitions are
|
||||
legal. Enabled in tests and the demo; off in production. Doubles as
|
||||
executable documentation of the event contract.
|
||||
|
||||
## Plan
|
||||
|
||||
2 first (taxonomy is a dependency of 1's error shape), then 1, then 3.
|
||||
Property tests (RFC 001) then close the loop: generated args ↔ validator ↔
|
||||
InferArgs agreement.
|
||||
|
||||
## Risks
|
||||
|
||||
Validator/InferArgs drift — covered by the RFC 001 composition property.
|
||||
Validation cost per call is negligible next to a model call.
|
||||
@@ -0,0 +1,39 @@
|
||||
# RFC 006: Doc-sync enforcement and API reports
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
AGENTS.md policy says docs and code must stay strictly in sync, but sync is
|
||||
verified by eyeball. Review has already caught drift twice (a cookbook
|
||||
example contradicting the type policy; a README citing the wrong
|
||||
registerAdapter call). Public API changes are similarly invisible — nothing
|
||||
makes "this commit changed the public surface" an explicit, reviewable fact.
|
||||
|
||||
## Proposal
|
||||
|
||||
1. **Typecheck documentation code blocks.** A script extracts fenced ```ts
|
||||
blocks from README.md / docs/architecture.md / packages/*/README.md into a
|
||||
temp project resolving workspace packages, and runs tsc. Blocks that are
|
||||
intentionally elided get an explicit `ts ignore-check` info string —
|
||||
opt-out is visible in the source. (twoslash is the fancier alternative;
|
||||
start with plain extraction.)
|
||||
2. **Generate or verify the event-taxonomy table.** The table in
|
||||
docs/architecture.md duplicates the `Events` declarations. Either generate
|
||||
it from source (ts-morph walk over the `declare module 'cordis'` blocks)
|
||||
or CI-assert that every declared event name appears in the table and vice
|
||||
versa.
|
||||
3. **API reports.** api-extractor (or `tsc --emitDeclarationOnly` + a
|
||||
normalized public-surface dump) producing a checked-in `etc/<pkg>.api.md`
|
||||
per package; CI fails if regeneration differs. Every public-API change
|
||||
becomes a diff line a reviewer (or review agent) must see.
|
||||
|
||||
## Plan
|
||||
|
||||
1 is a standalone script + CI step. 3 next (it also documents the surface for
|
||||
plugin authors). 2 last — verify-don't-generate is likely sufficient.
|
||||
|
||||
## Risks
|
||||
|
||||
Doc blocks often show fragments; the ignore-check escape hatch must stay rare
|
||||
or the gate is theater — lint the ratio if needed.
|
||||
@@ -0,0 +1,41 @@
|
||||
# RFC 007: Supply chain checks and vendor drift verification
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The vendor manifest (ADR 0001) is enforced at commit time in the *forward*
|
||||
direction (vendored change ⇒ manifest update) but nothing verifies the
|
||||
manifest's *claims*: that vendor/ actually equals upstream-at-SHA plus
|
||||
exactly the logged modifications. And the handful of true npm dependencies
|
||||
have no advisory monitoring or update cadence.
|
||||
|
||||
## Proposal
|
||||
|
||||
1. **Vendor drift check** (nightly CI): clone the upstream repos at the
|
||||
manifest SHAs (shallow), copy the corresponding package sources, and diff
|
||||
against `vendor/*/src`. The job fails unless the diff matches the logged
|
||||
local modifications (kept as a checked-in patch file per modification —
|
||||
the log entries become verifiable artifacts rather than prose).
|
||||
2. **Dependency advisories**: osv-scanner (or `yarn npm audit`) job on the
|
||||
lockfile, scheduled + on lockfile-touching PRs.
|
||||
3. **License inventory**: a script asserting every vendored package carries
|
||||
its LICENSE and that package.json `license` fields match the inventory in
|
||||
vendor/README.md (we mix vendored MIT with our BSD-3) — CI step.
|
||||
4. **Renovate** (or a scheduled agent task) proposing npm dependency updates
|
||||
in small PRs that ride the full gate suite; vendored packages are excluded
|
||||
(their updates follow the manifest sync procedure, ideally as a
|
||||
semi-automated agent workflow: fetch upstream, re-apply patches, run
|
||||
gates, open PR with the manifest table updated).
|
||||
|
||||
## Plan
|
||||
|
||||
3 is trivial — do first. 1 requires network access from CI to the upstream
|
||||
repos (private — needs a token) and converting the two existing logged
|
||||
modifications into patch files. 2 and 4 are config.
|
||||
|
||||
## Risks
|
||||
|
||||
Upstream repos are private mirrors; CI credentials and availability are the
|
||||
main friction for the drift check. If blocked, run it as a local scheduled
|
||||
agent task instead of CI.
|
||||
@@ -0,0 +1,44 @@
|
||||
# RFC 008: Deep-readonly public surfaces
|
||||
|
||||
Status: proposed
|
||||
|
||||
## Problem
|
||||
|
||||
The session log is append-only by contract, but `session.events` returns
|
||||
`readonly SessionEvent[]` whose *elements* are mutable: a plugin can reach in
|
||||
and rewrite history (`events[0].data.content.push(...)`), silently breaking
|
||||
replay equivalence and the derived-history guarantee. The same applies to
|
||||
derived messages and prompt assemblies passed through waterfalls — mutation
|
||||
is sometimes the intended idiom (waterfall middleware mutates the request)
|
||||
and sometimes corruption (mutating a *logged* event), and the types don't
|
||||
distinguish.
|
||||
|
||||
## Proposal
|
||||
|
||||
Make immutability part of the type where mutation is corruption:
|
||||
|
||||
- `SessionEvent` data becomes `DeepReadonly` on the way OUT of a session
|
||||
(`events`, `session/event` listeners); `append()` keeps taking plain
|
||||
mutable input. A `DeepReadonly<T>` utility type lands in dsh-llm next to
|
||||
the brand/never helpers.
|
||||
- `deriveMessages()` returns deep-readonly messages; the loop clones before
|
||||
handing a mutable request to the `agent/request` waterfall (mutation there
|
||||
is sanctioned — the clone makes the boundary explicit and cheap, once per
|
||||
step).
|
||||
- `PromptAssembly` stays mutable through its waterfall (sanctioned) but the
|
||||
registry's internal section list is cloned per assembly (already true).
|
||||
- Optionally, dev-mode `Object.freeze` of event data behind the RFC 005
|
||||
invariants flag, so sanctioned-mutation violations throw in tests rather
|
||||
than corrupting silently.
|
||||
|
||||
## Plan
|
||||
|
||||
Introduce `DeepReadonly`, flip the session read paths, fix resulting
|
||||
compile errors in consumers (expected: a handful in tests), add the
|
||||
freeze-in-dev option alongside RFC 005's invariants plugin.
|
||||
|
||||
## Risks
|
||||
|
||||
`DeepReadonly` types can produce noisy errors at waterfall boundaries where
|
||||
mutation IS the API — keep the mutable/readonly boundary exactly at "logged
|
||||
vs in-flight" and document it in the session README.
|
||||
@@ -0,0 +1,17 @@
|
||||
# RFCs
|
||||
|
||||
Proposals for substantial future work — reviewed before implementation,
|
||||
unlike ADRs (which record decisions already made). Each RFC groups a related
|
||||
set of ideas from the quality/robustness proposal (2026-06-11); statuses
|
||||
move proposed → accepted → implemented (then usually graduate to an ADR).
|
||||
|
||||
| # | Title | Status |
|
||||
|---|---|---|
|
||||
| [001](001-property-based-testing.md) | Property-based testing for protocol-shaped code | proposed |
|
||||
| [002](002-mutation-testing.md) | Mutation testing as the coverage counterweight | proposed |
|
||||
| [003](003-deterministic-and-stress-testing.md) | Deterministic tests + replay invariant fixture + race stress | proposed |
|
||||
| [004](004-architectural-conformance.md) | Architectural rules: dependency-cruiser, adapter conformance kit | proposed |
|
||||
| [005](005-runtime-validation-and-error-taxonomy.md) | Runtime arg validation, structured error taxonomy, dev-mode invariants | proposed |
|
||||
| [006](006-doc-sync-and-api-reports.md) | Doc-sync enforcement and API extractor reports | proposed |
|
||||
| [007](007-supply-chain-and-vendor-drift.md) | Supply chain checks and vendor drift verification | proposed |
|
||||
| [008](008-immutable-public-surfaces.md) | Deep-readonly public surfaces | proposed |
|
||||
Reference in New Issue
Block a user