Skip to content

Lane 1 -- P0.1 Public Error Contract (typed failure classes, no false promises, no leaks) - #292

Closed
mosesy5688-cell wants to merge 2 commits into
mainfrom
lane1-p01-public-error-contract
Closed

Lane 1 -- P0.1 Public Error Contract (typed failure classes, no false promises, no leaks)#292
mosesy5688-cell wants to merge 2 commits into
mainfrom
lane1-p01-public-error-contract

Conversation

@mosesy5688-cell

@mosesy5688-cell mosesy5688-cell commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Lane 1 -- P0.1 Public Error Contract. Single lane, branched from the frozen base. No merge, no deploy, no workflow dispatch is requested or authorised by this PR.

1. Base proof

git rev-parse HEAD          a5c770d9874fe0dbce7957c4ca15132aaa4abae9
git rev-parse HEAD^         c88c1f525d623f0b3a16715551d3a5983438e36a
git merge-base HEAD c88c1f5 c88c1f525d623f0b3a16715551d3a5983438e36a
git log --oneline c88c1f5..HEAD
  a5c770d feat(worker): P0.1 public error contract (typed failure classes, no false promises, no leaks)

One commit. Its parent IS the base SHA -- not a descendant of it. origin/main was c88c1f525d623f0b3a16715551d3a5983438e36a at branch creation, immediately before the push, and immediately after it.

2. TYPECHECK

TYPECHECK = NOT CONFIGURED / NOT CLAIMED

TypeScript is absent from package.json, package-lock.json and node_modules. No typecheck is required, no prep PR was created, and no temporary compiler was downloaded.

3. Observer census, independently derived

Enumerated from base-SHA source, not copied from the brief. Every surface that can observe a SnapshotContractError, and the class it now carries:

# Surface Route to the emitter at base Now
1 /bioactivities bioactivities.ts:65 instanceof 502 snapshot_contract/false
2 /papers papers.ts:65 instanceof 502 snapshot_contract/false
3 /trials trials.ts:65 instanceof 502 snapshot_contract/false
4 /target target.ts:98 instanceof 502 snapshot_contract/false
5 /negative-evidence no branch; fell to the residual 500 snapshot_contract/false (typed branch added)
6 /repurposing-evidence rethrown by the aggregator; no branch 500 snapshot_contract/false (typed branch added)
7 /compound zero catch blocks; reaches json500 500 snapshot_contract/false (shared classifier)
8 /xrefs zero catch blocks; reaches json500 500 snapshot_contract/false (shared classifier)
9 MCP get_target_drugs rethrow -> mcp.ts residual -32603, error.data snapshot_contract/false
10 MCP get_negative_evidence rethrow -> same -32603, error.data snapshot_contract/false
11 MCP get_repurposing_evidence rethrow -> same -32603, error.data snapshot_contract/false
12 MCP resolve_entity name-based rethrow -> same -32603, error.data snapshot_contract/false
13 MCP search propagates -> same -32603, error.data snapshot_contract/false

Delta from the brief's 5a: NONE. Thirteen surfaces, same routes, same emitters. All thirteen are asserted by tests/api/error-contract-routes.test.ts (33 tests, green).

The two name-based checks (entity-resolver.ts:127, repurposing-aggregator.ts:161, err.name === 'SnapshotContractError') are untouched; the typed branches downstream are what fix the classification.

Status is preserved everywhere. Surfaces 5-8 previously reached a generic 500 and still return 500; only the class is now correct. Section 6e freezes every existing status with exactly two named exceptions, and neither of these is one -- so the honest fix is the carriers, not the status.

4. The exported failure-contract table

src/worker/lib/failure-contract.ts, FAILURE_CONTRACT:

failure_class retryable produced Status
source_unavailable true true LIVE
shard_read_unavailable true true LIVE (introduced by this lane; maps onto the existing typed NegShardError)
parse_failed false true LIVE
snapshot_contract false true LIVE
shard_manifest_invalid false true LIVE
data_layer_unconfigured false true LIVE (9 paths)
unclassified true true LIVE (residual only)
timeout true true LIVE -- produced only by classifySourceLoadError's message branch (source-load-error.ts:85-86), which section 7d exempts from rewriting. It reaches four public carriers: bioactivities.ts, papers.ts, trials.ts, repurposing-evidence.ts.
object_integrity false false DECLARED BY THIS CONTRACT, NOT PRODUCED

object_integrity has zero occurrences across every tracked file at the base -- this contract introduces it. No served path verifies a checksum (the only checksum-verifying readers are imported by tests only; shardHashes is populated by two loaders and never read), and no branch emits an integrity label for a transport fault any more. It is not deleted and no producer is invented; a guard test asserts no emitting path yields it, so the day one does, the test says so.

data_layer_unconfigured is nine paths, not eight: the eight REST 503s plus mcp-handlers.ts:37, which is a ToolError(-32603) and would otherwise have left the MCP surface silent.

5. Serialized examples -- every LIVE class, REST and MCP

All captured from the real worker entry, not hand-written. Every REST example carries cache-control=null and retry-after=null.

REST

503 /api/v1/compound/CID:2244/negative-evidence
{ "error": "Data layer not configured", "failure_class": "data_layer_unconfigured",
  "retryable": false, "detail": "R2 binding SCIWEON_R2 is not bound to this Worker." }

502 /api/v1/target/P00533
{ "error": "Data integrity error", "failure_class": "snapshot_contract", "retryable": false,
  "detail": "The snapshot pointer failed contract validation. Nothing is served from an unrecognized contract." }

503 /api/v1/target/P00533          <- the target-index-absent path, no longer a domain 404
{ "error": "Target index not available", "failure_class": "source_unavailable", "retryable": true,
  "detail": "The target index could not be read from the current snapshot. This is a READ failure and NOT a finding that the target is absent." }

502 /api/v1/compound/CID:2244/papers
{ "source": "papers", "error": "Source unavailable", "failure_class": "parse_failed", "retryable": false,
  "detail": "An upstream source read failed. This is a READ failure and NOT a finding that no paper data exists." }

