docs+fix+test: D36 — pre-Phase-2 batch #2 (issues #2 #5 #6 #13 #14 #15)

Second batch of pre-Phase-2 cleanup. 6 GitHub issues closed in one
cohesive docs/governance commit with 1 small code addition (#2 debug
log line in server.mjs) and 1 transcript artifact (#15 new file).

Changes (7 files modified, 1 file created, +470 / -13):

**Code changes**

1. **#2 — cache_control partial-noop debug log** (server.mjs)

   ADR 0005 § D2 says: "for non-Anthropic targets, the bypass markers
   are noop'd (logged once per request at debug level so users can
   see they were ignored)". Pre-D36 logEvent fired only when an
   Anthropic hop actually bypassed; non-Anthropic hops with markers
   present were silently noop'd.

   New 12-line block in handleChatCompletions right after
   hasCacheControlMarkers is computed. Fires logEvent('debug',
   'cache_control_partial_noop', { chain: [<provider names>],
   marker_count: <count> }) when:
   - hasCacheControlMarkers === true AND
   - chain.some(hop => hop.provider !== 'anthropic')

   Fires at most once per request (top-level if, not in a loop). No
   log when no markers, or when every chain hop is Anthropic. The
   existing cache_bypass debug log inside shouldBypassCacheForHop is
   untouched.

   marker_count sums body-side and IR-side extractCacheControlMarkers
   results. At v0.1 the IR term is structurally 0 (openAIToIR strips
   cache_control); inline comment marks this as a revisit point for
   the future ADR 0003 amendment that activates cache_control in the
   IR whitelist.

**Documentation amendments**

2. **#5 — ADR 0002 vibe.mjs → mistral.mjs** (docs/adr/0002-plugin-architecture.md)

   § Decision filesystem layout: `vibe.mjs` corrected to
   `mistral.mjs` (file named after provider key per the convention
   anthropic.mjs/codex.mjs). The vibe.mjs entry was an early-draft
   naming choice that never landed. Amendment 5 prepended above
   Amendment 4 documenting the filename correction + explicit
   convention statement (file named after provider key, not CLI
   binary) for future contributors.

3. **#6 — mistral.mjs A5 flip + ALIGNMENT.md table update**
   (lib/providers/mistral.mjs + ALIGNMENT.md)

   Pre-D36: header A5 (model flag) status was UNPINNED-D-later-verifies
   but function body lines 376-380 said CONFIRMED-NOT-APPLICABLE
   (DeepWiki enumeration already confirmed `--model` does not exist
   on vibe CLI). Header now reflects CONFIRMED-NOT-APPLICABLE with
   DeepWiki citation (DOCS-4). ALIGNMENT.md Speculative-Candidate
   table mistral row: A5 removed from UNPINNED list; A4, A6, A7, A8
   preserved with parenthetical descriptions intact. No code change
   to function body (already correct).

4. **#13 — /v1/models alias entries — ALIGNMENT.md + spec-pin
   governance** (ALIGNMENT.md + docs/openai-spec-pin.md)

   Round-6 F10 flagged D27 F15's alias entries on /v1/models as
   borderline Rule 2(b) violation (OpenAI spec does not enumerate
   aliases as separate entries). Option C selected: keep current
   behavior, document the controlled deviation.

   - ALIGNMENT.md: new "Controlled deviations (entry-surface scope)"
     subsection under "Class-specific Exceptions". Entry 1 documents
     the /v1/models alias deviation with rationale (D27 F15
     onboarding), formal contract reference, field constraints
     (owned_by matches canonical, created matches canonical, no
     invented fields), SPOT reference (getAliasMap()), and re-
     evaluation trigger.
   - docs/openai-spec-pin.md: new alias-surfacing subsection under
     GET /v1/models with full 4-field contract table (id/object/
     created/owned_by), rationale, sourcing explanation, forward
     path.
   - server.mjs handleModels: NO CHANGE — behavior preserved.

