Skip to content

feat(lane3): merge integrity -- cross-cycle claim preservation on the compound axis (DO NOT MERGE INDIVIDUALLY) - #295

Closed
mosesy5688-cell wants to merge 1 commit into
mainfrom
lane3/merge-integrity-attempt-1
Closed

feat(lane3): merge integrity -- cross-cycle claim preservation on the compound axis (DO NOT MERGE INDIVIDUALLY)#295
mosesy5688-cell wants to merge 1 commit into
mainfrom
lane3/merge-integrity-attempt-1

Conversation

@mosesy5688-cell

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

Copy link
Copy Markdown
Owner

Lane 3 -- Merge Integrity (independent rebuild)

Preserves cross-cycle conflicting values at the compound deep-merge point.
deepMergeCompound is composed, never modified, so winner invariance is
structural rather than merely tested.

DO NOT MERGE INDIVIDUALLY. No individual lane PR merges or deploys; the
five-lane composition gate applies all five diffs.


Required attestations

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.

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.

10.18 composed-state statement. 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 PR is not a green composed state. A re-test gate on the composed branch
is a Founder decision.

10.2 TYPECHECK = NOT CONFIGURED / NOT CLAIMED. Per D-1 no typecheck is
required, no prep PR and no temporary compiler. Additionally tsconfig.json
excludes scripts/**, so a typecheck could not have covered this lane's .js
modules even if one existed -- though it would have covered this lane's
tests/factory/*.test.ts.


10.1 Base proof

git rev-parse HEAD      c49c1dbf5c5778243d402f18580ab0df086e4e63
git log -1 --format=%P  c88c1f525d623f0b3a16715551d3a5983438e36a   (single parent = BASE)
git merge-base HEAD c88c1f5...   c88c1f525d623f0b3a16715551d3a5983438e36a
git log --oneline c88c1f5..HEAD
  c49c1db feat(lane3): merge integrity -- cross-cycle claim preservation on the compound axis

git ls-remote origin refs/heads/main was c88c1f52... before the first edit,
immediately before commit, and immediately after push.

10.3 The six key names are absent from main at base

git grep -c <name> c88c1f5 -- . returns 0 files for every one of
competing_claims, preserved_against_null, field_sources,
claim_set_state, claim_overflow_fields, claim_overflow_counts.
Greenfield against main.

10.4 Zero diff on the oracle

git diff c88c1f5 -- scripts/factory/lib/aggregated-deep-merge.js prints
nothing (0 bytes of output). The file is not in the changed-file list.

10.5 The four frozen paths as implemented

export const CLAIMABLE_PATHS = Object.freeze([
    'inchi_key',
    'smiles_canonical',
    'molecular_formula',
    'iupac_name',
]);

Byte-identical to 3a. id and pubchem_cid are excluded and closed.

10.6 The line ledger AS EXECUTED

Step Change Delta Running
0 baseline -- 250
1 inline 4-line counter literal (:127-130) -> 1-line makeDeepMergeCounters() call -3 247
2 factory name added to the existing import line (:95) +0 247
3 one new import line for the claims module +1 248
4 compound strategy-map value changed in place (:97-100) +0 248
5 six one-per-line merged_deep_* stats (:169-174) packed into three -3 245
6 one stats line merged_deep_claims: deepCounters.claims, +1 246
7 let revalidateStats = null; +1 247
8 the 3j call inside the existing fname branch (:227-229) +1 248
9 one drop-count stat inside the existing if (bootstrapStats) block +1 249

Measured final: scripts/factory/lib/aggregated-merger.js = 249 lines,
headroom 1.
Step 3 costs only one line because revalidatePrevClaims is
re-exported from merge-claims-wrapper.js. Step 2 also drops the now-unused
deepMergeCompound name from that import (zero line delta). A zero-delta
comment extension on the existing :225 line explains the 3j call in place.

10.7 Winner invariance

23 assertions in tests/factory/merge-claims-differential.test.ts over the
full 4a corpus -- both sides populated and equal; both populated and different;
incoming null; incoming ''; incoming explicit undefined; previous null;
both null; key missing on either side; a key present on current with an
explicit undefined; keys only on prev; prev already carrying all six
keys while current carries none
; plus the E-1 split per path. Each case
strips the six keys from BOTH sides, then asserts deep equality, key-order
identity and canonical-string identity. All pass.

10.8 E-1 evidence, per path (emitted records)

inchi_key          winner="BSYNRYMUTXBXSQ-UHFFFAOYSA-N"  ->  preserved_against_null
   {"path":"inchi_key","value":"BSYNRYMUTXBXSQ-UHFFFAOYSA-N"}          field_sources: undefined
smiles_canonical   winner="CC(=O)OC1=CC=CC=C1C(=O)O"     ->  preserved_against_null
   {"path":"smiles_canonical","value":"CC(=O)OC1=CC=CC=C1C(=O)O"}      field_sources: undefined
molecular_formula  winner=null                            ->  competing_claims
   {"path":"molecular_formula","value":"C9H8O4","side":"previous","source":{"source":null,"status":"unknown"}}
   field_sources: {"molecular_formula":{"source":null,"status":"unknown"}}
iupac_name         winner=null                            ->  competing_claims
   {"path":"iupac_name","value":"2-acetyloxybenzoic acid","side":"previous","source":{"source":null,"status":"unknown"}}
   field_sources: {"iupac_name":{"source":null,"status":"unknown"}}

For the two structural paths the winner IS the previous value, so condition 4
holds and the event is a preservation. For the other two the winner is null,
condition 4 fails, and the previous value becomes a claim -- which is exactly
what happened to it.

10.9 Overflow: no throw, skip-and-continue, 3e shapes, stickiness

Per-field cap counted across both containers (4 claims + 4 preserved on
inchi_key = 8):

claim_set_state       : CLAIM_SET_INCOMPLETE_OVERFLOW
claim_overflow_fields : ["inchi_key"]
claim_overflow_counts : {"inchi_key":1}
Object.keys(counts) === fields : true
skip-and-continue, admitted on another path:
  [{"path":"iupac_name","value":"2-acetyloxybenzoic acid","side":"previous","source":{"source":null,"status":"unknown"}}]
counters: {"records":1,"claims_kept":5,"preserved_kept":4,"revalidate_drops":0,
           "nonfinite_rejected":0,"overflow_refusals":1,"overflow_records":1,
           "sample":[{"id":"sciweon::compound::CID:2244","path":"inchi_key"}]}
STICKY next merge: refusals=0  state=CLAIM_SET_INCOMPLETE_OVERFLOW  counts={"inchi_key":1}

The sample carries a record id and an allow-listed path and no value.
Cap-1 / cap / cap+1 are asserted for both caps; the record cap is exercised in
isolation with the path far below the field cap.

F-1 cross-cycle composition -- inherited 3 against a current-merge refusal
of 1 on the SAME path yields {"molecular_formula":3}. MAX = 3; SUM would give
4; overwrite would give 1.

10.10 Inherited re-validation, both containers, and the 3j boundary pass

revalidate_drops: 8
competing_claims kept : [{"path":"molecular_formula","value":"C9H8O5","side":"previous","source":{...}}]
preserved kept        : [{"path":"inchi_key","value":"KEEPKEYAAAAAAA-UHFFFAOYSA-N"}]
field_sources kept    : {"molecular_formula":{"source":null,"status":"unknown"}}

Dropped: a non-allow-listed path, a wrong shape, a null value, a non-finite
value, a dedup collision, a non-allow-listed preserved path, a wrong preserved
shape, and an illegal SourceRef. preserved_against_null is validated on
shape / allow-list / legality / dedup only -- never re-derived against
today's winner.

3j prev-load-boundary pass over a prev-only record:

stats: {"scanned":2,"dropped":3,"records_cleaned":1,"sample":["sciweon::compound::CID:9999"]}
winners unchanged: true

It never creates a container key on a record that has none, and never touches
the three sticky overflow keys.

10.11 No container- or record-level source promoted

field_sources     : {"molecular_formula":{"source":null,"status":"unknown"}}
every claim.source: {"source":null,"status":"unknown"}

Every field_sources entry this lane writes is {source: null, status: "unknown"}; the status: "known" branch is dead code in schema v1.0 and is
never written. A record carrying provenance.primary_source: "pubchem" and
external_ids.sources: ["unichem"] promotes neither.

10.12 Recovery path, and the pre-existing defect

scripts/factory/recovery-merge-all.js now exports the pure
recoveryMergeCompoundRecords(runRecords, accumulatedRecords) and wraps
main() in an import.meta.url === pathToFileURL(process.argv[1]).href guard.
Direct script execution behaviour is unchanged. The compound entry is wired
explicitly (fname === COMPOUND_FILE); the natural
MERGE_STRATEGY_PER_FILE[fname] wiring is NOT used, so the drug-label axis
stays unwired. A test asserts that against comment-stripped source.

PRE-EXISTING DEFECT, REPORTED AND NOT FIXED. That file resolves its key
function without a fallback (KEY_FN_PER_FILE[fname] at what is now :152)
while MERGE_FILES contains drug-labels.jsonl, which the key map lacks. The
throw is conditional: the loop continues on a missing object and the key
function is only invoked for a non-empty record list, so it fires only when a
historical bundle actually holds a non-empty drug-labels file. The compound
file is first in MERGE_FILES, so the compound strategy is reached before any
such throw. Fixing it was NOT necessary to wire the compound strategy.

THE RECOVERY TOOL WAS NOT RUN. R2 WAS NOT ACCESSED. No client was created,
no credential read, no object uploaded and no latest pointer advanced at any
point. All four R2_* environment variables were unset throughout.

10.13 npm run build

astro build completed: 1 page built in 1.38s. It is astro build over a
single static page and is entirely inert for scripts/**. Combined with
D-1, no gate compiles or type-checks this lane's modules; npm test is the
only verification.

10.14 npm test

Test Files  253 passed | 2 skipped (255)
     Tests  2895 passed | 13 skipped (2908)
  Duration  111.02s          exit code 0

Zero failures. The four new files: merge-claims-preservation 20 tests,
merge-claims-canonical-parity 44, merge-claims-differential 23,
merge-claims-overflow 17 -- 104 passed, 0 failed. rc3b-locator-runtime
and snomed-rehydrate both pass; the core.autocrlf=input workspace prevented
the CRLF failure entirely.

10.15 CES

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

[OK] CES CHECK PASSED: System is Compliant.

New filenames, all from the seven pre-approved names, all clean against the 14
Art 9.1 patterns and none beginning rights-:

scripts/factory/lib/merge-claims-canonical.js        185 lines
scripts/factory/lib/merge-claims-revalidate.js       158 lines
scripts/factory/lib/merge-claims-wrapper.js          246 lines
tests/factory/merge-claims-differential.test.ts      212 lines
tests/factory/merge-claims-preservation.test.ts      243 lines
tests/factory/merge-claims-overflow.test.ts          249 lines
tests/factory/merge-claims-canonical-parity.test.ts  158 lines

10.16 Byte audit (canonical command, section 11)

Command identity verified before use: 813 bytes, sha256
ffdf10c301824adb311ec1454f2bc44d24e58e696db36cc66acb66fd8123ac1a -- matching
the Founder-mandated value exactly.

scripts/factory/lib/aggregated-merger.js	bytes=10176	nul=0	controls=0
scripts/factory/lib/merge-claims-canonical.js	bytes=7032	nul=0	controls=0
scripts/factory/lib/merge-claims-revalidate.js	bytes=5965	nul=0	controls=0
scripts/factory/lib/merge-claims-wrapper.js	bytes=11254	nul=0	controls=0
scripts/factory/lib/stage-3-merge.js	bytes=8415	nul=0	controls=0
scripts/factory/recovery-merge-all.js	bytes=8797	nul=0	controls=0
tests/factory/merge-claims-canonical-parity.test.ts	bytes=7179	nul=0	controls=0
tests/factory/merge-claims-differential.test.ts	bytes=9952	nul=0	controls=0
tests/factory/merge-claims-overflow.test.ts	bytes=12279	nul=0	controls=0
tests/factory/merge-claims-preservation.test.ts	bytes=11431	nul=0	controls=0
exit code: 0

Every changed committed blob reports nul=0 controls=0. The dedup separator is
written as the escape '\^@'; there is no literal NUL byte on disk.
tests/factory/umls-mrconso-probe.test.ts is NOT in this diff and therefore
never enters the audit.

10.17 Per-file line counts, splitlines() semantics (never wc -l)

File Before After Headroom
scripts/factory/lib/aggregated-merger.js 250 249 1
scripts/factory/lib/stage-3-merge.js 93 127 123
scripts/factory/recovery-merge-all.js 186 213 37
scripts/factory/lib/merge-claims-canonical.js NEW 185 65
scripts/factory/lib/merge-claims-revalidate.js NEW 158 92
scripts/factory/lib/merge-claims-wrapper.js NEW 246 4
tests/factory/merge-claims-differential.test.ts NEW 212 38
tests/factory/merge-claims-preservation.test.ts NEW 243 7
tests/factory/merge-claims-overflow.test.ts NEW 249 1
tests/factory/merge-claims-canonical-parity.test.ts NEW 158 92
scripts/factory/lib/aggregated-deep-merge.js 162 162 zero diff

Workspace: git ls-files --eol reports 781 w/lf, 0 w/crlf. Disk bytes
equal index blob bytes (raw sha256sum against git cat-file blob) on every
file sampled, and
sha256(scripts/rc3b-audit/locator-artifact-schema.json) on disk is
33e65017a28e77c54e2c819ffb1b7238deaed03aa95be09a75e29d9daabc8837.


Design notes the reviewer should check

stage-3-merge.js now EMITS the counters. Its per-file printing was the
only consumer of the stats object and printed a hardcoded key list, so a new
merged_deep_claims key would have been added to an object and then discarded
-- a permanent claim loss would have produced no CI output at all. The printing
is extracted into an exported pure logMergeStatsPerFile(perFile) so a test can
assert a non-zero refusal count with no client, no credential and no R2. That
test is green.

The F-1 composition rule is exported as composeOverflow. The frozen L3-1
cases -- inherited 3 / current 2 and inherited 2 / current 3 on the SAME path --
are NOT reachable end to end: under schema v1.0 all four paths are top-level
scalars, so the winner is always one of the two sides' own values and at most
one new candidate per path survives the equal-to-winner filter. A single merge
can therefore refuse at most one item per path. The frozen numbers are
exercised directly against the rule, and end-to-end merges cover the reachable
same-path and disjoint-path cases. Only MAX satisfies all three cases: SUM
fails both same-path cases (5 vs 3), keep-inherited fails the second (2 vs 3),
and overwrite fails the first and third. The disjoint case alone passes under
SUM and keep-inherited, which is why all three are required.

Scope. Zero diff on aggregated-deep-merge.js; nothing under src/**,
.github/**, wrangler.toml, or any other scripts/** path. No third
strategy-map key -- the map still has exactly two keys and the compound value
was changed in place. No value-identity assertion on the compound entry was
added. No file whose basename begins rights- was created.


Process-boundary disclosure

During evidence preparation, the executor wrote one temporary diff file to
Git-Bash /tmp/lane3dm.txt instead of the lane-specific scratch directory and
then deleted it. No other lane file was read or modified, and the temporary
file never entered a repository blob or commit. This was a confirmed
scratch-isolation procedure violation. Independent verification found no
effect on the accepted tree.

Accepted implementation judgments

The frozen same-path overflow numbers are not constructible through one current
end-to-end merge because the four governed paths are top-level scalars and the
winner filter admits at most one new refused candidate per path. The production
path uses composeOverflow directly; the frozen max rule is tested at that pure
boundary, while reachable cases remain end-to-end tested.

For testability and the 250-line cap, logMergeStatsPerFile was extracted as a
pure function, three canonicalisation helpers were relocated, and existing
assertions were moved between pre-approved test files. No assertion was removed
and no tested semantics were weakened.

… compound axis

Preserves cross-cycle conflicting values at the compound deep-merge point.
deepMergeCompound is COMPOSED, never modified: aggregated-deep-merge.js
carries a zero diff, so winner invariance is structural rather than merely
tested. Six frozen top-level keys are emitted, and nothing else.

E-1 split, per path:
  inchi_key / smiles_canonical    winner IS the previous value
                                  -> preserved_against_null
  molecular_formula / iupac_name  incoming null wins and the previous value
                                  is destroyed -> competing_claims

New modules (all seven pre-approved filenames):
  scripts/factory/lib/merge-claims-canonical.js    canonicalisation, equality
  scripts/factory/lib/merge-claims-revalidate.js   3h re-validation + 3j pass
  scripts/factory/lib/merge-claims-wrapper.js      the compound strategy

Wiring, against the frozen ten-step ledger:
  aggregated-merger.js  250 -> 249 lines, headroom 1
  stage-3-merge.js      emits the claims counters and bounded samples; the
                        only consumer printed a hardcoded key list, so a new
                        counter would have been silently discarded
  recovery-merge-all.js exports a pure compound-recovery merge and wraps
                        main() in an import.meta.url guard (F-2). Compound
                        axis only; the drug-label axis stays unwired.

Overflow is fail-soft-LOUD: never throws, never process.exit, never silently
truncates. Admission is skip-and-continue. The three overflow keys are sticky
and compose cross-cycle by F-1: union of fields, per-path MAX of counts.

The pre-existing recovery key-function defect (drug-labels.jsonl has no key
map entry, conditional throw) is REPORTED, NOT FIXED.

Tests: 104 new assertions across four files; full suite 2895 passed / 13
skipped; CES passed; astro build green.

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