503 /api/v1/compound/CID:2244/bioactivities
{ "source": "bioactivities", "error": "Source unavailable", "failure_class": "timeout", "retryable": true,
  "detail": "An upstream source read failed. This is a READ failure and NOT a finding that no bioactivity data exists." }

503 /api/v1/compound/CID:2244/negative-evidence
{ "error": "Negative-evidence service unavailable", "failure_class": "shard_read_unavailable", "retryable": true,
  "detail": "A sharded read could not be completed. This is a READ failure and NOT a finding that no data exists." }

503 /api/v1/compound/CID:2244/negative-evidence
{ "error": "Negative-evidence service unavailable", "failure_class": "shard_manifest_invalid", "retryable": false,
  "detail": "A shard manifest failed its structural checks. This is a producer-data fault and NOT a finding that no data exists." }

500 /api/v1/compound/CID:2244
{ "error": "Internal server error", "failure_class": "unclassified", "retryable": true,
  "detail": "An unexpected internal failure occurred. It is NOT a finding that no data exists." }

MCP -- carriers in error.data, message prose only

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32603,
    "message": "Data layer not configured (R2 binding missing)",
    "data": { "failure_class": "data_layer_unconfigured", "retryable": false } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32000,
    "message": "A sharded negative-evidence read failed. This is a READ failure and NOT a finding that no negative evidence exists.",
    "data": { "failure_class": "shard_read_unavailable", "retryable": true } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32000,
    "message": "A sharded negative-evidence read failed. This is a READ failure and NOT a finding that no negative evidence exists.",
    "data": { "failure_class": "shard_manifest_invalid", "retryable": false } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32000,
    "message": "An upstream evidence source read failed. This is a SOURCE FAILURE and NOT a finding that no evidence exists.",
    "data": { "failure_class": "source_unavailable", "retryable": true, "source": "snapshot-pointer" } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32000,
    "message": "The target index could not be read from the current snapshot. This is a READ failure and NOT a finding that the target is absent.",
    "data": { "failure_class": "source_unavailable", "retryable": true } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32603,
    "message": "The snapshot pointer failed contract validation. Nothing is served from an unrecognized contract.",
    "data": { "failure_class": "snapshot_contract", "retryable": false } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32603,
    "message": "Internal server error",
    "data": { "failure_class": "unclassified", "retryable": true } } }

{ "jsonrpc": "2.0", "id": 1, "error": {
    "code": -32000,
    "message": "An upstream evidence source read failed. This is a SOURCE FAILURE and NOT a finding that no evidence exists.",
    "data": { "failure_class": "parse_failed", "retryable": false, "source": "papers" } } }

Stated precisely: I did not separately capture an MCP timeout example. It travels the identical SourceLoadError path as the MCP parse_failed example immediately above -- same tool, same code, same message, data.failure_class reading timeout and retryable reading true. The REST timeout example above is real and captured.

-32602 invalid-params carries no data. Client input is out of scope and is not stamped with carriers; this is asserted.

tools/list excerpt -- sciweon_get_target_drugs.description, served on every call

Before (base): ... If the index is not yet built (immediately post-deploy) or the target has no bioactivities in the current snapshot, returns {resolved: false} rather than an error. ...

After:

... If the target has no bioactivities in the current snapshot, returns {resolved: false} rather than an error; if the index itself cannot be read, the tool returns an error carrying failure_class and retryable, never a {resolved: false} payload. ...

6. Leak sentinel (9b.7)

tests/api/error-contract-routes.test.ts, 22 sentinel assertions, all green. The underlying error's message is read of snapshots/latest.json failed for shard bucket-0007 SENT-<timestamp>-<random>; the token is generated per run, so a stale fixture cannot make it pass. Every one of the eight REST routes and both MCP aliases across all five tools is asserted to contain neither the token, nor snapshots/latest.json, nor bucket-0007 -- in the serialized body, in error.message, and in error.data. A control assertion proves the sentinel is genuinely present in the underlying error, so the suite cannot pass vacuously.

 ✓ tests/api/error-contract-routes.test.ts (33 tests) 38ms
   Test Files  1 passed (1)
        Tests  33 passed (33)

No public body echoes an underlying error message any more. At base, five sites did: worker.ts:142, target.ts:118, negative-evidence.ts:101, repurposing-evidence.ts:88, mcp.ts:138 -- each passed the raw message through whenever it was under 200 characters.

7. No Retry-After, no failure-response Cache-Control

Confirmed. git grep -ci "retry-after" -- src/ returns zero at base and after. Every failure case in the matrix asserts res.headers.get('retry-after') === null and res.headers.get('cache-control') === null. The cache-control headers that remain in src/worker/** are all on 200 responses.

8. src/worker.ts net-zero budget

export interface Env   BEFORE: src/worker.ts:36
export interface Env   AFTER:  src/worker.ts:36
net line delta above :139 = 0   (header block 5 lines -> 4; one import added)
file total 144 lines before, 144 after

The grant was used exactly: the error-contract text at :19-23 (five lines, not six), json500 at :139-144, and one added import. src/worker.ts:24 ( */, the docblock terminator) is preserved verbatim -- it appears on neither side of the diff, proven by git diff -U0 src/worker.ts | grep -E "^[+-] \*/$" returning nothing. The Env interface, the route registration and dispatch body, the _health branch, the ASSETS fallthrough, and the version labels at :2/:16/:17 are all outside the diff.

The funding deletion is :22 (429 -- rate limited (not implemented yet; reserved)), an unearned future-capability claim inside the very block being rewritten to remove unearned claims.

9. Import anchor (0b), quoted

Lane 1's added import goes immediately AFTER handleNegativeEvidence (the first line of the import block).

26: import { handleNegativeEvidence } from './worker/api/negative-evidence';
27: import { classifyThrown, failureBody } from './worker/lib/failure-contract';
28: import { handleMcp } from './worker/api/mcp';

One import line, at the mandated anchor, non-adjacent to the end of the block.

10. classifySourceLoadError exemption (7d)

