From 8fd8f86942299fcf905066183068278363d6c785 Mon Sep 17 00:00:00 2001 From: dtzp555 Date: Mon, 25 May 2026 07:13:14 +1000 Subject: [PATCH] =?UTF-8?q?release(phase-1-cleanup):=20v0.1.1=20=E2=80=94?= =?UTF-8?q?=20pre-Phase-2=20batch=20(D35-D42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 cleanup release per CLAUDE.md release_kit.phase_rolling_mode policy. Closes 16 of 17 pre-Phase-2 GitHub issues; #16 stays OPEN as v1.x tracker with design ratified in ADR 0005 Amendment 8. Bumps package.json from 0.1.0 → 0.1.1. Promotes CHANGELOG "Unreleased" section to "## v0.1.1 — 2026-05-25" with full D35-D42 entries (backfilled D35/D36/D37 entries per cold-audit round 7 Finding 1) and final cleanup batch summary covering all 8 D-day commits that landed on `main` between 2026-05-24 and 2026-05-25. Tag v0.1.1 will be pushed after this commit lands and CI is green. release.yml auto-creates the GitHub Release from the CHANGELOG v0.1.1 section (extracted via awk pattern match on `## v0.1.1` heading). D37's phase_rolling_mode gate now passes because Unreleased is sentinel-only after this commit. **What v0.1.1 delivers (D35-D42):** - D35 — streaming empty-stream headers (#9) + truncation marker (#10) + irVersion strict check (#11) + alignment.yml scripts/** removal (#12) + X-OLP-Latency-Ms uniform audit (#4) - D36 — cache_control partial-noop log (#2) + ADR 0002 filename correction (#5) + mistral A5 flip (#6) + /v1/models alias governance (#13) + cache_control determinism test (#14) + anthropic v2.1.89 transcript artifact (#15) - D37 — release.yml phase_rolling_mode CI gate (#17) - D38 — maxConcurrent runtime enforcement + new CONCURRENCY_LIMIT hard trigger (#1) - D39 — CacheStore.delete + cache_evicted_truncated log + sticky- cache regression test + SPAWN_TIMEOUT asymmetry ADR (#3) - D40 — X-OLP-Fallback-Detail debug header + per-hop tuple collection + 4KB cap + RFC 7230 non-ASCII escape (#7) - D41 — X-OLP-Provider-Used chain-origin semantics documented (#8) - D42 — streaming singleflight v1.x design ADR + multi-layer safeguards (issue #16; STAYS OPEN as v1.x tracker) **Test growth:** 416 (v0.1.0) → 468 (v0.1.1). +52 tests across the 8 D-day batch. All 468 pass at the release-commit head. **Cold-audit cycle:** 1 final round (round 7) over D35-D42 + v0.1.0 state. Result: PASS_WITH_MINOR with 4 findings (1×P3 + 3×P4). - Finding 1 (P3 CHANGELOG missing D35/D36/D37) — FIXED in this commit via backfill. - Finding 2 (P4 ADR 0005 Amendment 8 "Amendment 1/3/5" notation ambiguity) — FIXED in this commit (replaced with explicit § Decision body item-1 + Amendment 3 + Amendment 5 wording). - Finding 3 (P4 ADR 0005 Amendment 8 Context paragraph cites line 811 but actual branch is at lines 817-823) — FIXED in this commit (Context paragraph now points at lines 817-823 with the TODO anchor at ~810 as the navigable landmark). - Finding 4 (P4 ADR 0002 missing Amendment 2 — pre-existing gap) — FIXED in this commit (added explanatory note under the Amendments heading explaining the gap is intentional and load- bearing). No P1 or P2 findings. **Phase 1 cleanup release_kit checklist** (per CLAUDE.md): - [x] All 8 D-day deliverables landed on main (D35-D42) - [x] CI green on every D-day commit + on this release commit's head - [x] Cold-audit round 7 PASS_WITH_MINOR (0 P1/P2; 4 findings all fixed in this commit) - [x] 16 of 17 pre-Phase-2 GitHub issues closed (#1-#15 and #17); #16 stays OPEN as v1.x tracker with design ratified - [x] Issue #16 status comment posted (D42 commit b0c080d) referencing ADR 0005 Amendment 8 design ratification - [x] CHANGELOG "Unreleased" promoted to "## v0.1.1 — 2026-05-25" with D35-D42 entries (including backfilled D35/D36/D37) - [x] package.json bumped from 0.1.0 → 0.1.1 - [x] docs/v1x-roadmap.md created at D42 — 7 deferred items with code anchors + GitHub-issue cross-refs + concrete start triggers - [x] Editorial findings 2/3/4 from round 7 folded into this commit - [ ] Tag pushed (next step in this PR's lifecycle) - [ ] release.yml triggered + GitHub Release created (auto on tag push; D37 phase_rolling_mode gate will pass because Unreleased is now sentinel-only — gate dry-run verified locally) **Known limitations carried to v1.x:** full list with code anchors + start triggers in docs/v1x-roadmap.md. The 7 items are: 1. Streaming-path singleflight (issue #16, ADR 0005 Amendment 8) 2. Multi-key auth (lib/keys.mjs) 3. Soft-trigger reactivation (ADR 0004 Amendment 2) 4. /health activeSpawns integration (ADR 0002 Amendment 6) 5. Provider-level cacheKeyFields mask (ADR 0005 Amendment 7) 6. Streaming-path SPAWN_FAILED salvage 7. D40 AUTH_MISSING tuple test coverage Authority: - All 8 D-day commit messages (D35-D42 on main) - ADR 0002 Amendments 5+6 + Amendment-2-gap note (D36, D38, this commit's editorial fix) - ADR 0004 Amendments 3+4+5+6 (D34, D38, D40, D41) - ADR 0005 Amendments 6+7+8 (D34, D42) + this commit's Amendment 8 editorial fixes - ALIGNMENT.md controlled-deviations subsection (D36 #13) - docs/v1x-roadmap.md (D42 — new file, this commit's checklist references it) - docs/provider-audits/anthropic.md (D36 #15 — new file) - CC 开发铁律 v1.6 § 10.x — every D-day had an independent fresh-context opus reviewer per Iron Rule 10; round 7 cold audit was a separate fresh-context full-pass auditor - CLAUDE.md release_kit_overlay phase_rolling_mode — this is the Phase 1 cleanup release that the policy was written for Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 62 +++++++++++++++++++++++++++ docs/adr/0002-plugin-architecture.md | 2 + docs/adr/0005-cache-cross-provider.md | 4 +- package.json | 2 +- 4 files changed, 67 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb8655..f596423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,48 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this ## Unreleased +(empty — Phase 2 entries land here once Phase 2 opens) + +## v0.1.1 — 2026-05-25 + +### Phase 1 cleanup — pre-Phase-2 batch (D35–D42, closes 16 of 17 issues) + +**Overview.** v0.1.1 closes the post-v0.1.0 cleanup batch covering all 17 pre-Phase-2 issues raised during the 6-round cold-audit cycle on the Phase 1 deliverable. 8 D-day commits (D35–D42) shipped between 2026-05-24 and 2026-05-25. 16 issues closed; issue #16 (streaming singleflight) stays OPEN as the v1.x tracker with its design ratified in ADR 0005 Amendment 8. + +**Test count: 416 (v0.1.0) → 468 (v0.1.1).** +52 tests across the cleanup batch. + +### D35 — pre-Phase-2 batch #1 (issues #4 #9 #10 #11 #12) + +- **#4 — X-OLP-Latency-Ms uniform.** Audit confirmed already-correct via D32; D35 adds the `#4-audit` regression test pinning the 5-header invariant on the 503 no-provider sendError so future drift is caught immediately. +- **#9 — Streaming empty-then-clean-exit headers.** Zero-chunk streaming path now guards `!res.headersSent` and emits Content-Type=text/event-stream, Cache-Control=no-cache, Connection=keep-alive, X-Accel-Buffering=no, plus all 5 X-OLP-* headers via olpHeaders before writing `SSE_DONE`. Zero-chunk path correctly does NOT cache. +- **#10 — Streaming post-first-chunk error truncation marker.** Two sibling fixes: catch-block-firstChunkEmitted=true and error-chunk-after-first-chunk both now emit synthetic `{type:'stop', finish_reason:'length'}` via `irChunkToOpenAISSE` + `SSE_DONE` + `res.end()`. Per ADR 0004 § Fallback safety: post-first-chunk truncation surfaces as `length` finish, never a hang. +- **#11 — `validateIRRequest` irVersion strict check.** ADR 0003 IR contract pins irVersion to `'1.0'`. Validator now: `obj.irVersion !== undefined && obj.irVersion !== '1.0'` → rejection. Strict string match — `undefined` accepted (back-compat), `'1.0'` accepted, `'2.0'` rejected, numeric `1.0` rejected (`1.0 !== '1.0'`). +- **#12 — `alignment.yml` scripts/** trigger removal.** Removed from both `push.paths` and `pull_request.paths` since the `scripts/` directory does not currently exist (planned for Phase 7). +- **Test count:** 416 → 424 (+8). + +### D36 — pre-Phase-2 batch #2 (issues #2 #5 #6 #13 #14 #15) + +- **#2 — cache_control partial-noop debug log.** `server.mjs handleChatCompletions` fires `logEvent('debug', 'cache_control_partial_noop', { chain, marker_count })` at most once per request when markers present AND chain has at least one non-Anthropic hop. Per ADR 0005 § D2. +- **#5 — ADR 0002 vibe.mjs → mistral.mjs.** § Decision filesystem layout corrected to match the shipped file naming convention (file named after provider key, not CLI binary). Amendment 5 documents the correction + makes the convention statement explicit for future contributors. +- **#6 — mistral.mjs A5 flip + ALIGNMENT.md table update.** Header A5 (model flag) flipped from `UNPINNED-D-later-verifies` to `CONFIRMED-NOT-APPLICABLE` with DeepWiki citation; ALIGNMENT.md Speculative-Candidate table mistral row updated to remove A5. +- **#13 — /v1/models alias governance.** ALIGNMENT.md gains "Controlled deviations (entry-surface scope)" subsection documenting the alias surface as a controlled Rule 2(b) deviation; `docs/openai-spec-pin.md` gains the alias-surfacing subsection with full 4-field contract table. +- **#14 — cache_control slot determinism regression test.** 4 tests in test-features.mjs construct hand-built IRs with synthetic markers (bypassing openAIToIR which strips them at v0.1) and verify the cache key SHA-256 is deterministic. Per ALIGNMENT.md Rule 2 (No Invention), no `sortMarkers` helper shipped — the slot is dead-code at v0.1. +- **#15 — Anthropic v2.1.89 transcript artifact.** New file `docs/provider-audits/anthropic.md` as a single living version-capture artifact. Records observed `claude --version` (2.1.132 at capture date 2026-05-24), pinned version (v2.1.89 from D4), drift note, sample invocation, flag-surface table for 5 OLP-consumed flags. Closes the circular ALIGNMENT.md ↔ plugin header citation by anchoring on an external artifact. +- **Test count:** 424 → 431 (+7). + +### D37 — release.yml phase_rolling_mode gate (issue #17) + +- **CI gate enforcing phase_rolling_mode promotion discipline.** New "Enforce phase_rolling_mode (Unreleased must be promoted)" step in `release.yml` between the version-match check and the CHANGELOG extraction step. Awk extracts content between `## Unreleased` and the next `## ` heading; sed strips blank lines and parenthetical-sentinel-only lines. Non-trivial remaining content fails the workflow with `::error::` instructing the maintainer to promote Unreleased → `## v` per CLAUDE.md release_kit.phase_rolling_mode. +- **Dry-run validated against 4 cases:** current sentinel-only Unreleased → PASS; synthetic non-trivial Unreleased → FIRES with offending lines reported; no Unreleased section → PASS; multi-sentinel + blank lines → PASS. +- **Gate is purely additive** — fires only on tag push to `v*.*.*`, does not affect normal push/PR CI. +- **Test count:** 431 → 431 (no test change — CI workflow only). + ### D38 — maxConcurrent runtime enforcement (issue #1) - **Spawn lifecycle gate** — `hints.maxConcurrent` is now enforced at runtime per ADR 0002 Amendment 6: `lib/providers/index.mjs` exports a per-provider `tryAcquireSpawn` / `releaseSpawn` / `getActiveSpawnCount` semaphore; `server.mjs` gates both the buffered and streaming spawn call sites in `handleChatCompletions` with a try/finally release. Saturation surfaces as `ProviderError(CONCURRENCY_LIMIT)` which the fallback engine treats as a hard trigger (ADR 0004 Amendment 4) — the chain advances to the next hop instead of queueing. If the entire chain is saturated, the user receives a chain-exhausted error via the existing exhaustion path. Closes #1. Queue+timeout deferred (see ADR 0002 Amendment 6 § Design choice). Test count 431 → 447. +- **Spawn lifecycle gate** — `hints.maxConcurrent` is now enforced at runtime per ADR 0002 Amendment 6: `lib/providers/index.mjs` exports a per-provider `tryAcquireSpawn` / `releaseSpawn` / `getActiveSpawnCount` semaphore; `server.mjs` gates both the buffered and streaming spawn call sites in `handleChatCompletions` with a try/finally release. Saturation surfaces as `ProviderError(CONCURRENCY_LIMIT)` which the fallback engine treats as a hard trigger (ADR 0004 Amendment 4) — the chain advances to the next hop instead of queueing. If the entire chain is saturated, the user receives a chain-exhausted error via the existing exhaustion path. Closes #1. Queue+timeout deferred (see ADR 0002 Amendment 6 § Design choice). Test count 431 → 447. + ### D39 — D16 follow-ups (issue #3): explicit cache delete + eviction log + SPAWN_TIMEOUT asymmetry doc - **Part 1 — `CacheStore.delete(keyId, cacheKey)`** — adds an explicit eviction primitive to `lib/cache/store.mjs`. Returns `boolean` (true if entry present and removed; false otherwise) and removes empty per-keyId namespace `Map` entries from the outer store for memory hygiene (matches the D38 `_activeSpawns` pattern). `server.mjs` D16 salvage path replaces `cacheStore.set(..., ttlMs=0)` (lazy tombstone that lived in the namespace `Map` until the next `get`/`peek` purged it) with `cacheStore.delete(...)` (immediate removal). Cache semantics unchanged — truncated responses still don't persist. ADR 0005 § "Cache write conditions" item 1 authority. @@ -45,6 +83,30 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this - **Authority:** ADR 0005 Amendment 8 (this commit); ADR 0005 Amendment 6 (D34 — original deferral note); GitHub issue #16 (round-6 F13 — sibling TOCTOU); ADR 0002 Amendment 6 (D38 — `tryAcquireSpawn` semantics that §7 coordination builds on); ADR 0004 Amendment 5 (D40 — observability pattern §11 extends); `CLAUDE.md` release_kit_overlay phase_rolling_mode — under Unreleased; CC 开发铁律 v1.6 § 10.x (design-only amendment; fresh-context reviewer not required per the Iron Rule 10 implementation-phase scope, documented in the amendment's procedural mechanism). - **Test count:** 468 → 468 (no test change — design-only). +### Phase 1 cleanup release_kit checklist + +- [x] All 8 D-day deliverables landed on main (D35-D42) +- [x] CI green on every D-day commit + on this release commit's head +- [x] Cold-audit round 7 (fresh-context opus full-pass) — PASS_WITH_MINOR, 0 P1/P2 findings +- [x] 16 of 17 pre-Phase-2 GitHub issues closed (#1-#15 and #17); #16 stays OPEN as v1.x tracker +- [x] Issue #16 status comment posted referencing ADR 0005 Amendment 8 design ratification +- [x] CHANGELOG "Unreleased" promoted to "## v0.1.1 — 2026-05-25" with D35-D42 entries +- [x] `package.json` bumped from 0.1.0 → 0.1.1 +- [x] `docs/v1x-roadmap.md` created — 7 deferred items with anchors + start triggers +- [ ] Tag pushed (next step in this PR's lifecycle) +- [ ] `release.yml` triggered + GitHub Release created (auto on tag push; D37 phase_rolling_mode gate will pass because Unreleased is now sentinel-only) + +### Known limitations carried to v1.x + +Full list with code anchors + start triggers in [`docs/v1x-roadmap.md`](./docs/v1x-roadmap.md): +- Streaming-path singleflight (issue #16, ADR 0005 Amendment 8 design ratified) +- Multi-key auth (`lib/keys.mjs`) +- Soft-trigger reactivation (ADR 0004 Amendment 2) +- `/health` activeSpawns integration (ADR 0002 Amendment 6 forward note) +- Provider-level `cacheKeyFields` mask (ADR 0005 Amendment 7 forward note) +- Streaming-path SPAWN_FAILED salvage (bundled with #1 in v1.x) +- D40 AUTH_MISSING tuple test coverage (test polish) + ## v0.1.0 — 2026-05-24 ### Phase 1 Close — Multi-provider proxy core diff --git a/docs/adr/0002-plugin-architecture.md b/docs/adr/0002-plugin-architecture.md index 2eff2d2..94377e5 100644 --- a/docs/adr/0002-plugin-architecture.md +++ b/docs/adr/0002-plugin-architecture.md @@ -7,6 +7,8 @@ ## Amendments +> **Note on numbering.** Sequence is 1, 3, 4, 5, 6 — Amendment 2 was never written. The reserved slot was originally planned for a separate `maxConcurrent` ratification, but that content was folded into Amendment 1 (the retroactive contract-sync amendment) at filing time and the gap was not backfilled. The gap is intentional and load-bearing — no missing content; do not renumber Amendments 3+ to close it (cross-references to Amendment N from other docs would silently break). + ### Amendment 6 — 2026-05-24: `maxConcurrent` runtime enforcement landed (D38, issue #1) - **Finding:** Amendment 1 (2026-05-23) ratified `maxSpawnTimeMs` into the Provider contract but explicitly noted that `hints.maxConcurrent` remained **declarative-only at v0.1** — type-validated at startup in `lib/providers/base.mjs` (`validateProvider` requires it to be a non-negative integer) but unenforced at runtime (no semaphore / in-flight counter / spawn queue in `server.mjs`). Cold-audit catch from D11 (commit `f659e29`): the diff-review reviewer grep-verified that the original ADR draft's claim "Enforced by the spawn-concurrency guard in `server.mjs`" was false. GitHub issue #1 was filed to track the gap. D38 closes that gap. diff --git a/docs/adr/0005-cache-cross-provider.md b/docs/adr/0005-cache-cross-provider.md index 2d2030d..128493c 100644 --- a/docs/adr/0005-cache-cross-provider.md +++ b/docs/adr/0005-cache-cross-provider.md @@ -11,7 +11,7 @@ **Status:** Design ratified. Implementation deferred to v1.x. -**Context.** Amendment 6 (D34) formally deferred streaming-path D4 singleflight participation with the note "the design alone warrants a dedicated ADR." Round-6 cold-audit F13 (filed as issue #16) raised the sibling TOCTOU window: `server.mjs:782 preCheckHit = await cacheStore.peek(...)` followed by streaming-branch entry at line 811 (gated on `!preCheckHit`) creates a race where, between peek and spawn, a concurrent populator can write the cache OR a TTL can expire. The streaming branch is path-locked at the moment of the peek result. +**Context.** Amendment 6 (D34) formally deferred streaming-path D4 singleflight participation with the note "the design alone warrants a dedicated ADR." Round-6 cold-audit F13 (filed as issue #16) raised the sibling TOCTOU window: `server.mjs:782 preCheckHit = await cacheStore.peek(...)` followed by the streaming-branch entry conditionals at lines 817–823 (the TODO anchor sits just above at line ~810 and is the navigable landmark; line numbers may drift across commits) creates a race where, between peek and spawn, a concurrent populator can write the cache OR a TTL can expire. The streaming branch is path-locked at the moment of the peek result. This amendment ratifies the v1.x design so the implementation work has a single specification to follow. @@ -59,7 +59,7 @@ This amendment ratifies the v1.x design so the implementation work has a single - For each `client ∈ attachedClients`: if `client.queueByteSize + chunkSize > PER_CLIENT_QUEUE_CAP` (default 1 MB), the client is disconnected with `STREAM_BACKPRESSURE` (see §8). Otherwise push the chunk into `client.queue`, update `queueByteSize`, fire `resolveNext` if pending. When the source iterator returns/throws/aborts, the tee task: - - On normal completion: writes `accumulatedChunks` to cache via the standard cache-write conditions (cacheable opt-out, truncated-not-cached, size cap — all from Amendment 1/3/5). Resolves all clients' `resolveNext` with their remaining queue then sentinel-marks `done`. Releases the D38 spawn slot once. Removes the entry from `_streamingInflight`. + - On normal completion: writes `accumulatedChunks` to cache via the standard cache-write conditions — `truncated-not-cached` from § Decision § "Cache write conditions" item 1; `cacheable=false` opt-out from Amendment 3; `claude -p --output-format text` wire-shape limitation from Amendment 5; size cap from Amendment 3. (Note: ADR 0005 has no Amendment 1 heading — the section §-Decision body item-1 is the source for `truncated-not-cached`, NOT a numbered amendment.) Resolves all clients' `resolveNext` with their remaining queue then sentinel-marks `done`. Releases the D38 spawn slot once. Removes the entry from `_streamingInflight`. - On source error: rejects all clients with the error. Does NOT write cache. Releases the D38 spawn slot. Removes entry. - On source abort (all clients disconnected): cancels the iterator via AbortController, releases the slot, removes entry. No cache write (partial response not persisted, matches D16 buffered-path SPAWN_FAILED salvage NOT applying to abort). diff --git a/package.json b/package.json index f795d63..534e203 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "olp", - "version": "0.1.0", + "version": "0.1.1", "description": "Personal multi-provider LLM proxy. Successor to OCP. One HTTP endpoint, multiple subscriptions behind it, automatic routing + fallback + caching.", "type": "module", "main": "server.mjs",