5. **#15 — Anthropic v2.1.89 transcript artifact**
   (docs/provider-audits/anthropic.md NEW; ALIGNMENT.md +
   lib/providers/anthropic.mjs cross-references)

   Round-6 F12: ALIGNMENT.md anthropic row pin (v2.1.89, observed at
   D4) cited the plugin header; plugin header cited the observation
   date but no transcript. Circular per Rule 1 ("observed behaviour,
   transcript attached").

   New file docs/provider-audits/anthropic.md (single living artifact,
   not version-specific):
   - Date of capture: 2026-05-24
   - Observed `claude --version`: 2.1.132 (Claude Code) — captured
     today on the project maintainer's primary workstation
   - Plugin-pinned version: @anthropic-ai/claude-code v2.1.89 (from
     D4 implementation pin in ALIGNMENT.md Provider Authority Pins)
   - Version drift: honestly documented — pin is v2.1.89, live is
     v2.1.132, drift within tolerance, re-audit triggers named
   - Sample invocation: `claude -p --output-format text --no-session-
     persistence --model <model> [--debug]`
   - Flag-surface table: 5 OLP-consumed flags verbatim from
     `claude -p --help` (-p / --output-format / --no-session-
     persistence / --model / --debug)
   - Citation cross-references back to ALIGNMENT.md + plugin header

   ALIGNMENT.md anthropic row: appended "transcript artifact: docs/
   provider-audits/anthropic.md (captured 2026-05-24)" — closes the
   circular citation.

   lib/providers/anthropic.mjs header: 5-line pointer to the artifact
   with version numbers stated explicitly.

**Tests** (test-features.mjs): 424 → 431 (+7):

- #2 partial-noop log ×3:
  - Suite 9f case 1: markers + mixed chain → fires once at level=debug
  - Suite 9f case 2: no markers → suppressed
  - Suite 9f case 3: anthropic-only chain → suppressed

- #14 cache_control slot determinism ×4:
  - #14a: markers-present IR produces different key from no-markers IR
  - #14b: same IR computed twice yields identical key
  - #14c: two independently-constructed IRs with identical payloads
    yield same key
  - #14d: both top-level and content-array-nested markers affect the key

  All 4 tests call computeCacheKey directly on hand-built IRs
  (bypassing openAIToIR which strips markers at v0.1). Per ALIGNMENT.md
  Rule 2 (No Invention), no sortMarkers helper added — the slot is
  dead-code at v0.1 and shipping a helper without a caller authority
  would be invention. Test comment documents the forward-activation
  contract.

Pre-commit fold-in (per evidence-first checkpoint #4):
- **D36 reviewer flagged marker_count latent double-count risk**
  (Suggestion #1, non-blocking). The sum at server.mjs:435-437 is
  safe at v0.1 (IR term structurally 0) but will 2× when a future
  ADR 0003 amendment activates cache_control in the IR whitelist.
  Folded in a 4-line comment marking the revisit point.

Two other non-blocking reviewer suggestions not folded:
- Test count brief-vs-deliverable discrepancy (4 not 3 for #14) is
  informational — the 4-test variant is strictly better (covers
  content-array nesting which is a real extractCacheControlMarkers
  contract path).
- Recapture-procedure git-add reminder in anthropic.md is low-priority
  procedure documentation.

Authority:
- ADR 0005 § D2 — cache_control partial-noop debug log requirement (#2)
- ADR 0002 § Decision filesystem layout (Amendment 5) — plugin file
  naming convention (#5)
- DeepWiki vibe CLI flag enumeration — A5 not applicable (#6)
- ALIGNMENT.md Rule 2(b) + docs/openai-spec-pin.md GET /v1/models —
  alias controlled deviation (#13)
- ADR 0005 cache key stability invariant + ADR 0003 forward-compat —
  cache_control slot determinism contract (#14)
- ALIGNMENT.md Rule 5 (observed behaviour, transcript attached) +
  Provider Authority Pins anthropic row — transcript artifact (#15)
- CC 开发铁律 v1.6 § 10.x — independent fresh-context reviewer
- CLAUDE.md release_kit_overlay phase_rolling_mode — D36 under
  "Unreleased" against Phase 2; no version bump

Reviewer (Iron Rule v1.6 § 10.x Mode A, fresh-context opus,
independent of drafter): APPROVE. Critical depth checks:
- #2: gate condition fires only on (markers AND non-anthropic-hop);
  pre-existing cache_bypass log inside shouldBypassCacheForHop
  untouched
- #5: lib/providers/ directory verified — mistral.mjs exists,
  vibe.mjs does not exist
- #6: mistral.mjs spawn-site body comment (lines 376-380) already
  CONFIRMED-NOT-APPLICABLE pre-D36 and unchanged in this diff
- #13: server.mjs handleModels unchanged (verified via grep)
- #14: all 4 tests pass against current code; no sortMarkers helper
  shipped (Rule 2 No Invention honored)
- #15: live `claude --version` independently run by reviewer →
  matches artifact (2.1.132); all 5 OLP-consumed flags independently
  verified present in `claude -p --help`
- Hygiene: 0 hits for personal markers, home paths, OAuth tokens,
  internal IPs across all 8 files
- 431/431 tests pass in reviewer's independent npm test run

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 20:38:16 +10:00
co-authored by Claude Opus 4.7
parent 2600185edb
commit e96752a528
8 changed files with 591 additions and 13 deletions
+370
View File
@@ -1543,6 +1543,160 @@ describe('Cache layer — computeCacheKey (Suite 9)', () => {
});
});
// ── D36 #14: cache_control slot determinism regression ─────────────────────
//
// Context (issue #14 + ADR 0005 Amendment 4):
// computeCacheKey at lib/cache/keys.mjs:~224 calls extractCacheControlMarkers
// on ir.messages. openAIToIR (lib/ir/openai-to-ir.mjs translateMessage) does
// NOT whitelist cache_control, so v0.1 IRs constructed from OpenAI requests
// always have markers stripped → the slot is structurally null. D27 F10
// Amendment 4 acknowledged this is forward-compatible: when a future ADR 0003
// amendment adds cache_control to the IR field set, the slot starts carrying
// meaningful data without a cache-key schema change.
//
// What this suite guards:
// 1. The cache_control slot IS populated when markers are present on the IR
// (forward-compat scenario — markers attached directly to an IR object
// bypassing the openAIToIR translation).
// 2. Computing the key twice with the same marker payload yields identical
// keys (determinism — bedrock invariant per ADR 0005 § cache key stability).
// 3. The slot is included in the composition — two IRs differing ONLY in
// cache_control markers produce different keys.
//
// Future activation contract (when ADR 0003 adds cache_control to the IR
// whitelist and openAIToIR starts preserving markers):
// - extractCacheControlMarkers must continue to return markers in messages-
// iteration order; nested-in-content markers follow the outer message in
// positional order (see lib/cache/keys.mjs:126-148).
// - The current implementation JSON.stringifies the marker array verbatim
// (no pre-sort) — determinism relies on messages-array order being stable
// across translations and on per-marker object key insertion order being
// stable (which it is for objects authored at one source site).
// - If a future amendment introduces semantic equivalences across marker
// orderings (e.g., "two markers in different positions should still hit
// the same cache entry"), the implementation must add a sortMarkers helper
// before serialization. This test would then need an update to assert the
// normalized-order behavior. As of D36, no such equivalence is claimed; the
// test asserts the strict "same input → same output" determinism only.
//
// Risk path chosen: option (a) test-only (no code change in lib/cache/keys.mjs).
// Rationale: the dead-code path is unreachable from openAIToIR at v0.1, so a
// helper added now would have zero callers; preferred to defer the helper until
// the IR amendment actually activates the slot. Per ALIGNMENT.md Rule 2 (No
// Invention), shipping a sortMarkers helper today would be invention without a
// caller authority. Adding tests is risk-free; adding helpers is not.
describe('D36 #14 — cache_control slot determinism regression', () => {
// Construct an IR with synthetic cache_control markers attached directly
// (bypass openAIToIR which strips them at v0.1).
function makeIRWithMarkers(markers, opts = {}) {
const messages = opts.messages ?? [
{ role: 'user', content: 'sample-prompt' },
];
// Attach the first marker to the first message; if a second marker is
// provided, attach it to the second message (or to message 0 nested in a
// content array, depending on opts.nestSecond).
const annotated = messages.map((m, idx) => {
if (idx === 0 && markers[0]) {
return { ...m, cache_control: markers[0] };
}
if (idx === 1 && markers[1]) {
return { ...m, cache_control: markers[1] };
}
return m;
});
return makeIR({ messages: annotated, ...opts.irOverrides });
}
it('D36 #14a: cache_control slot is populated when markers are present on the IR', () => {
// The slot is normally null at v0.1 (openAIToIR strips). When markers ARE
// present (e.g. an IR constructed directly with markers, or after a future
// ADR 0003 amendment preserves them), the slot carries the markers.
const irNoMarkers = makeIR({ messages: [{ role: 'user', content: 'hi' }] });
const irWithMarker = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [{ role: 'user', content: 'hi' }],
});
const kNo = computeCacheKey('anthropic', 'claude-haiku-4-5', irNoMarkers);
const kYes = computeCacheKey('anthropic', 'claude-haiku-4-5', irWithMarker);
// Different keys: marker presence is observable in the cache key.
assert.notEqual(kNo, kYes,
'IR with cache_control markers must produce a different cache key from IR without markers');
assert.equal(typeof kYes, 'string');
assert.equal(kYes.length, 64);
});
it('D36 #14b: cache key is deterministic across two computations on the same IR with markers', () => {
// The bedrock invariant: same inputs → same key. The cache_control slot
// must not introduce nondeterminism (e.g., timestamp, random, iteration
// order from a Map).
const ir = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [
{ role: 'user', content: 'first turn' },
{ role: 'assistant', content: 'first reply' },
],
});
const k1 = computeCacheKey('anthropic', 'claude-haiku-4-5', ir);
const k2 = computeCacheKey('anthropic', 'claude-haiku-4-5', ir);
assert.equal(k1, k2, 'cache key must be deterministic for the same IR with markers');
});
it('D36 #14c: same markers in the same positional order produce the same key', () => {
// Two IRs constructed independently with the same marker payload at the
// same positions must produce the same key. This is the forward-activation
// contract: when openAIToIR preserves markers, two identical OpenAI
// requests must produce identical cache keys.
const irA = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [{ role: 'user', content: 'hello' }],
});
const irB = makeIRWithMarkers([{ type: 'ephemeral' }], {
messages: [{ role: 'user', content: 'hello' }],
});
const kA = computeCacheKey('anthropic', 'claude-haiku-4-5', irA);
const kB = computeCacheKey('anthropic', 'claude-haiku-4-5', irB);
assert.equal(kA, kB,
'two independently-constructed IRs with identical marker payloads must share a cache key');
});
it('D36 #14d: markers nested in content array participate in the cache key (extractCacheControlMarkers contract)', () => {
// extractCacheControlMarkers (lib/cache/keys.mjs:126-148) finds markers at
// both message top-level AND nested inside content array parts. The cache
// key must reflect both surfaces. This is the slot's full forward-compat
// contract — when the IR carries cache_control, OLP must observe it on the
// wire shape the IR provides, including the OpenAI-style content-array
// nesting that Anthropic prompt-caching uses in production.
const irTopLevel = makeIR({
messages: [
{ role: 'user', content: 'hi', cache_control: { type: 'ephemeral' } },
],
});
const irNested = makeIR({
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'hi', cache_control: { type: 'ephemeral' } },
],
},
],
});
const irNoMarkers = makeIR({
messages: [{ role: 'user', content: 'hi' }],
});
const kTop = computeCacheKey('anthropic', 'claude-haiku-4-5', irTopLevel);
const kNested = computeCacheKey('anthropic', 'claude-haiku-4-5', irNested);
const kNone = computeCacheKey('anthropic', 'claude-haiku-4-5', irNoMarkers);
// Both marker surfaces must register against the key (be observably distinct
// from the no-markers case).
assert.notEqual(kTop, kNone, 'top-level cache_control marker must affect cache key');
assert.notEqual(kNested, kNone, 'nested cache_control marker must affect cache key');
});
});
describe('Cache layer — extractCacheControlMarkers + hasCacheControl (Suite 9 cont.)', () => {
// ── Test 9: extractCacheControlMarkers — top-level ───────────────────
@@ -2425,6 +2579,222 @@ describe('D13 — cache_control per-hop bypass correctness (Suite 9d)', () => {
});
});
// ── Suite 9f: D36 #2 cache_control partial-noop debug log ────────────────
//
// Authority: ADR 0005 § Context — "for non-Anthropic targets, the bypass
// markers are noop'd (logged once per request at debug level so users can
// see they were ignored)."
//
// Contract: at request entry in handleChatCompletions, after hasCacheControlMarkers
// is computed, emit ONE logEvent('debug', 'cache_control_partial_noop', { chain, marker_count })
// IF AND ONLY IF (a) markers are present AND (b) at least one chain hop is
// non-Anthropic. The log is suppressed when no markers, or when every hop is
// Anthropic.
//
// Implementation strategy: monkeypatch process.stdout.write (debug events go
// to stdout per the logEvent helper in server.mjs:56-63) and grep for the
// event name across the captured writes.
describe('D36 #2 — cache_control partial-noop debug log (Suite 9f)', () => {
let serverD36;
let portD36;
let savedAnthropicTokenD36;
let savedCodexAuthPathD36;
let suiteCodexAuthFileD36;
// ── stdout capture helpers ───────────────────────────────────────────
let stdoutWrites = [];
let origStdoutWrite = null;
function startStdoutCapture() {
stdoutWrites = [];
origStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = (chunk, ...rest) => {
const s = typeof chunk === 'string'
? chunk
: (Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk));
stdoutWrites.push(s);
return origStdoutWrite(chunk, ...rest);
};
}
function stopStdoutCapture() {
if (origStdoutWrite) {
process.stdout.write = origStdoutWrite;
origStdoutWrite = null;
}
}
/** Returns array of parsed JSON events whose `event` field === eventName. */
function findEvents(eventName) {
const found = [];
for (const w of stdoutWrites) {
for (const line of w.split('\n')) {
if (!line.trim()) continue;
if (!line.includes(`"event":"${eventName}"`)) continue;
try {
const parsed = JSON.parse(line);
if (parsed?.event === eventName) found.push(parsed);
} catch { /* not JSON — ignore */ }
}
}
return found;
}
before(async () => {
savedAnthropicTokenD36 = process.env.CLAUDE_CODE_OAUTH_TOKEN;
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'fake-anthropic-token-for-d36-suite';
const { writeFileSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const { join: pathJoin } = await import('node:path');
suiteCodexAuthFileD36 = pathJoin(tmpdir(), `olp-test-d36-codex-auth-${Date.now()}.json`);
writeFileSync(suiteCodexAuthFileD36, JSON.stringify({ accessToken: 'fake-codex-token-for-d36-suite' }), 'utf8');
savedCodexAuthPathD36 = process.env.OPENAI_CODEX_AUTH_PATH;
process.env.OPENAI_CODEX_AUTH_PATH = suiteCodexAuthFileD36;
const testProviders = loadProviders({ enabled: { anthropic: true, openai: true } });
const { loadedProviders: lp, cacheStore: cs } = await import('./server.mjs');
for (const [name, p] of testProviders) {
lp.set(name, p);
}
cs.clear();
serverD36 = createOlpServer();
portD36 = 22000 + Math.floor(Math.random() * 500);
await new Promise((resolve, reject) => {
serverD36.listen(portD36, '127.0.0.1', resolve);
serverD36.once('error', (e) => {
if (e.code === 'EADDRINUSE') {
portD36++;
serverD36.listen(portD36, '127.0.0.1', resolve);
serverD36.once('error', reject);
} else reject(e);
});
});
});
after(async () => {
stopStdoutCapture();
__resetSpawnImpl();
codexResetSpawnImpl();
if (savedAnthropicTokenD36 !== undefined) {
process.env.CLAUDE_CODE_OAUTH_TOKEN = savedAnthropicTokenD36;
} else {
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
}
if (savedCodexAuthPathD36 !== undefined) {
process.env.OPENAI_CODEX_AUTH_PATH = savedCodexAuthPathD36;
} else {
delete process.env.OPENAI_CODEX_AUTH_PATH;
}
if (suiteCodexAuthFileD36) {
const { unlinkSync } = await import('node:fs');
try { unlinkSync(suiteCodexAuthFileD36); } catch { /* ignore */ }
}
if (!serverD36) return;
return new Promise(r => serverD36.close(r));
});
it('D36 #2a: markers + non-Anthropic chain → cache_control_partial_noop log fires once', async () => {
// Route to openai (gpt-5.5) with cache_control markers present. Per ADR 0005
// § Context, the partial-noop log must fire because the chain contains a
// non-Anthropic hop and markers are present.
codexSetSpawnImpl(makeMockCodexSpawn([
JSON.stringify({ content: 'codex-out-d36-2a' }),
JSON.stringify({ type: 'stop' }),
]));
startStdoutCapture();
try {
const r = await fetch({
port: portD36,
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-5.5',
messages: [
{ role: 'user', content: 'hello-d36-2a', cache_control: { type: 'ephemeral' } },
],
},
});
assert.equal(r.status, 200, `Expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
} finally {
stopStdoutCapture();
}
const events = findEvents('cache_control_partial_noop');
assert.equal(events.length, 1, `Expected exactly 1 cache_control_partial_noop event, got ${events.length}`);
const ev = events[0];
assert.equal(ev.level, 'debug', `event level should be 'debug', got '${ev.level}'`);
assert.ok(Array.isArray(ev.chain), 'chain must be an array');
assert.ok(ev.chain.includes('openai'),
`chain should include 'openai', got: ${JSON.stringify(ev.chain)}`);
assert.equal(typeof ev.marker_count, 'number', 'marker_count must be a number');
assert.ok(ev.marker_count >= 1, `marker_count must be >= 1, got ${ev.marker_count}`);
});
it('D36 #2b: no markers + non-Anthropic chain → cache_control_partial_noop does NOT fire', async () => {
codexSetSpawnImpl(makeMockCodexSpawn([
JSON.stringify({ content: 'codex-out-d36-2b' }),
JSON.stringify({ type: 'stop' }),
]));
startStdoutCapture();
try {
const r = await fetch({
port: portD36,
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-5.5',
messages: [
{ role: 'user', content: 'hello-d36-2b' }, // no cache_control
],
},
});
assert.equal(r.status, 200, `Expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
} finally {
stopStdoutCapture();
}
const events = findEvents('cache_control_partial_noop');
assert.equal(events.length, 0,
`Expected 0 cache_control_partial_noop events when no markers; got ${events.length}: ${JSON.stringify(events)}`);
});
it('D36 #2c: markers + anthropic-only chain → cache_control_partial_noop does NOT fire', async () => {
// anthropic single-hop chain — every hop is anthropic, so the partial-noop
// log must be suppressed. The cache_bypass log (already documented) is the
// correct signal for this case, not partial_noop.
__setSpawnImpl(makeMockSpawn(['anthropic-out-d36-2c']));
startStdoutCapture();
try {
const r = await fetch({
port: portD36,
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'claude-haiku-4-5',
messages: [
{ role: 'user', content: 'hello-d36-2c', cache_control: { type: 'ephemeral' } },
],
},
});
assert.equal(r.status, 200, `Expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
} finally {
stopStdoutCapture();
}
const events = findEvents('cache_control_partial_noop');
assert.equal(events.length, 0,
`Expected 0 cache_control_partial_noop events when chain is anthropic-only; got ${events.length}: ${JSON.stringify(events)}`);
});
});
// ── Suite 11: Codex plugin (D6) ──────────────────────────────────────────
//
// All tests are UNIT tests. No real `codex` binary is invoked.