source-load-error.ts:72-95 classifies by message regex and is the sole producer of source_unavailable, parse_failed and timeout. Rule 4c.1 forbids message inference. The ruling is that 4c.1 binds NEW code and the shard path; classifySourceLoadError is exempt and is NOT rewritten in this lane. Its status mapping is unchanged. Replacing it would re-classify faults on four loaders and change statuses that 6e freezes, which is a separate lane. Its /short read|etag|...|missing/ branch already yields source_unavailable/true, which is D-2-consistent, so the exemption does not perpetuate a contradiction. Its behaviour is bit-for-bit unchanged by this PR because every r2-fetch.ts message it sniffs is preserved verbatim (section 12).

11. The four couplings (7c) and the three repoints (8a)

Couplings:

  1. tests/factory/neg-shard-roundtrip.test.ts:94 asserts the strict decode throws matching /strict/i. The new message keeps "strict" verbatim (Shard decode failure (strict): ...), so the assertion is untouched and green.
  2. decompressPayload(x, true) has four callers in src/: rk16/canonical-reader.ts, rk16/directory-reader.ts, rk16/projection-reader.ts (the three rk16 strict readers) plus neg-evidence-filter.ts:37. The rk16 three inherit the new ShardDataInvalidError type with behaviour unchanged -- it extends Error and preserves the message. neg-evidence-filter.ts:37 is the one whose behaviour intentionally changes: a strict decode failure there is now typed, so the neg path classifies it as shard_manifest_invalid instead of falling to a message sniff.
  3. tests/api/target.test.ts:147 asserted res.status === 404 on the target-index-absent path. Rewritten to 503 plus both carriers.
  4. tests/api/mcp-target.test.ts:160-163 asserted body.error === undefined and payload.resolved === false on the false-clean. Rewritten to the error-carrier form (error.code === -32000, error.data === {source_unavailable, true}), and the stale docstring at :5-6 went with it.

Both of couplings 3 and 4 are behavioural and break independently of any string. Section 9d's "no existing test asserts the old strings" is true (git grep -i "retry shortly" -- tests/ returns zero at base) and, on its own, misleading.

Repoints of NegShardError (extracted to src/worker/lib/neg-shard-error.ts), all three:

  • src/worker/api/negative-evidence.ts:17
  • src/worker/lib/mcp-handlers.ts:15
  • tests/worker/neg-stale-manifest-legacy-fallback.test.ts:20 (the one test importer)

The extraction was required by headroom: neg-evidence-loader.ts was 249 lines (headroom 1). It is now 245 (headroom 5). SnapshotContractError was NOT given metadata -- snapshot-context.ts is at 250 lines, headroom 0, and is untouched.

Module organisation, stated because it is a judgment call. Section 8a describes one extracted module carrying NegShardError, the union, the instanceof classifier, ShardDataInvalidError and classifyThrown. Section 8c pre-approves two filenames for it and section 9a requires a single exported contract table, so the content is split across both pre-approved names: neg-shard-error.ts holds the shard error types and the by-type classifier; failure-contract.ts holds the class union, the table, the body/data builders and classifyThrown. This split is what makes 0a satisfiable -- json500 needs both classifyThrown and the body builder, and it is granted exactly ONE import line, so the two must live in one module. No third filename was created.

12. r2-fetch.ts -- six discriminants, call sites, and the blast-radius confirmation

Six throw sites, each now R2ReadError with a structural discriminant, message text preserved verbatim:

site discriminant message
:41 not_found R2 object not found: ${key}
:54 disappeared R2 object disappeared between head() and get(): ${key}
:57 etag_drift R2 etag drifted mid-fetch for ${key} (head=..., get=...)
:63 short_read Short read on ${key}: got N bytes, expected M ...
:123 range_failed R2 range fetch failed: ${key} [offset, +length)
:128 short_range_read Short range read on ${key}@offset+length: got N bytes ...

short_range_read is a SEPARATE discriminant: its message is Short range read on ..., which does not contain the substring Short read, so it matched no consumer regex at base. range_failed and short_range_read KEEP their generic-500 outcome and are not re-routed -- collapsing :128 into short_read would move a route from 500 to 502 against the 6e freeze.

Verbatim-message proof: extracting every template literal from the file before and after and diffing shows only additions, all inside the new doc comment -- not one original message literal was changed or removed.

Every call site of the six throws. r2-fetch.ts is imported by 18 production modules plus one in-src test file. The throwing functions are fetchR2Object (not_found/disappeared/etag_drift/short_read) and fetchR2RangeBytes (range_failed/short_range_read); fetchR2GunzippedText and fetchR2JsonText both delegate to fetchR2Object. Call sites:

src/worker/api/bioactivities.ts:55          src/worker/lib/mcp-handlers.ts:186
src/worker/api/papers.ts:55                 src/worker/lib/neg-evidence-filter.ts:37
src/worker/api/target.ts:98                 src/worker/lib/neg-evidence-loader.ts:49
src/worker/api/trials.ts:55                 src/worker/lib/neg-evidence-loader.ts:156
src/worker/lib/bioactivity-loader.ts:22     src/worker/lib/neg-manifest-loader.ts:113
src/worker/lib/compound-loader.ts:107       src/worker/lib/paper-loader.ts:25
src/worker/lib/compound-loader.ts:145       src/worker/lib/repurposing-aggregator.ts:162
src/worker/lib/compound-loader.ts:184       src/worker/lib/target-loader.ts:83
src/worker/lib/compound-loader.ts:194       src/worker/lib/trial-loader.ts:17
src/worker/lib/compound-loader.ts:200       src/worker/lib/trial-loader.ts:38
src/worker/lib/compound-manifest-loader.ts:108   src/worker/lib/xref-index-loader.ts:137
src/worker/lib/compound-search.ts:93        src/worker/lib/rk15-reader-migration.test.ts:227
src/worker/lib/compound-search.ts:122       src/worker/lib/rk15-reader-migration.test.ts:233
src/worker/lib/compound-search.ts:123       src/worker/lib/rk15-reader-migration.test.ts:237
src/worker/lib/entity-resolver.ts:125
src/worker/lib/entity-resolver.ts:161

