From 5d655f2beab569ac804890493bfafcf6045caabe Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:58:58 +0200 Subject: [PATCH 1/3] Release failed Base64 scratch and extend 1.1 acceptance coverage --- Scripts/ci.py | 4 +- Sources/JSON/Coding/JSONBase64.swift | 50 ++-- .../JSONPatchTests/AcceptanceWaveTests.swift | 36 +++ .../JSONSchemaTests/AcceptanceWaveTests.swift | 55 ++++ .../AcceptanceWaveTests.swift | 61 ++++ .../Release1_1/AcceptanceWaveTests.swift | 267 ++++++++++++++++++ .../Sources/ProbeCases/PublicBehavior.swift | 34 +++ 7 files changed, 484 insertions(+), 23 deletions(-) create mode 100644 Tests/JSONPatchTests/AcceptanceWaveTests.swift create mode 100644 Tests/JSONSchemaTests/AcceptanceWaveTests.swift create mode 100644 Tests/JSONStreamingTests/AcceptanceWaveTests.swift create mode 100644 Tests/JSONTests/Release1_1/AcceptanceWaveTests.swift diff --git a/Scripts/ci.py b/Scripts/ci.py index d0925f0..4e0f19d 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -6,7 +6,7 @@ from preflight import collect ROOT = pathlib.Path(__file__).resolve().parents[1] -SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks' +SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -71,7 +71,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 2] if 'thread' in command else [9, 11, 12, 38, 86] + expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 3] if 'thread' in command else [11, 12, 14, 41, 97] passed = passed and sorted(counts) == expected if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) diff --git a/Sources/JSON/Coding/JSONBase64.swift b/Sources/JSON/Coding/JSONBase64.swift index 9124c98..63922b3 100644 --- a/Sources/JSON/Coding/JSONBase64.swift +++ b/Sources/JSON/Coding/JSONBase64.swift @@ -30,28 +30,36 @@ package enum JSONBase64 { return (text,count) } package static func decode(_ string: String,meter: inout JSONWorkMeter) throws -> Data { - var iterator = string.utf8.makeIterator(), bytes: [UInt8] = [] - func malformed() -> JSONFailure { .init(.syntax,code: .invalidSyntax) } - func emit(_ byte: UInt8,meter: inout JSONWorkMeter) throws { - try meter.work(); try meter.retain(1); bytes.append(byte) - } - while let a = iterator.next() { - try meter.work() - guard let b = iterator.next(),let c = iterator.next(),let d = iterator.next(),let x = value(a),let y = value(b) else { throw malformed() } - try emit(x << 2 | y >> 4,meter: &meter) - if c == 61 { - guard d == 61, y & 15 == 0, iterator.next() == nil else { throw malformed() }; break + let before = meter.used(.retainedBytes) + do { + var iterator = string.utf8.makeIterator(), bytes: [UInt8] = [] + func malformed() -> JSONFailure { .init(.syntax,code: .invalidSyntax) } + func emit(_ byte: UInt8,meter: inout JSONWorkMeter) throws { + try meter.work(); try meter.retain(1); bytes.append(byte) } - guard let z = value(c) else { throw malformed() } - try emit(y << 4 | z >> 2,meter: &meter) - if d == 61 { guard z & 3 == 0, iterator.next() == nil else { throw malformed() }; break } - guard let w = value(d) else { throw malformed() } - try emit(z << 6 | w,meter: &meter) + while let a = iterator.next() { + try meter.work() + guard let b = iterator.next(),let c = iterator.next(),let d = iterator.next(),let x = value(a),let y = value(b) else { throw malformed() } + try emit(x << 2 | y >> 4,meter: &meter) + if c == 61 { + guard d == 61, y & 15 == 0, iterator.next() == nil else { throw malformed() }; break + } + guard let z = value(c) else { throw malformed() } + try emit(y << 4 | z >> 2,meter: &meter) + if d == 61 { guard z & 3 == 0, iterator.next() == nil else { throw malformed() }; break } + guard let w = value(d) else { throw malformed() } + try emit(z << 6 | w,meter: &meter) + } + for _ in bytes { try meter.work() } + try meter.retain(UInt64(bytes.count)); try meter.checkpoint() + let result = Data(bytes) + try meter.checkpoint(); try meter.release(UInt64(bytes.count)) + return result + } catch { + // No result owns failed scratch. Refund live bytes, never consumed + // work or terminal health, so ordinary corruption can be handled. + try meter.release(meter.used(.retainedBytes) - before) + throw error } - for _ in bytes { try meter.work() } - try meter.retain(UInt64(bytes.count)); try meter.checkpoint() - let result = Data(bytes) - try meter.checkpoint(); try meter.release(UInt64(bytes.count)) - return result } } diff --git a/Tests/JSONPatchTests/AcceptanceWaveTests.swift b/Tests/JSONPatchTests/AcceptanceWaveTests.swift new file mode 100644 index 0000000..b90f843 --- /dev/null +++ b/Tests/JSONPatchTests/AcceptanceWaveTests.swift @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +import JSONPatch + +@Test func qaWidePatchEditsPreserveExactKeyOrderThroughIndexTransitions() throws { + for width in [31, 32, 33, 63, 64, 65] { + let members = (0.. 0) + } + // Invalid evaluation must not mutate the reused immutable plan. + #expect(try JSONSchemaValidator().validate(value, against: plan).isValid) + } +} + +@Test func qaRepeatedPatternSearchesShareAggregateQuotaAndRecoverOnNewInvocation() throws { + let schema = try JSONParser().parse(#"{"patternProperties":{"^k[0-9]+$":true}}"#) + let plan = try JSONSchemaCompiler().compile(JSONSchemaDocument(schema)) + let instance = JSON.object(try JSONObject(members: (0..<64).map { .init("k\($0)", value: .null) })) + let report = try JSONSchemaValidator().validateWithReport(instance, against: plan) + #expect(report.result.isValid && report.statistics[.aggregateRegexWorkUnits] > 0) + var limits = JSONLimits.balanced + limits[.aggregateRegexWorkUnits] = report.statistics[.aggregateRegexWorkUnits] + #expect(try JSONSchemaValidator(options: .init(limits: limits)).validate(instance, against: plan).isValid) + limits[.aggregateRegexWorkUnits] -= 1 + do { _ = try JSONSchemaValidator(options: .init(limits: limits)).validate(instance, against: plan); Issue.record("Exhaustion became a verdict") } + catch let failure as JSONFailure { #expect(failure.category == .resource && failure.resource?.metric == .aggregateRegexWorkUnits) } + #expect(try JSONSchemaValidator().validate(instance, against: plan).isValid) +} diff --git a/Tests/JSONStreamingTests/AcceptanceWaveTests.swift b/Tests/JSONStreamingTests/AcceptanceWaveTests.swift new file mode 100644 index 0000000..e231a37 --- /dev/null +++ b/Tests/JSONStreamingTests/AcceptanceWaveTests.swift @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONStreaming + +@Test func qaWideStreamEverySplitPreservesPayloadLexemesAndScalarKeys() async throws { + let fields = (0..<33).map { "\"k\($0)\":\($0)" }.joined(separator: ",") + let text = "{\(fields),\"é\":9007199254740993,\"é\":-0.00e+2,\"🚀\":0.1}" + let expected = Data(text.utf8) + for split in 0...text.utf8.count { + var iterator = JSONValueSequence(Chunks(text + " \r\n", split: split)).makeAsyncIterator() + let value = try #require(await iterator.next()) + let restored = try JSONCodablePayload(preserving: value).value() + #expect(try JSONWriter(options: .init(numberStyle: .preserved)).write(restored) == expected) + #expect(try restored.read { try $0["é"].requiredInteger(as: UInt64.self) } == 9_007_199_254_740_993) + #expect(try await iterator.next() == nil) + } +} + +@Test func qaWideDuplicateFailureHasStableLocationAtEverySplit() async throws { + let fields = (0..<33).map { "\"k\($0)\":null" }.joined(separator: ",") + let prefix = "[{\(fields)," + let text = prefix + "\"k0\" ???}]" + for split in 0...text.utf8.count { + var iterator = JSONTopLevelArraySequence(Chunks(text, split: split)).makeAsyncIterator() + do { + while let event = try await iterator.next() { + if case .documentCompleted = event { Issue.record("Malformed document completed") } + } + Issue.record("Duplicate accepted") + } catch let failure as JSONFailure { + #expect(failure.code == .duplicateKey) + #expect(failure.location?.byteOffset == UInt64(prefix.utf8.count)) + } + #expect(try await iterator.next() == nil) + } +} + +private actor QACancellingSink { + var accepted: [UInt8] = [] + func accept(_ chunk: JSONByteChunk) { + accepted.append(contentsOf: chunk.bytes) + if accepted.count == 3 { withUnsafeCurrentTask { $0?.cancel() } } + } +} +@Test func qaSinkCancellationAfterAcceptanceKeepsPrefixAndStopsHandoffs() async throws { + let value = try JSONParser().parse(#"{"é":[9007199254740993,0.1,null]}"#) + let expected = Array(try JSONWriter().write(value)) + let task = Task { + let sink = QACancellingSink() + do { + try await JSONAsyncWriter(chunkSize: 1).write(value) { chunk in + await sink.accept(chunk) + } + Issue.record("Cancelled writer completed") + } catch is CancellationError { } + #expect(await sink.accepted == Array(expected.prefix(3))) + } + try await task.value +} diff --git a/Tests/JSONTests/Release1_1/AcceptanceWaveTests.swift b/Tests/JSONTests/Release1_1/AcceptanceWaveTests.swift new file mode 100644 index 0000000..399ebb7 --- /dev/null +++ b/Tests/JSONTests/Release1_1/AcceptanceWaveTests.swift @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +@testable import JSON + +private func qaPreserved(_ value: JSON) throws -> Data { + try JSONWriter(options: .init(numberStyle: .preserved)).write(value) +} + +// Fixed seed; the complete compact token is the independent fidelity oracle. +private func qaDocuments() -> [String] { + var seed: UInt64 = 0x534A_1100_A11C_E001 + return (0..<192).map { index in + seed = seed &* 6_364_136_223_846_793_005 &+ 1_442_695_040_888_963_407 + let exact = "9007199254740993\(seed % 1000)" + let fields = (0..<(32 + Int(seed % 5))).map { "\"k\($0)\":\($0)" }.joined(separator: ",") + switch index % 6 { + case 0: return "null" + case 1: return index % 12 == 1 ? "true" : "false" + case 2: return index % 12 == 2 ? "-0.00e+2" : exact + case 3: return "\"é-é-🚀-\(seed % 1000)\"" + case 4: return "[\(exact),0.1,-0.00e+2,null,[],{},\"é\"]" + default: return "{\(fields),\"é\":\(exact),\"é\":0.1,\"/~/\":null,\"\":false}" + } + } +} + +@Test func qaGeneratedExactRoundTripsAcrossNativeAndForeignPayloads() throws { + let documents = qaDocuments() + #expect(documents.count == 192) + for (index, compact) in documents.enumerated() { + let expected = Data(compact.utf8) + let root = try JSONParser().parse(" \r\n" + compact + "\t ") + #expect(try qaPreserved(root) == expected, "seed case \(index)") + let projected = try root.read { try $0.decode(JSON.self) } + #expect(try qaPreserved(projected) == expected) + let payload = try JSONCodablePayload(preserving: root) + let native = try JSONValueEncoder().encode(payload) + let nativeRestored = try native.decode(JSONCodablePayload.self) + let jsonRestored = try JSONDecoder().decode(JSONCodablePayload.self, from: JSONEncoder().encode(payload)) + let plistRestored = try PropertyListDecoder().decode(JSONCodablePayload.self, from: PropertyListEncoder().encode(payload)) + for restored in [nativeRestored, jsonRestored, plistRestored] { + #expect(try qaPreserved(restored.value()) == expected, "seed case \(index)") + } + } +} + +@Test func qaIndexThresholdEditsAgreeWithOrderedScalarOracle() throws { + for width in [0, 1, 7, 8, 15, 16, 31, 32, 33, 63, 64, 65, 127, 128, 257] { + let keys = (0.. startingWork, "failed work must not be refunded") + } +} +@Test func qaCaughtMalformedDataReleasesScratchWithoutRefundingWork() throws { + for malformed in ["AB==", "AAB=", "AAAA!", "AAAAAA==x"] { + _ = try JSON.string(malformed).decode(QARecoveringData.self) + } +} + +@Test func qaReaderTighteningPersistsAfterOrdinaryProjectionFailure() throws { + let root = try JSONParser().parse(#"{"history":"0123456789","n":1,"bad":"wrong"}"#) + let admission = try root.readWithReport { _ in 0 } + var coding = JSONCodingOptions() + coding.limits[.workUnits] = admission.statistics[.workUnits] + 1000 + #expect(throws: JSONFailure.self) { + try root.read { cursor in + do { _ = try cursor["bad"].decode(Int.self, coding: coding) } catch is DecodingError { } + for _ in 0..<1000 { _ = try cursor["n"].requiredInteger(as: Int.self) } + } + } + coding = .init(); coding.limits[.stringBytes] = 9 + #expect(throws: JSONFailure.self) { + try root.read { cursor in + do { _ = try cursor["n"].decode(Int.self, coding: coding) } catch {} + return 0 // Restriction checks admitted history, including unselected text. + } + } +} + +private enum QACallerError: Error { case expected } +private struct QAThrowingModel: Decodable { + init(from decoder: any Decoder) throws { + _ = try decoder.singleValueContainer().decode(Double.self) + throw QACallerError.expected + } +} +@Test func qaCaughtCallerProjectionReleasesOwnedTemporaries() throws { + let number = try JSONParser().parse("0.125") + func attempt(_ cursor: JSONReadCursor) throws { + do { _ = try cursor.decode(QAThrowingModel.self); Issue.record("Caller error disappeared") } + catch QACallerError.expected { } + } + let one = try number.readWithReport { cursor in + try attempt(cursor) + #expect(try cursor.requiredDouble(policy: .exact) == 0.125) + } + var limits = JSONLimits.balanced; limits[.retainedBytes] = one.statistics.peakLogicalRetainedBytes + let repeated = try number.readWithReport(limits: limits) { cursor in + for _ in 0..<100 { try attempt(cursor) } + #expect(try cursor.requiredDouble(policy: .exact) == 0.125) + } + #expect(repeated.statistics[.retainedBytes] == 0) + #expect(repeated.statistics[.workUnits] > one.statistics[.workUnits]) +} + +@Test func qaBase64CleanupKeepsResourceAndCancellationTerminal() async throws { + let canonical = Data(repeating: 255, count: 256).base64EncodedString() + var limits = JSONLimits.balanced; limits[.retainedBytes] = 49 + var meter = try JSONWorkMeter(limits: limits, domain: .coding) + try meter.retain(17) // An enclosing owner's reservation must survive. + do { _ = try JSONBase64.decode(canonical, meter: &meter); Issue.record("Expected scratch refusal") } + catch let failure as JSONFailure { #expect(failure.resource?.metric == .retainedBytes) } + #expect(meter.used(.retainedBytes) == 17 && meter.used(.workUnits) > 0) + #expect(throws: JSONFailure.self) { try meter.checkpoint() } + try meter.release(17) + let cancelled = Task { + var options = JSONLimits.balanced; options[.cancellationQuantum] = 1 + var local = try JSONWorkMeter(limits: options, domain: .coding) + try local.retain(17) + local.checkpointObserver = { work in + if work >= 20 { withUnsafeCurrentTask { $0?.cancel() } } + } + do { _ = try JSONBase64.decode(canonical, meter: &local); Issue.record("Cancellation disappeared") } + catch is CancellationError { } + #expect(local.used(.retainedBytes) == 17 && local.used(.workUnits) >= 20) + #expect(throws: CancellationError.self) { try local.checkpoint() } + try local.release(17) + } + try await cancelled.value +} + +@Test func qaPayloadCreateAndOpenInclusiveWorkRetentionMatrix() throws { + for compact in ["null", "0.1", "-0.00e+2", "[]", "{}", #"{"é":9007199254740993,"é":[null,false]}"#] { + let value = try JSONParser().parse(compact) + let made = try JSONCodablePayload.preservingWithReport(value) + let opened = try made.result.valueWithReport() + for (operation, statistics) in [(0, made.statistics), (1, opened.statistics)] { + for metric in [JSONResourceMetric.workUnits, .retainedBytes] { + let boundary = metric == .retainedBytes ? statistics.peakLogicalRetainedBytes : statistics[metric] + var limits = JSONLimits.balanced; limits[metric] = boundary + if operation == 0 { _ = try JSONCodablePayload(preserving: value, limits: limits) } + else { #expect(try qaPreserved(made.result.value(limits: limits)) == Data(compact.utf8)) } + limits[metric] = boundary - 1 + do { + if operation == 0 { _ = try JSONCodablePayload(preserving: value, limits: limits) } + else { _ = try made.result.value(limits: limits) } + Issue.record("Missing inclusive quota refusal") + } catch let failure as JSONFailure { + #expect(failure.category == .resource && failure.resource?.metric == metric) + } + } + } + } +} + +@Test func qaMappedScalarPathsUseUTF8ByteCoordinates() throws { + let text = "{\n \"é\":{\"/~/\":[null,\"wrong\"]},\n \"é\":false\n}" + let document = try JSONParser().parseDocument(Data(text.utf8), source: "qa") + let prefix = "{\n \"é\":{\"/~/\":[null," + do { _ = try document.read { try $0["é"]["/~/"][1].requiredInteger(as: Int.self) }; Issue.record("Missing mapped failure") } + catch let error as JSONAccessFailure { + #expect(error.path == [.key("é"), .key("/~/"), .index(1)]) + #expect(error.cause.location?.byteOffset == UInt64(prefix.utf8.count)) + #expect(error.cause.location?.line == 2) + #expect(error.cause.location?.column == UInt64(prefix.split(separator: "\n").last!.utf8.count + 1)) + } + #expect(try document.read { try $0["e\u{301}"].requiredBoolean() } == false) +} + +@Test func qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes() async throws { + let root = try JSONParser().parse(#"{"é":9007199254740993,"é":0.1,"a":[null,false]}"#) + let payload = try JSONCodablePayload(preserving: root) + let expected = try qaPreserved(root) + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0..<16 { + group.addTask { + for _ in 0..<20 { + #expect(try qaPreserved(payload.value()) == expected) + let report = try root.readWithReport { cursor in + #expect(try cursor["é"].requiredInteger(as: UInt64.self) == 9_007_199_254_740_993) + return try cursor["e\u{301}"].requiredDouble(policy: .rounded) + } + #expect(report.result == 0.1 && report.statistics[.retainedBytes] == 0) + } + } + } + try await group.waitForAll() + } +} diff --git a/Tools/MobileQualification/Sources/ProbeCases/PublicBehavior.swift b/Tools/MobileQualification/Sources/ProbeCases/PublicBehavior.swift index 0b8da28..b017134 100644 --- a/Tools/MobileQualification/Sources/ProbeCases/PublicBehavior.swift +++ b/Tools/MobileQualification/Sources/ProbeCases/PublicBehavior.swift @@ -52,6 +52,40 @@ public func publicBehavior() throws { let reopened = try PropertyListDecoder().decode(JSONCodablePayload.self, from: PropertyListEncoder().encode(payload)) try verify(exactBytes(reopened.value()) == Data(fixture.inner_json.utf8), "opaque payload fidelity") } + try acceptanceWave() +} +private struct RecoveringData: Decodable { + let rejected: Int + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + var count = 0 + for _ in 0..<100 { + do { _ = try container.decode(Data.self); throw ProbeFailure.assertion("malformed Base64 accepted") } + catch is DecodingError { count += 1 } + } + rejected = count + } +} +private func acceptanceWave() throws { + var limits = JSONLimits.balanced; limits[.retainedBytes] = 32 + let recovered = try JSON.string("AAAA!").decodeWithReport(RecoveringData.self, coding: .init(limits: limits)) + try verify(recovered.result.rejected == 100 && recovered.statistics[.retainedBytes] == 0, "ordinary corruption releases scratch") + try verify(recovered.statistics[.workUnits] > 100 && recovered.statistics.peakLogicalRetainedBytes <= 32, "failed work remains cumulative") + let members = (0..<33).map { JSONMember("k\($0)", value: .number(JSONNumber(integer: $0))) } + [.init("é", value: 1), .init("e\u{301}", value: 2)] + let wide = JSON.object(try JSONObject(members: members)) + let payload = try JSONCodablePayload(preserving: wide) + let reopened = try JSON(encoding: payload).decode(JSONCodablePayload.self).value() + try verify(exactBytes(reopened) == exactBytes(wide), "indexed native payload fidelity") + let raw = try JSONParser().parse(#"{"patternProperties":{"^k[0-9]+$":{"type":"integer"}},"properties":{"é":{"const":1},"e\u0301":{"const":2}},"unevaluatedProperties":false}"#) + let plan = try JSONSchemaCompiler().compile(JSONSchemaDocument(raw)) + try verify(JSONSchemaValidator().validate(reopened, against: plan).isValid, "indexed evaluated locations") + let patch = try JSONPatch(json: JSONParser().parse(#"[{"op":"move","from":"/k32","path":"/new"},{"op":"remove","path":"/k0"}]"#)) + guard case .document(let changed) = try patch.applying(to: reopened) else { throw ProbeFailure.assertion("indexed Patch root removed") } + try changed.read { cursor in + try verify(cursor["new"].requiredInteger(as: Int.self) == 32, "indexed Patch selection") + try verify(cursor.presence(of: "k0") == .absent, "indexed Patch removal") + try verify(cursor["é"].requiredInteger(as: Int.self) == 1 && cursor["e\u{301}"].requiredInteger(as: Int.self) == 2, "indexed scalar keys") + } } private struct Chunks: AsyncSequence, Sendable { typealias Element = JSONByteChunk From e6eb46da346bee9892119fdc414abd7783e66b2c Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 14:17:03 +0200 Subject: [PATCH 2/3] Record QA evidence and qualify actual index boundary stages --- Documentation/Architecture/API.md | 2 +- .../Architecture/Release1.1/Progress.json | 80 +++++++++++++++---- Documentation/Architecture/Release1.1/QA.md | 69 ++++++++++++++++ Documentation/Qualification/README.md | 11 ++- README.md | 6 +- Scripts/ci.py | 10 ++- .../JSONPatchTests/AcceptanceWaveTests.swift | 4 +- .../JSONSchemaTests/AcceptanceWaveTests.swift | 2 +- .../Release1_1/AcceptanceWaveTests.swift | 4 +- 9 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 Documentation/Architecture/Release1.1/QA.md diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index 05343bb..9df8e3c 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -35,7 +35,7 @@ order by default. Exact duplicates fail checked construction and parsing. | `JSONObject.build(limits:_:)` | Exclusive noncopyable member accumulator; checks before append | | `JSON.buildArray(limits:_:)` | Exclusive array accumulator; nested arrays remain one element | | `append(contentsOf:)` on array accumulator | Explicit flattening with charged work | -| Object subscripts by String/JSONKey | Unmetered linear exact-key lookup; nil means absence | +| Object subscripts by String/JSONKey | Unmetered exact-key lookup using immutable metadata; nil means absence | | `JSON.requiredMember(_:limits:)` | Bounded lookup; throws on wrong kind or missing member | | `JSON.presence(of:limits:)` | Bounded lookup separating absent, null and non-null value | | `JSONObject.presence(of:)` | Unmetered convenience with the same three states | diff --git a/Documentation/Architecture/Release1.1/Progress.json b/Documentation/Architecture/Release1.1/Progress.json index 17fc5f8..d0075ec 100644 --- a/Documentation/Architecture/Release1.1/Progress.json +++ b/Documentation/Architecture/Release1.1/Progress.json @@ -1,20 +1,68 @@ { "schema_version": 1, "target_release": "1.1.0", - "milestone": "implementation, runtime qualification and reviewable candidate", - "tested_core_sha": "a48eb337b70eb075ce37c99dc3d150032f720d00", - "preserved_history": ["1.0.0", "W00-W12", "G01-G12", "RC01-RC08", "F01-F06", "J01-J04", "R0 turn 001"], - "root_function_counts": {"JSON":86,"JSONStreaming":11,"JSONSchema":38,"JSONRegexRuntime":12,"JSONPatch":9}, - "root_function_total":156, - "parameterized_invocations":173, - "cap_probes":49, - "readme_examples":10, - "authored_public_docc_symbols":582, - "payload_fixtures":8, - "r5_matrix": {"macOS":"executed","iPhone_simulator":"executed","iPad_simulator":"executed","physical_iPhone":"executed","physical_iPad":"blocked; unavailable"}, - "conditional_features": {"SIMD":"deferred","parallel_batches":"deferred","E11":"deferred","E12":"deferred"}, - "evidence_policy":"Actual final native/safety/docs/mobile receipts and materialized Turn002 archive identify commands, source hashes, counts and limitations. Documentation-only successors map unchanged core/test inputs explicitly.", - "publication_performed":false, - "remaining_release_blockers":["Physical iPad smoke, disk persistence and release measurements for the agreed R5 matrix", "Complete maintainer review and protected PR integration before final release readiness"], - "performance_review":"IntegratedPerformance.md; unchanged 10% trigger, retain initial regression measurements and final accepted dispositions" + "milestone": "additional acceptance and conformance wave", + "tested_core_sha": "5d655f2beab569ac804890493bfafcf6045caabe", + "preserved_history": [ + "1.0.0", + "W00-W12", + "G01-G12", + "RC01-RC08", + "F01-F06", + "J01-J04", + "R0 turn 001" + ], + "root_function_counts": { + "JSON": 97, + "JSONStreaming": 14, + "JSONSchema": 41, + "JSONRegexRuntime": 12, + "JSONPatch": 11 + }, + "root_function_total": 175, + "parameterized_invocations": 173, + "cap_probes": 49, + "readme_examples": 10, + "authored_public_docc_symbols": 582, + "payload_fixtures": 8, + "r5_matrix": { + "macOS": "executed", + "iPhone_simulator": "executed", + "iPad_simulator": "executed", + "physical_iPhone": "executed", + "physical_iPad": "blocked; unavailable" + }, + "conditional_features": { + "SIMD": "deferred", + "parallel_batches": "deferred", + "E11": "deferred", + "E12": "deferred" + }, + "evidence_policy": "Actual final native/safety/docs/mobile receipts and materialized Turn002 archive identify commands, source hashes, counts and limitations. Documentation-only successors map unchanged core/test inputs explicitly.", + "publication_performed": false, + "remaining_release_blockers": [ + "Physical iPad smoke, disk persistence and release measurements for the agreed R5 matrix", + "Complete maintainer review and protected PR integration before final release readiness" + ], + "performance_review": "IntegratedPerformance.md; unchanged 10% trigger, retain initial regression measurements and final accepted dispositions", + "prior_milestones": [ + { + "turn": 2, + "tested_core_sha": "a48eb337b70eb075ce37c99dc3d150032f720d00", + "root_function_total": 156, + "source_manifest_sha256": "2b69a683002eaa4e112bd005b3eb06e63dc8e06bfcd2b5c8485f076870f26797", + "return_sha256": "151ce56037763e1ca3d6d0439521020f34ca368ef77b50c31b8ef4e27fb282b6" + } + ], + "qa_wave": { + "added_functions": 19, + "baseline_swift_test_files_retained": 48, + "scope": "QA.md", + "native_full": "passed on tested_core_sha", + "safety_full": "passed on tested_core_sha; TSAN5", + "mobile": "both SDKs/simulators/physical iPhone passed; iPad physical blocked", + "supplemental_boundary_review": "pending final qa lane", + "docs": "pending final DocC lane", + "accounting_fix": "Failed Base64 scratch released; work/terminal health retained; caps/weights/version unchanged" + } } diff --git a/Documentation/Architecture/Release1.1/QA.md b/Documentation/Architecture/Release1.1/QA.md new file mode 100644 index 0000000..e5a619b --- /dev/null +++ b/Documentation/Architecture/Release1.1/QA.md @@ -0,0 +1,69 @@ +# Additional 1.1 acceptance and conformance wave + +The Turn003 wave adds 19 Swift Testing behavior functions to the 156-function +Turn002 candidate. Exact discovery is JSON97, Streaming14, Schema41, Regex12 and +Patch11: 175 functions, with all 173 prior parameterized invocations and all 48 +earlier Swift test files retained. No independent policy exclusion is reclassified. + +| Subject | Additional evidence | +| --- | --- | +| Exact transport | Fixed seed `0x534A1100A11CE001`, 192 documents across six root kinds; compact token bytes independently check native projection, Version 1 opening and native/Foundation JSON/property-list transport | +| Numeric projections | 772 integral spellings, independently checked signed/unsigned 8-bit ranges, including equivalent fractional/exponent spellings | +| Keys/index publication | Checked construction, accumulator, parser, edits and reusable scopes; actual member boundaries 7/8/9, 31/32/33, 63/64/65 and 127/128/129; ordered scalar-key oracle | +| Base64 | 259 canonical byte fixtures; 1,152 mutated unused-padding-bit cases; repeated caught corruption, prefix ownership, quota failure and in-loop cancellation | +| Scoped coding | Permanent ceilings after ordinary failure, prior unselected-text admission, caller-error scratch release and mapped Unicode UTF-8 byte coordinates | +| Payload budgets | Six roots, creation/opening work and live-retention inclusive/minus-one matrix; identical preserved bytes | +| Shared state | 16 workers ×20 openings/read scopes over shared immutable inputs; checked Sendable and five-case TSAN selection | +| Schema | Explicitly rooted definitions and negative cases; wide persisted instances/evaluated scalar locations, plan reuse after invalidity, cumulative regex search boundary | +| Patch | Ordered scalar-key edits and real 32→31→32 stages; cumulative quota refusal publishes no partial result | +| Streaming | Every byte split of a wide scalar-key/lexeme document and malformed duplicate; stable second-key location, terminal pulls, cancellation after three accepted sink bytes | + +The first executed wave reproduced a logical-retention accounting defect: shared +Base64 decoding left partial scratch charged after ordinary corruption. A caller +catching that error could accumulate freed-byte reservations and later get a false +resource refusal. Decode now releases only its invocation's failed reservation. +Consumed work and terminal resource/cancellation state remain charged/latched; +successful Data ownership retains its prior accounting. This corrects Version 1 +accounting without changing weights, caps, grammar or the public API. It is not an +allocator-memory leak or an RSS improvement claim. Original failure receipts remain. + +A new projection test initially calibrated its limit using native rounded decoding +then added a larger exactness-proof operation. Its correction measures the same +complete operation on both sides. Compiler harness corrections use an explicit +JSONKey initializer and an actor-owned Sendable sink; no unsafe annotation or +compiler bypass is introduced. A separate review expanded generated field widths +to include actual member boundaries after adding the special keys. + +Use `Scripts/ci.py --lane native`, `--lane safety`, `--lane docs` and the supplemental +`--lane qa` on the supported host. Full native execution retains JSONTestSuite +318 discovered/287 evaluated/31 policy and Schema2337/2300/37, 82 authored vectors, +nine budget vectors, 4,396 malformed replays, 31,824 regex cases/442 patterns and +18,853 Unicode cases/94,265 comparisons. All49 public cap outcomes remain required. +The rooted public consumer retains20 cases and distinct operational errors. + +The public mobile harness additionally checks100 caught malformed Data projections +under32 logical bytes and wide indexed payload/Schema/Patch behavior. Both SDKs, +isolated iPhone/iPad simulators and the physical iPhone execute the updated harness; +disk save/reopen/update uses three independently exited processes and eight frozen +fixtures. Physical iPad qualification remains blocked. Simulator execution does +not establish physical-device performance. + +The affected Base64 paths have three serial alternating release-native pairs, +each with3 warmups,9 samples and100 public invocations/sample, finite balanced caps, +matched input hashes and identical work. Across27 samples/side, valid decode +medians range from−1.28% to+4.98%; native payload decode is+1.16% and corruption +recovery−0.09%. No new10% timing trigger is crossed. Recovery's corrected logical +peak is16 rather than300, with final live0 rather than300; work remains506. Keep +raw variance/ranges and process-wide RSS separate from these logical counters. +The earlier wide-index and integrated streaming dispositions remain unchanged. + +Physical iPhone14ProMax/iOS27.2 updated public QA measures9 release samples after3 +warmups: median50.457167ms, process-wide peak25,133,056 bytes, thermal state0 at +both observations. This harness adds work relative to Turn002; the two medians +are not a matched speed comparison. SDK/UIKit/process costs are included. + +Actual receipts, failed-source reproduction, raw samples, source bindings, clean +archive rebuilding and compiled catalogues are materialized in the local Turn003 +return. Hosted Native job artifacts supply remote source/command receipts. This +wave does not merge the protected PR stack, qualify an MCP server, publish1.1, +select SIMD/parallel batches, or close the unavailable physical iPad gate. diff --git a/Documentation/Qualification/README.md b/Documentation/Qualification/README.md index 3ef6d3c..df669fc 100644 --- a/Documentation/Qualification/README.md +++ b/Documentation/Qualification/README.md @@ -12,7 +12,7 @@ and seven additional nonparameterized cases. The first production lookup increme the independent runner adds nine budget/report vectors. There are three separate JSON/JSONSchema consumer tests. All discoveries must be nonzero. -The current integrated 1.1 candidate checks exactly 156 root functions: +The Turn002 integrated 1.1 candidate checked exactly 156 root functions: JSON 86, JSONStreaming 11, JSONSchema 38, JSONRegexRuntime 12 and JSONPatch 9. All 173 parameterized invocations remain. The earlier counts above describe their historical stages, not this source. Three additional public/mobile Swift Testing @@ -20,6 +20,15 @@ functions exercise eight pinned payload fixtures and three independently exited SwiftData disk phases. The seven-function R0 subset, all 49 cap probes, rooted 20-case consumer and independent policies retain their exact checks. +The additional QA wave adds 19 behavior functions, for exactly 175: +JSON 97, JSONStreaming 14, JSONSchema 41, JSONRegexRuntime 12 and JSONPatch 11. +All 48 earlier Swift test files and 173 parameterized invocations are retained. +The focused `python3 Scripts/ci.py --lane qa` checks exact counts 11/3/3/2 in +debug, release and Address Sanitizer. It supplements the full native/safety lanes +and does not replace independent qualification or any of the 49 cap probes. +Thread Sanitizer checks five shared-state cases, including concurrent payload +opening and separate read scopes over one immutable root. See [QA](../Architecture/Release1.1/QA.md). + | Independent input | Observed policy | | --- | --- | | JSONTestSuite, pinned in FixtureRegistry.json | 318 discovered: 101 accepted, 186 rejected, 31 strict/finite-policy rejections | diff --git a/README.md b/README.md index 7e5330e..d7a42f0 100644 --- a/README.md +++ b/README.md @@ -365,12 +365,16 @@ documentation. Receipts record actual commands, toolchains, source hashes and ca counts. Policy exclusions are reported separately from evaluated successes. See [qualification](Documentation/Qualification/README.md) and [CI](Documentation/CI/README.md). -Performance is measured separately on native release builds. The current reviewed +Performance is measured separately on native release builds. The released 1.0 streaming comparison is approximately **11% slower**, exceeding the unchanged 10% review trigger, with its accepted disposition and measurement limits documented in [performance notes](Documentation/Performance/README.md). Logical retention remains separate from process RSS. Scalar scanning and plan-local immutable regex sharing are the qualified choices. Projection and JSON text-sequence framing remain deferred. +The 1.1 candidate's separate matched stream comparison is **15.76% slower**; its +[integrated review](Documentation/Architecture/Release1.1/IntegratedPerformance.md) +retains the accepted disposition. The [additional QA wave](Documentation/Architecture/Release1.1/QA.md) +records conformance coverage and the Base64 accounting correction. ## Contributing, support and licensing diff --git a/Scripts/ci.py b/Scripts/ci.py index 4e0f19d..4022e19 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -15,6 +15,12 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + if lane == 'qa': + return [ + ['swift', 'test', '--filter', 'qa'], + ['swift', 'test', '-c', 'release', '--filter', 'qa'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/ci-address', '--filter', 'qa'], + ] if lane == 'native': return [ ['python3', '-B', 'Scripts/public_check.py'], @@ -71,7 +77,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 3] if 'thread' in command else [11, 12, 14, 41, 97] + expected = [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 3] if 'thread' in command else [11, 12, 14, 41, 97] passed = passed and sorted(counts) == expected if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) @@ -89,7 +95,7 @@ def check_result(command, code, log): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0'], required=True) + parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) diff --git a/Tests/JSONPatchTests/AcceptanceWaveTests.swift b/Tests/JSONPatchTests/AcceptanceWaveTests.swift index b90f843..3bf3ad8 100644 --- a/Tests/JSONPatchTests/AcceptanceWaveTests.swift +++ b/Tests/JSONPatchTests/AcceptanceWaveTests.swift @@ -4,7 +4,9 @@ import JSON import JSONPatch @Test func qaWidePatchEditsPreserveExactKeyOrderThroughIndexTransitions() throws { - for width in [31, 32, 33, 63, 64, 65] { + // The two scalar-distinct keys make width 30 an actual 32-member object. + // Moving a member removes it before adding, exercising 32 -> 31 -> 32. + for width in [29, 30, 31, 32, 33, 61, 62, 63, 64, 65] { let members = (0.. [String] { } @Test func qaIndexThresholdEditsAgreeWithOrderedScalarOracle() throws { - for width in [0, 1, 7, 8, 15, 16, 31, 32, 33, 63, 64, 65, 127, 128, 257] { + // Four special keys follow the generated fields. Include actual member + // counts 7/8/9, 31/32/33, 63/64/65 and 127/128/129, not just field widths. + for width in [0, 1, 3, 4, 5, 7, 8, 15, 16, 27, 28, 29, 31, 32, 33, 59, 60, 61, 63, 64, 65, 123, 124, 125, 127, 128, 257] { let keys = (0.. Date: Mon, 12 Oct 2026 14:22:46 +0200 Subject: [PATCH 3/3] Close QA ledger and record source-bound review disposition --- CHANGELOG.md | 3 +++ .../Release1.1/IntegratedPerformance.md | 11 ++++++++ .../Architecture/Release1.1/Progress.json | 6 ++--- .../Architecture/Release1.1/Review.md | 26 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24794ad..d325fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ qualification remains a release gate. No tag/publication follows this change. - Correct native ISO-8601 calendar fields and strict Base64 unused padding bits. See API migration guidance; no limits/profile weights or dependencies raised. +- Release failed Base64 scratch from logical retention while preserving consumed + work and terminal operational errors. Add 19 acceptance/conformance tests, + public mobile recovery checks and a focused debug/release/ASAN QA lane. ## 1.0.0 — 2026-10-11 diff --git a/Documentation/Architecture/Release1.1/IntegratedPerformance.md b/Documentation/Architecture/Release1.1/IntegratedPerformance.md index 62d704b..98e50da 100644 --- a/Documentation/Architecture/Release1.1/IntegratedPerformance.md +++ b/Documentation/Architecture/Release1.1/IntegratedPerformance.md @@ -53,3 +53,14 @@ nine release samples after three warmups: median54.406542 ms and process peak RS allocation or hard-RSS claims, and no matched mobile optimization comparison. Physical iPad measurements remain blocked. Logical bytes, allocator observations, process RSS and caller computation remain distinct. + +The additional QA wave changes only Base64 failure cleanup in the runtime. Affected +public Data/payload paths have three matched, alternating release-native process +pairs (27 samples per source/workload, 100 invocations/sample). Valid Data medians +range from −1.28% to +4.98%; payload decoding is +1.16%, corruption recovery −0.09%. +Input hashes/work counters match; no new 10% timing trigger is crossed. The corrected +recovery ledger reports final live zero instead of 300 and peak 16 instead of 300. +This is truthful accounting of freed scratch, without an RSS/allocator improvement +claim. The unchanged index and twelve-workload dispositions above are retained. +The expanded physical QA harness has different work and cannot be compared with +the earlier phone median as a matched speed result. See [QA](QA.md). diff --git a/Documentation/Architecture/Release1.1/Progress.json b/Documentation/Architecture/Release1.1/Progress.json index d0075ec..e87a50f 100644 --- a/Documentation/Architecture/Release1.1/Progress.json +++ b/Documentation/Architecture/Release1.1/Progress.json @@ -38,7 +38,7 @@ "E11": "deferred", "E12": "deferred" }, - "evidence_policy": "Actual final native/safety/docs/mobile receipts and materialized Turn002 archive identify commands, source hashes, counts and limitations. Documentation-only successors map unchanged core/test inputs explicitly.", + "evidence_policy": "Turn003 materializes actual native/safety/QA/docs/SDK/simulator/iPhone receipts, raw measurements and reproducible source archive. Turn002 and original history remain immutable. Source bindings identify unchanged runtime bytes and expanded test cases.", "publication_performed": false, "remaining_release_blockers": [ "Physical iPad smoke, disk persistence and release measurements for the agreed R5 matrix", @@ -61,8 +61,8 @@ "native_full": "passed on tested_core_sha", "safety_full": "passed on tested_core_sha; TSAN5", "mobile": "both SDKs/simulators/physical iPhone passed; iPad physical blocked", - "supplemental_boundary_review": "pending final qa lane", - "docs": "pending final DocC lane", + "supplemental_boundary_review": "passed at e6eb46da346bee9892119fdc414abd7783e66b2c: 19 exact cases each in debug/release/ASAN", + "docs": "passed at e6eb46da346bee9892119fdc414abd7783e66b2c: four catalogues, 582 authored public symbols, 10 README examples; refresh final documentation successor", "accounting_fix": "Failed Base64 scratch released; work/terminal health retained; caps/weights/version unchanged" } } diff --git a/Documentation/Architecture/Release1.1/Review.md b/Documentation/Architecture/Release1.1/Review.md index 7ea7982..8a7e9e1 100644 --- a/Documentation/Architecture/Release1.1/Review.md +++ b/Documentation/Architecture/Release1.1/Review.md @@ -33,3 +33,29 @@ actual commands/source and nonzero counts. Source archives exclude private input Git/build trees, signing profiles and device identifiers. Physical iPad remains blocked; conditional SIMD/parallel batches stay deferred, with no unqualified performance claim. No merge/tag/publication is implied by this review. + +## Additional Turn003 review + +The same implementing agent performed a separate source, fixture, receipt and +test-body review against parent `9e61f158`. This remains distinct from independent +human review or GitHub approval. Native/full safety ran at `5d655f2`; the reviewed +boundary additions ran through the exact 19-case debug/release/ASAN QA lane at +`e6eb46d`. All runtime library bytes are unchanged between those two stages. + +- Medium: shared Base64 failure retained freed scratch in the logical ledger. + Preserve the 400 failed recovery assertions; restore only the failed decoder's + live reservation. Successful ownership, work and terminal health remain intact. + Full native/ASAN, quota/cancellation-prefix tests and a public 32-byte consumer + verify the correction. Three matched benchmark pairs retain identical work. +- Qualification: special keys added to generated widths obscured actual final + index boundaries. Expanded the corpus to actual 7/8/9, 31/32/33, 63/64/65 and + 127/128/129 members; Patch now explicitly visits 32→31→32. All added cases pass + debug/release/ASAN without changing any prior test file or policy exclusion. +- Documentation: the API table still described direct object lookup as linear. + It now describes immutable exact-key metadata. README distinguishes released + 1.0's historical stream regression from the candidate's separate +15.76% pair. + +The final source binding, clean reproduction, actual catalogue outputs and hosted +CI receipts accompany the local Turn003 return. Both SDKs, both simulator families +and the updated physical iPhone public/disk harness pass. Physical iPad remains +blocked; protected integration and publication require separate maintainer action.