fix(lock-ts): normative 512 nesting cap + verify() error contract (DSE-1527) - #102
Merged
Merged
Conversation
…E-1527) The TypeScript verifier refused JSON nested past 512 levels while the Python reference accepted anything up to the interpreter's ~1000-frame recursion limit, so two conforming implementations disagreed on the same document (CSO re-verify of #99, N1). SPEC.md §4 now names 512 as the normative bound; canon() and read_lock() enforce it with an explicit iterative check raising DepthError (a ValueError); vectors gain canonical/depth-512-accepted and malformed/depth-513-rejected so both harnesses pin the exact bound. verify() in @mcp-warden/lock now throws only LockFormatError (N2): a canonicalizer or depth failure on the observed surface is wrapped rather than leaking as JcsError/DepthError. The Python vector harness routes every malformed lock through read_lock(), the public reader.
… MAX_CANON_DEPTH, narrowed verify() catch - read_lock(): a >~1000-deep file raises RecursionError from json.loads on 3.11 before the depth check runs; map it to the documented ValueError. New vector malformed/lock-depth-1200-rejected (raw text) pins that BOTH readers refuse it. - hashing.MAX_JSON_DEPTH -> MAX_CANON_DEPTH, distinct from content_models. MAX_JSON_DEPTH (16, content-envelope profile); SPEC §4 scopes the 512 bound. - verify() folds only JcsError/DepthError into LockFormatError, with `cause`; anything else is a programming error and is not masked.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two Low residuals from the CSO re-verification of #99 (DSE-1527).
N1 — the two implementations did not share a nesting bound.
@mcp-warden/lockrefused JSON nested past 512 levels; the Python reference accepted anything up to the interpreter's ~1000-frame recursion limit (probed on the builder: 513–990 levels canonicalized fine, ~1000 raised). Two conforming implementations disagreed on the same document.N2 —
DepthErrorcould escapeverify()untyped, contradicting the "throwsLockFormatError" contract.What changed
docs/SPEC.md§4: normative nesting bound = 512 (root = 0, each enclosing array/object adds one); relying on a host recursion limit is explicitly non-conformant. §12.1 wording aligned.hashing.py:MAX_CANON_DEPTH(distinct fromcontent_models.MAX_JSON_DEPTH = 16, the content-envelope profile's bound), typedDepthError(ValueError), iterativecheck_depth();canon()checks before serializing.lockfile.read_lock(): depth check before schema validation, intelligibleValueErrormessage.vectors/:canonical/depth-512-accepted(513 arrays → deepest at 512, MUST canonicalize) andmalformed/depth-513-rejected(514 arrays, MUST be refused). Regenerated deterministically — only the two new cases +manifest.jsonchanged. Count 84 → 87 (14 canonical / 9 digest / 50 drift / 14 malformed).packages/lock-tsverify(): wrapsbuildFromSurface+computeDrift; throws onlyLockFormatError(aJcsError/DepthErroron the observed side no longer leaks). README table updated.malformedlock now goes throughread_lock()(the public reader) instead ofWardenLock.model_validate, asvectors/README.mdstep 4 asks of every implementation.test_hashing.py(512 ok / 513 refused, leaf counts, objects count, iterative check survives 5000-deep) andtest_lockfile.py(control read, hostile read refused with the depth message, at-bound read never trips the depth check); TS tests for the exact canonicalizer bound and theverify()error contract (deep surface + unpaired surrogate).Acceptance criteria
canon/read_lockreject depth > 512 with a typed error; TS unchanged (same constant).verify()never throws anything butLockFormatError; covered by a test.malformedthroughread_lock.Verification (dse-builder-01, py3.12, node 22)
test_hashing+test_lockfile+test_spec_vectors→ 114 passedruff check .cleannpm ci+ the package test script → 97/97vectors/tools/generate.pyregeneration: byte-identical for all 84 pre-existing cases.CSO review fold-ins (GREEN at
6007c40; folded at4e2764e)read_lock()could surface a bareRecursionErrorfor a file nested past the interpreter's ~1000-frame limit (3.11), beforecheck_depth()ever ranexcept (json.JSONDecodeError, RecursionError)→ the documentedValueErrormalformed/lock-depth-1200-rejected(rawlock_text, 1200 deep) — Python viaread_lock(), TS viaparseLock/verify— both harnessescontent_models.MAX_JSON_DEPTH(16, content-envelope profile)MAX_CANON_DEPTH; SPEC §4 gains a sentence scoping the 512 bound to lock/surface canonicalizationtest_hashing/test_lockfileupdatedverify()catch was broad enough to mask programming errors;DepthErrorimport in the TS test was already present but not visible in the diffJcsError/DepthError, re-thrown asLockFormatErrorwith{ cause }; anything else propagatescause instanceof JcsError🤖 Generated with Claude Code
https://claude.ai/code/session_01Lu4ZLyWWL7TXMGYpNFU5Dc