Behaviour confirmation for callers that do NOT read the discriminant. Exactly three consumers read .discriminant -- target.ts, negative-evidence.ts, repurposing-evidence.ts. Two more branch on instanceof R2ReadError without reading it (mcp-handlers.ts, and classifyThrown in failure-contract.ts), and both treat all six discriminants identically. Every other call site above is unaffected, because R2ReadError extends Error and preserves .message byte-for-byte: err instanceof Error and err.message return exactly what a bare Error returned. This was verified by direct assertion at runtime, not by inspection. Concretely, this is also why the exempt classifySourceLoadError is unchanged -- it sniffs those same message strings and still sees them.

One further typed throw was required and is reported explicitly: neg-evidence-loader.ts:153 (Legacy neg-evidence not found: ${key}) became R2ReadError('not_found', ...) with its message preserved. Under type-based dispatch that path would otherwise have moved from 404 to 500 -- a third exception to the 6e status freeze. Typing it keeps the 404 exactly where it was.

13. HTTP status is not a retryability signal

Stated plainly, as required: in this contract HTTP status is not a retryability signal. 502 carries both retryable classes (source_unavailable) and non-retryable ones (snapshot_contract); the same class appears at 502 on some routes and 503 on others; and 500 carries both snapshot_contract (false) and unclassified (true). retryable is the only retry carrier, and it is never absent.

14. npm run build

> astro build
[build] output: "static"   [build] mode: "static"
[vite] ✓ built in 907ms
[build] 1 page(s) built in 1.57s
[build] Complete!

What this establishes for this lane: nothing. astro build compiles a single static page from src/pages/index.astro. It does not bundle, compile or type-check src/worker/**. Combined with the TYPECHECK item above, no gate in this cycle compiles or type-checks this lane's code. The evidence level for this lane is "tests pass" -- not "production is fixed".

15. npm test

 Test Files  252 passed | 2 skipped (254)
      Tests  2895 passed | 13 skipped (2908)
   Duration  104.33s

Zero failures. Zero pre-existing failures to report. The same suite was run at the base SHA in this workspace before any edit and was also fully green (249 passed | 2 skipped (251), 2791 passed | 13 skipped (2804)), so the delta is exactly the three new test files and their 104 new tests. rc3b-locator-runtime passes (the core.autocrlf=input workspace prevents its failure mode) and snomed-rehydrate passes.

Verbatim output for every new test file:

 ✓ tests/worker/failure-contract.test.ts (53 tests) 229ms
   Test Files  1 passed (1)
        Tests  53 passed (53)

 ✓ tests/worker/mcp-failure-carriers.test.ts (18 tests) 74ms
   Test Files  1 passed (1)
        Tests  18 passed (18)

 ✓ tests/api/error-contract-routes.test.ts (33 tests) 38ms
   Test Files  1 passed (1)
        Tests  33 passed (33)

Reading of 9b.1, stated because it is a judgment call. "Every LIVE class on every emitting route" is implemented as: for each LIVE class, exercise it on every route that can actually produce it, and assert the serialized body. A literal "every class on every route" is unsatisfiable for the same structural reason 6b already fixed for the not-produced classes -- /compound has no shard path, so it can never construct a NegShardError, and no amount of test fixture can make it. The test additionally asserts that the union of exercised classes equals the set of LIVE classes, so no LIVE class can silently go uncovered.

Two test failures occurred during development and are recorded rather than hidden: (a) the /repurposing-evidence timeout case initially raced four parallel loaders and reported source_unavailable; fixed by serving every layer except the timing-out one; (b) two shard cases returned 200 because neg-manifest-loader's per-isolate cache is keyed by snapshot identity and a prior case had populated it; fixed by giving each sharded scenario its own snapshot date. Both were test-fixture defects, not source defects. Both are green.

16. CES

python scripts/check_compliance.py
[CES] Initiating Sciweon Compliance Enforcement Script...
[CES] English strict mode: True

[OK] CES CHECK PASSED: System is Compliant.
exit 0

Run from the repository root with python, not npm run ces (whose python3 resolves to a non-functional stub on this machine; CI is unaffected). Every new filename, all from the pre-approved list in 8c and all clean against the 14 Art 9.1 patterns:

src/worker/lib/failure-contract.ts
src/worker/lib/neg-shard-error.ts
tests/worker/failure-contract.test.ts
tests/worker/mcp-failure-carriers.test.ts
tests/api/error-contract-routes.test.ts

No .mjs was used to gain headroom. tests/worker/mcp.test.ts (250 lines, headroom 0) was not modified -- the MCP parity coverage was split into the new tests/worker/mcp-failure-carriers.test.ts rather than trimmed.

17. Byte audit -- canonical command, verbatim output

The command text was reproduced byte-identically before running: 813 B, sha256 ffdf10c301824adb311ec1454f2bc44d24e58e696db36cc66acb66fd8123ac1a -- the Founder-mandated identity. The counting logic was first validated on a known-positive control (nul=1 controls=2) and a known-negative control (nul=0 controls=0), whose expected values differ.

src/worker.ts	bytes=5607	nul=0	controls=0
src/worker/api/bioactivities.ts	bytes=3614	nul=0	controls=0
src/worker/api/compound.ts	bytes=3616	nul=0	controls=0
src/worker/api/mcp.ts	bytes=6649	nul=0	controls=0
src/worker/api/negative-evidence.ts	bytes=6376	nul=0	controls=0
src/worker/api/papers.ts	bytes=3554	nul=0	controls=0
src/worker/api/repurposing-evidence.ts	bytes=4936	nul=0	controls=0
src/worker/api/target.ts	bytes=7108	nul=0	controls=0
src/worker/api/trials.ts	bytes=3562	nul=0	controls=0
src/worker/api/xrefs.ts	bytes=4609	nul=0	controls=0
src/worker/lib/failure-contract.ts	bytes=5665	nul=0	controls=0
src/worker/lib/mcp-handlers.ts	bytes=9845	nul=0	controls=0
src/worker/lib/mcp-tools.ts	bytes=7364	nul=0	controls=0
src/worker/lib/neg-evidence-filter.ts	bytes=6280	nul=0	controls=0
src/worker/lib/neg-evidence-loader.ts	bytes=11752	nul=0	controls=0
src/worker/lib/neg-manifest-loader.ts	bytes=5749	nul=0	controls=0
src/worker/lib/neg-shard-error.ts	bytes=2643	nul=0	controls=0
src/worker/lib/neg-shard-router.ts	bytes=2702	nul=0	controls=0
src/worker/lib/r2-fetch.ts	bytes=6517	nul=0	controls=0
src/worker/lib/repurposing-aggregator.ts	bytes=8456	nul=0	controls=0
src/worker/lib/shard-codec.ts	bytes=3518	nul=0	controls=0
tests/api/error-contract-routes.test.ts	bytes=5986	nul=0	controls=0
tests/api/mcp-target.test.ts	bytes=8231	nul=0	controls=0
tests/api/target.test.ts	bytes=8155	nul=0	controls=0
tests/worker/failure-contract.test.ts	bytes=11028	nul=0	controls=0
tests/worker/mcp-failure-carriers.test.ts	bytes=8998	nul=0	controls=0
tests/worker/neg-stale-manifest-legacy-fallback.test.ts	bytes=6180	nul=0	controls=0
EXIT=0

27 changed blobs, all nul=0 controls=0, exit 0. tests/factory/umls-mrconso-probe.test.ts is unmodified and therefore does not enter the audit, so the section-10 STOP condition does not fire and no exemption was created.

Line endings: git ls-files --eol reports 779 tracked files, 779 w/lf, 0 w/crlf in the worktree after committing.

18. Per-file line counts, before and after

Counted with len(content.splitlines()), never wc -l -- the counting command was validated on a known-positive and known-negative control whose expected values differ (a file without a trailing newline: splitlines() 3 vs wc -l 2).

File before after headroom
src/worker.ts 144 144 106
src/worker/api/bioactivities.ts 88 90 160
src/worker/api/compound.ts 94 96 154
src/worker/api/mcp.ts 143 159 91
src/worker/api/negative-evidence.ts 105 132 118
src/worker/api/papers.ts 88 90 160
src/worker/api/repurposing-evidence.ts 92 109 141
src/worker/api/target.ts 145 160 90
src/worker/api/trials.ts 88 90 160
src/worker/api/xrefs.ts 101 103 147
src/worker/lib/failure-contract.ts NEW 143 107
src/worker/lib/mcp-handlers.ts 192 209 41
src/worker/lib/mcp-tools.ts 104 104 146
src/worker/lib/neg-evidence-filter.ts 135 136 114
src/worker/lib/neg-evidence-loader.ts 249 245 5
src/worker/lib/neg-manifest-loader.ts 143 154 96
src/worker/lib/neg-shard-error.ts NEW 62 188
src/worker/lib/neg-shard-router.ts 67 68 182
src/worker/lib/r2-fetch.ts 135 167 83
src/worker/lib/repurposing-aggregator.ts 193 196 54
src/worker/lib/shard-codec.ts 76 79 171
tests/api/error-contract-routes.test.ts NEW 142 108
tests/api/mcp-target.test.ts 165 168 82
tests/api/target.test.ts 171 176 74
tests/worker/failure-contract.test.ts NEW 250 0
tests/worker/mcp-failure-carriers.test.ts NEW 192 58
tests/worker/neg-stale-manifest-legacy-fallback.test.ts 124 125 125

Largest file after the change is tests/worker/failure-contract.test.ts at 250 (headroom 0; raised from 243 by the section 26 coverage correction, which offset its additions by compacting cosmetic lines in that same file). Next largest is src/worker/lib/neg-evidence-loader.ts at 245. The cap is > 250. src/worker/lib/snapshot-context.ts (250, headroom 0) and tests/worker/mcp.test.ts (250, headroom 0) are untouched. src/worker/lib/rk15-reader-migration.test.ts (240, headroom 10 -- a .test.ts living under src/worker/lib/, inside this lane's grant and collected by vitest's default include) is untouched.

19. Composed-state statement

This PR's CI did NOT run against c88c1f5. On a pull_request event actions/checkout@v4 with no ref: checks out refs/pull/N/merge, the GitHub-generated merge of this branch with main as it stood when the checks last executed. A green lane PR is therefore evidence about one lane against one base snapshot; it is NOT evidence about the composed five-lane state.

No CI run in this cycle exercises the state produced by composing Lane 2 -> Lane 1 -> Lane 4 -> Lane 3S -> Lane 3. A green single-lane PR is not a green composed state. A re-test gate on the composed branch is a Founder decision. The PR gate set is four jobs, not two: CI / test, CI / security-scan, CI / schema-validate, CES Gatekeeper / enforce-compliance; this PR touches none of the schema deny-list paths.

20. Response-version bindings

This PR does not move any response-version binding other than those owned by this lane. The composed transition table is owned by the composition gate.

Verified mechanically: none of the seven x-sciweon-rights-filter lines, none of the six F-3 binding lines (negative-evidence.ts, repurposing-evidence.ts, target.ts, xrefs.ts schema-minor, mcp.ts SERVER_INFO.version, mcp.ts x-sciweon-mcp-version) and no PROTOCOL_VERSION appears on either side of this diff. Their values are byte-identical to base (rc3a-v1, 1.2, 1.0, 0.6.0, 1.1, 2025-03-26). Their line numbers shift where surrounding code grew; no value moves.

src/worker/lib/source-rights-filter.ts (Lane 3S's) is not modified. src/worker/lib/claim-containment-*.ts and src/worker/api/health.ts do not exist at this base and are not created.

21. Known deferral for the composition gate

This branch shifts src/worker.ts line numbers by +1 above json500. The 21 worker.ts:NN citations in scripts/verify/rk15-v3c-surfaces.js are consequently stale in this branch. scripts/** is outside Lane 1's scope; the composition gate recomputes them after applying the Lane 2 and Lane 1 diffs.

22. Open PM item -- the enumerated-file source lint (9b.8)

Test 9b.8 lints an enumerated file list, never a directory walk. src/worker/api/health.ts is excluded because it does not exist at this base. Files that a LATER lane creates under src/worker/** will fall outside this lint until it is extended:

  • src/worker/api/health.ts (Lane 2) -- its "available" / "unavailable" literals sit outside this lint once the Lane 2 diff is applied in the composition tree.
  • src/worker/lib/claim-containment-*.ts (Lane 3S).
  • src/worker/lib/source-rights-filter.ts is fenced off from this lane and is deliberately not in the list, so a Lane 3S edit cannot redden a Lane 1 test.

The enumeration, not a directory walk, is what makes the lint immune to diff-application order.

23. Ordering language

:17. All Lane 2's, and the Lane 2 diff is applied before the Lane 1 diff in the composition tree.

Once the Lane 2 diff is applied in the composition tree, its "available" / "unavailable" literals sit outside this lint, as an open PM item (section 22).

24. Diff stat

 src/worker.ts                                      |  12 +-
 src/worker/api/bioactivities.ts                    |  14 +-
 src/worker/api/compound.ts                         |   4 +-
 src/worker/api/mcp.ts                              |  22 +-
 src/worker/api/negative-evidence.ts                |  57 +++--
 src/worker/api/papers.ts                           |  14 +-
 src/worker/api/repurposing-evidence.ts             |  37 +++-
 src/worker/api/target.ts                           |  55 +++--
 src/worker/api/trials.ts                           |  14 +-
 src/worker/api/xrefs.ts                            |   4 +-
 src/worker/lib/failure-contract.ts                 | 143 ++++++++++++
 src/worker/lib/mcp-handlers.ts                     |  33 ++-
 src/worker/lib/mcp-tools.ts                        |   2 +-
 src/worker/lib/neg-evidence-filter.ts              |   3 +-
 src/worker/lib/neg-evidence-loader.ts              |  22 +-
 src/worker/lib/neg-manifest-loader.ts              |  13 +-
 src/worker/lib/neg-shard-error.ts                  |  62 ++++++
 src/worker/lib/neg-shard-router.ts                 |   5 +-
 src/worker/lib/r2-fetch.ts                         |  44 +++-
 src/worker/lib/repurposing-aggregator.ts           |   5 +-
 src/worker/lib/shard-codec.ts                      |   5 +-
 tests/api/error-contract-routes.test.ts            | 142 ++++++++++++
 tests/api/mcp-target.test.ts                       |  19 +-
 tests/api/target.test.ts                           |  15 +-
 tests/worker/failure-contract.test.ts              | 250 +++++++++++++++++++++
 tests/worker/mcp-failure-carriers.test.ts          | 192 ++++++++++++++++
 .../neg-stale-manifest-legacy-fallback.test.ts     |   3 +-
 27 files changed, 1069 insertions(+), 122 deletions(-)

Every changed path is inside src/worker/**, the line-limited src/worker.ts grant, or tests/**. Nothing under scripts/**, wrangler.toml or .github/** is touched.

25. Actual CI result on this PR (superseded head a5c770d)

Section 26 carries the CI result for the current head. All four gate jobs were green on the superseded head a5c770d:

test                 pass  1m50s   CI
security-scan        pass  17s     CI
schema-validate      pass  11s     CI
enforce-compliance   pass  5s      CES Gatekeeper

Per section 19 this is evidence about ONE lane against ONE base snapshot, taken on refs/pull/292/merge. It is not evidence about the composed five-lane state.

26. Test-only coverage correction (head 390a6e4)

One new commit on top of a5c770d. It touches exactly one file,
tests/worker/failure-contract.test.ts. No production code, status, message,
failure class, existing case, base SHA or workflow file is changed. The base is
still c88c1f525d623f0b3a16715551d3a5983438e36a.

26.1 What was wrong

CASES held 34 route/class pairs and was missing two the code can genuinely
reach. The completeness assertion could not have caught it: covers every LIVE class compares CASES.map(c => c[0]) -- the CLASS column only. It proves every
class NAME appears somewhere in the matrix; a missing route/class PAIR is
invisible to it.

26.2 The two added pairs

compound x source_unavailable -> 500, with snapshots/latest.json absent
(the existing emptyBucket fixture). loadSnapshotContext runs OUTSIDE
loadTier1's try (src/worker/lib/compound-loader.ts:107; the try opens at
:109), so R2ReadError('not_found') propagates through handleCompound,
which has no catch of its own, to json500. Status is 500, not 503.

xrefs x source_unavailable -> 500, with a VALID pointer on its own date
(2026-07-04, distinct from every other fixture date) and the xref-index object
VISIBLE to head() but ABSENT from get(). xrefIndexExists
(xref-index-loader.ts:95) sees the object and takes the indexed path;
loadXrefKind then reaches fetchR2Object, whose get() returns null, raising
R2ReadError('disappeared') (r2-fetch.ts:84). It propagates through
resolveEntity and the catch-less handleXrefs to json500.

classifyThrown maps every R2ReadError discriminant to source_unavailable
(failure-contract.ts:141), so both land on the same class.

source_unavailable now covers all eight routes.
8 + 8 + 8 + 3 + 4 + 1 + 1 + 3 = 36.

26.3 Both new serialized bodies, verbatim

GET /api/v1/compound/CID:2244 with an absent latest pointer -- status 500,
retry-after null, cache-control null, headers
[["content-type","application/json"]]:

{"error":"Internal server error","failure_class":"source_unavailable","retryable":true,"detail":"A source object could not be read intact. This is a READ failure and NOT a finding that no data exists."}

GET /api/v1/xrefs?id=CHEMBL25 with the index seen by head() and absent from
get() -- status 500, retry-after null, cache-control null, headers
[["content-type","application/json"]]:

{"error":"Internal server error","failure_class":"source_unavailable","retryable":true,"detail":"A source object could not be read intact. This is a READ failure and NOT a finding that no data exists."}

Both carry failure_class: source_unavailable and retryable: true, which is
the contract table's value for that class. Neither carries a forbidden term
(checked by assertNoForbiddenTerm over stringValues(body) against the
existing 22-pattern FORBIDDEN list), and neither carries retry-after or
cache-control.

26.4 The membership oracle, as written

The 36 pairs are hand-written from the contract table and the per-route census.
They are deliberately NOT derived from CASES or from ROUTES: an expectation
computed from the thing under test holds for whatever that thing contains and
proves nothing, which is exactly the defect being corrected. The flatMap only
expands hand-authored literal rows.

    // 9b.1a - the EXACT membership oracle. These 36 pairs are hand-written from the
    // contract table and the per-route census, deliberately NOT derived from CASES:
    // an expectation computed from the thing under test proves nothing.
    it('CASES holds exactly the 36 hand-listed route/class pairs', () => {
        const want = Object.entries({
            data_layer_unconfigured: 'compound neg bio trials papers repurposing target xrefs',
            snapshot_contract: 'compound neg bio trials papers repurposing target xrefs',
            source_unavailable: 'compound neg bio trials papers repurposing target xrefs',
            parse_failed: 'bio trials papers', timeout: 'bio trials papers repurposing',
            shard_read_unavailable: 'neg', shard_manifest_invalid: 'neg', unclassified: 'compound neg target',
        }).flatMap(([cls, routes]) => routes.split(' ').map(r => `${cls}|${r}`));
        expect(want).toHaveLength(36);
        expect(CASES.map(c => `${c[0]}|${c[1]}`).sort()).toEqual([...want].sort());
    });

The pre-existing LIVE-class assertion is retained unchanged; it was never wrong,
only insufficient alone. Both assertions now stand.

Non-vacuousness, demonstrated rather than asserted. On a throwaway copy of
the file with the single line ['source_unavailable', 'xrefs', 500, xrefGone],
deleted, the run reported 1 failed | 54 passed (55) -- the only failure being
the new oracle, expected [ Array(35) ] to deeply equal [ Array(36) ]. The
LIVE-class assertion passed on that mutant. That is precisely the blindness this
correction removes. The copy was deleted; it was never staged or committed.

26.5 The 34 existing cases are untouched

Both spreads and all 34 literal tuples are byte-identical; the two new tuples
are inserted after the existing source_unavailable block and nothing is
reordered. The diff for CASES is three added lines (two cases and one
comment) and zero removed lines.

26.6 Disclosed: cosmetic compaction inside the same file

CES Art 5.1 caps every scanned file at 250 lines and this file was already at
243, leaving 7 lines of headroom against roughly 22 lines of required
additions. Rather than spill into a second file (outside the two permitted
changes), the additions were offset by compacting cosmetic regions of the SAME
permitted file: the header docblock, three comment blocks, the beforeAll
guard, the assertNoForbiddenTerm loop nesting, the bucket() head/get
statement layout, and the 9b.2 loop body. Every one is a lexical change only
-- no condition, order or value is altered -- and the full file remains green.
The file now sits at exactly 250, the cap being > 250; three other files in
the repo already sit at 250.

26.7 Verification

Affected file: 56 passed (56), up from 53 passed (53) -- the two new cases
counted in both 9b.1 and 9b.2, plus the oracle.

Full suite: 2897 passed | 13 skipped, with one failure, reported not
suppressed
: tests/factory/dailymed-adapter-incremental.test.ts > fetchIncremental early-stops on first item older than sinceToken, "Test timed
out in 5000ms". It is a pre-existing flake, unrelated to this change: run in
isolation it failed once and then passed 5 passed (5) on the next run with no
code change, and it imports only
scripts/ingestion/adapters/dailymed-adapter.js and dailymed-fetcher.js --
nothing this branch touches. CI's test job passed.

CES: [OK] CES CHECK PASSED: System is Compliant. exit 0.

Line endings: git ls-files --eol reports 779 tracked, 779 w/lf, 0 w/crlf.
The committed blob contains 0 CR bytes (git cat-file blob, per the byte-proof
rule).

All four gate jobs green on head 390a6e4:

test                 pass  2m16s   CI
security-scan        pass  26s     CI
schema-validate      pass  8s      CI
enforce-compliance   pass  7s      CES Gatekeeper

Per section 19 this remains evidence about ONE lane against ONE base snapshot,
taken on refs/pull/292/merge. It is not evidence about the composed five-lane
state.

mosesy5688-cell and others added 2 commits September 3, 2026 08:19
…alse promises, no leaks)

Stop telling the public WHEN a failure will clear or WHAT future process will
fix it; stop leaking internal identifiers through unexpected errors; keep
telling consumers WHETHER a failure class is of a kind a retry could resolve.

- one exported failure contract (src/worker/lib/failure-contract.ts): nine
  classes -> {retryable, default_detail, produced}; every emitting path
  constructs from it, so `retryable` is never absent
- classify at the throw site BY TYPE (never by message): r2-fetch.ts gains a
  typed R2ReadError with six structural discriminants; five bare producer-data
  throws plus a new manifest shape guard become ShardDataInvalidError
- NegShardError extracted to src/worker/lib/neg-shard-error.ts and split by the
  type of its cause into shard_read_unavailable / shard_manifest_invalid
- a transport fault is no longer routed into a domain 404 (target route) nor
  into a 200 resolved:false MCP payload; every other status and JSON-RPC code
  is unchanged
- MCP carriers travel in error.data; the message carries prose only
- the four false "next factory-1 cron will produce it" promises are gone,
  including the one served on every tools/list call
- no public body echoes an underlying error message any more

Tests: per-class serialization on every emitting route, a declared-not-produced
guard, the thirteen-surface observer census, MCP error/result/tools-list
parity, a leak sentinel, and an enumerated-file source lint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c1bKNiDv8zfaPunCXUANq
… contract

The 9b.1 matrix carried 34 route/class pairs and missed two that the code can
genuinely reach, and its completeness assertion could not have noticed:
`covers every LIVE class` compares CASES.map(c => c[0]) -- the CLASS column
only. It proves every class NAME appears somewhere; a missing route/class PAIR
is invisible to it.

- add compound x source_unavailable -> 500. With snapshots/latest.json absent,
  loadSnapshotContext runs OUTSIDE loadTier1's try (compound-loader.ts:107), so
  R2ReadError('not_found') propagates through the catch-less /compound handler
  to json500.
- add xrefs x source_unavailable -> 500. With a VALID pointer on its own date
  and the xref index visible to head() but absent from get(), xrefIndexExists
  takes the indexed path and loadXrefKind raises R2ReadError('disappeared'),
  which reaches json500 through the catch-less /xrefs handler.
- add an EXACT route/class membership oracle. Its 36 pairs are hand-written
  from the contract table and the per-route census, deliberately NOT derived
  from CASES: an expectation computed from the thing under test holds for
  whatever that thing contains and proves nothing. Deleting any single pair
  from CASES now fails this assertion -- and only this one.

The pre-existing LIVE-class assertion is retained unchanged; it was never
wrong, only insufficient on its own. All 34 existing cases, both spreads
included, are byte-identical.

source_unavailable now covers all eight routes: 8 + 8 + 8 + 3 + 4 + 1 + 1 + 3
= 36. Both new serialized bodies carry failure_class source_unavailable and
retryable true, no forbidden vocabulary and no retry-after / cache-control
header.

Test-only. No production code, status, message, failure class or existing case
is changed. Cosmetic line compaction inside this same file keeps it within the
CES 250-line monolith cap (Art 5.1); no compacted region changes behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c1bKNiDv8zfaPunCXUANq
mosesy5688-cell added a commit that referenced this pull request Sep 5, 2026
* replay #291 deployment identity (BASE..229e4e5, verbatim)

* replay #292 public error contract (BASE..390a6e4, verbatim; worker.ts 3-way merged with #291)

* replay #293 rights candidate registry (BASE..c11e3b5, verbatim)

* replay #294 public claim containment (BASE..73613b0, verbatim; 5 api files 3-way merged with #292)

* replay #295 merge integrity (BASE..c49c1db, verbatim)

* composition gate: F-3 version bindings, CI identity step, gate tests

Composition of the five Founder-accepted lanes (#291 #292 #293 #294 #295)
replayed from BASE c88c1f5 in the frozen order. This commit isolates
everything the composition gate itself authored.

F-3 response-contract bindings (MONOTONIC RESPONSE-CONTRACT MARKERS, not
product SemVer claims -- they exist so a cached pre-composition body is
distinguishable from a post-composition one at the serving boundary):
  negative-evidence.ts    x-sciweon-schema-minor  1.2   -> 1.3
  repurposing-evidence.ts x-sciweon-schema-minor  1.0   -> 1.1
  target.ts               x-sciweon-schema-minor  0.6.0 -> 0.6.1
  xrefs.ts                x-sciweon-schema-minor  1.1   -> 1.2
  mcp.ts                  SERVER_INFO.version     0.6.0 -> 0.6.1
  mcp.ts                  x-sciweon-mcp-version   0.6.0 -> 0.6.1
PROTOCOL_VERSION (2025-03-26) deliberately UNCHANGED.

CI: one read-only identity-recording step in the EXISTING test job. No new
job, no widened permissions. continue-on-error keeps a token/API failure
from turning CI red; the gate treats any absent value as a qualification
failure.

Tests: version-sync (SERVER_INFO.version and the x-sciweon-mcp-version
header are two distinct bindings; nothing else prevents them diverging),
plus the per-surface containment matrix including the repurposing
classification lane 3S did not individually verify.

KNOWN RED, NOT FIXED HERE: tests/api/target.test.ts:174 asserts
x-sciweon-schema-minor === '0.6.0' and now fails against the mandated
'0.6.1'. That file is lane #292's accepted content and is one of the 50
files required to stay byte-identical, so the composition gate is not
authorised to change it. Escalated for a founder ruling.

* test(composition): correct stale target schema-minor oracle; add three REST runtime probes

Bounded Correction 1 to the five-lane composition gate. Founder ruling: the
frozen F-3 contract target.ts = '0.6.1' is CORRECT; tests/api/target.test.ts
was a STALE ORACLE still expecting '0.6.0'. The oracle is corrected; the
contract is NOT rolled back.

The corrected assertion remains a real probe: it still calls handleTarget(),
still reads x-sciweon-schema-minor off the real Response, and still compares
by exact equality. Only the wrong expected literal changed.

Closes three REST runtime-coverage gaps. Before this commit the four REST
schema-minor bindings were pinned only by source-text scans in
tests/worker/composition-version-bindings.test.ts; now each is also asserted
against a live handler response:

  target            0.6.1  tests/api/target.test.ts
  negative-evidence 1.3    tests/api/composition-surface-matrix.test.ts
  xrefs             1.2    tests/api/composition-surface-matrix.test.ts
  repurposing       1.1    tests/api/composition-containment-matrix.test.ts

The two MCP bindings (SERVER_INFO.version, x-sciweon-mcp-version) already had
real handleMcp() runtime probes and are unchanged.

Qualifies the Tier-2 matrix row so removed_key_count = 6 reads as a
wiring-capacity measurement over an artificially injected fixture, not as a
property of production PubChem Tier-2 data.

Comment-only corrections to two stale bump annotations; no emitted value, no
rights-filter marker and no code changed:

  negative-evidence.ts  "bumped 1.1 -> 1.2"  ->  "bumped 1.2 -> 1.3"
  xrefs.ts              "bumped 1.0 -> 1.1"  ->  "bumped 1.1 -> 1.2"

Additive commit; five files; no lane content altered. PROTOCOL_VERSION
('2025-03-26') untouched. No merge, no deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019c1bKNiDv8zfaPunCXUANq

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mosesy5688-cell

Copy link
Copy Markdown
Owner Author

Closed without merge. This exact lane head was accepted only as an
input to composition PR #296. Its approved content entered main solely
through #296, squash commit
0e8a7f7,
deployed by automatic push run 33944482862.

This PR was never merged or deployed independently. Do not reopen,
merge, cherry-pick, amend, or use it as an execution base.
The source branch is retained unchanged as audit evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant