From db991c51dc15e8e47a0356a66ae7e03f4ed51b98 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Fri, 16 Oct 2026 18:52:55 +0200 Subject: [PATCH] Reconcile public snapshot draft on current Inspection proposal --- .../Architecture/ExtractionManifest.json | 4 +- .../NAV7SnapshotReconciliation.md | 21 ++ .../SnapshotExperiment.md | 85 ++++++ .../SnapshotPromotion.md | 63 ++++ Documentation/CI/README.md | 18 ++ .../Qualification/TestDiscovery.json | 10 +- README.md | 9 + Scripts/ci.py | 12 +- Scripts/qualify_snapshot.py | 120 ++++++++ Scripts/test_qualification_harness.py | 6 +- Sources/JSON/Reading/JSONReadScope.swift | 15 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/JSONInspection.md | 8 + .../Documentation.docc/StructuralSnapshots.md | 85 ++++++ .../JSONStructuralSnapshot.swift | 240 +++++++++++++++ .../JSONNavigation/JSONTraversalContext.swift | 4 + .../SnapshotAcceptanceTests.swift | 278 ++++++++++++++++++ .../SnapshotPromotionTests.swift | 123 ++++++++ Tools/SnapshotQualification/Package.swift | 12 + Tools/SnapshotQualification/README.md | 10 + .../Sources/SnapshotConsumer/Main.swift | 151 ++++++++++ 21 files changed, 1261 insertions(+), 15 deletions(-) create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV7SnapshotReconciliation.md create mode 100644 Documentation/Architecture/StructuralIntelligence/SnapshotExperiment.md create mode 100644 Documentation/Architecture/StructuralIntelligence/SnapshotPromotion.md create mode 100644 Scripts/qualify_snapshot.py create mode 100644 Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md create mode 100644 Sources/JSONInspection/JSONStructuralSnapshot.swift create mode 100644 Tests/JSONInspectionTests/SnapshotAcceptanceTests.swift create mode 100644 Tests/JSONInspectionTests/SnapshotPromotionTests.swift create mode 100644 Tools/SnapshotQualification/Package.swift create mode 100644 Tools/SnapshotQualification/README.md create mode 100644 Tools/SnapshotQualification/Sources/SnapshotConsumer/Main.swift diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 95a8694..0956116 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -299,7 +299,9 @@ "1.6 S3 G7 seeded grammar, scalar-key permutation tests, public replay consumer and strict cardinality/matrix harness are original Apache-2.0 contributions. The 64-schema authored fixture has SHA-256 cb55604183a6db7df5fa968db283e7abeae794277d2b0cef033b0bdb92e615cb. Independent python-jsonschema 4.26.0 is research qualification only, with exact numeric binding; no Python runtime dependency or copied validator/engine is distributed.", "1.6 S3 G8 materializes the unchanged synthetic corpus per caller access, replacing process-lifetime stored JSON payloads exposed by Linux ASan/LSan. The public process-lifetime consumer and strengthened exact/value-copy test are original Apache-2.0 qualification code; no suppression, runtime fallback, new dependency or corpus/digest change is introduced.", "Composition reference-load fixtures, serial descriptor staging, public-only adapter, capacity/terminal tests and strict CI receipt checks are original Apache-2.0 code. Generated JSON graphs introduce no upstream protocol fixtures, engine or dependency. Core public APIs, finite profiles and accounting weights are unchanged.", - "NAV5 public array-evidence packaging, external consumer and DocC are original Apache-2.0 code. Existing exact shape evidence, exclusive reducer, weights and I01-I17 semantics are unchanged; no upstream engine or dependency is adapted. Promotion is a draft candidate, not a released product." + "NAV5 public array-evidence packaging, external consumer and DocC are original Apache-2.0 code. Existing exact shape evidence, exclusive reducer, weights and I01-I17 semantics are unchanged; no upstream engine or dependency is adapted. Promotion is a draft candidate, not a released product.", + "NAV5 package-only complete structural snapshot, parent-linked metadata, original source-ownership transfer and S01-S24 synthetic tests are original Apache-2.0 code. The existing read scope admits once; its default public read/traversal/inspection completion behavior is preserved. No persistent identity, upstream algorithm or new dependency is introduced.", + "NAV6 snapshot access-level promotion, external consumer, independent 28-record maintainer oracle, terminal-transfer/concurrent lookup proofs and compiled DocC are original Apache-2.0 contributions. No upstream engine, new semantic authority, dependency or additional product is adapted." ], "jsonhtml_export_original_contributions": { "license": "Apache-2.0", diff --git a/Documentation/Architecture/StructuralIntelligence/NAV7SnapshotReconciliation.md b/Documentation/Architecture/StructuralIntelligence/NAV7SnapshotReconciliation.md new file mode 100644 index 0000000..df22de3 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV7SnapshotReconciliation.md @@ -0,0 +1,21 @@ +# NAV7 dependent snapshot candidate + +This draft replays37d84e8..b77d43d onto the NAV7 current-main N2 proposal, +including #44 DocC index repair and #45 release metadata. Original #42/#43 and +NAV6 heads remain unchanged. Snapshot source, read-scope ownership designation, +exclusive context, all original S/V tests,14-case external consumer and compiler +probe scripts are byte-identical to b77d43d. No algorithm replacement occurs. + +JSONInspection remains the one opt-in candidate product:44 Inspection declarations +plus23 authored snapshot additions, with Navigation56 unchanged. Construction is +one original scope/admission; a complete immutable snapshot owns one source and +compact records. Each path/value/span lookup has its own explicit bounded full- +source admission. Ordinals are local positions, not transferable node handles. + +G4 native performance remains HOLD with zero timed snapshot workloads and no +retry authorization. Logical counters are policy, not RSS, allocation count or +speed. This source requires fresh Debug/Release/ASan/TSan/strict, ownership/public +negative codegen, external consumer, DocC/site, Linux, WASI/WasmKit, SDK and archive +proofs. Actual outcomes are source-bound in the private NAV7 return; this document +does not invent a pass. No merge, ready transition, release or public projection +is authorized. P1 private experiments remain a separate conditional branch. diff --git a/Documentation/Architecture/StructuralIntelligence/SnapshotExperiment.md b/Documentation/Architecture/StructuralIntelligence/SnapshotExperiment.md new file mode 100644 index 0000000..6fc0ce3 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/SnapshotExperiment.md @@ -0,0 +1,85 @@ +# NAV5 I0 — Complete source-attached snapshot + +Status: accepted design for a package-only experiment; implementation remains +conditional on the N2 public surface freeze and repeated owned-reducer proof. +Base: frozen N2 API candidate 2e952c00ea2f4c2a943032a54f3a3fd85b1b96fb, +with the subsequent generated-identity correction replayed separately. Both descend +from 462f3ea and main 7d72c88. No public N3 API, +new product, persistence, actor, projection, index-by-path or traversal option. + +## Representation and invariants + +One immutable Sendable snapshot holds one JSONDocument plus compact records. +Each record contains ordinal, optional parent, optional exact edge component, +kind, depth, immediate child count and a finalized half-open subtree end. No JSON +subtree, absolute path, span, source bytes or label is stored per record. Ordinals +are snapshot-local inputs to instance methods; no detached node/handle type exists +and no API accepts a replacement document. Metadata lookup is bounds checked. + +The sole builder calls inspectOwned with its default complete physical root and +no options or callback supplied by the caller. Before a depth-d hit, close stack +entries while count > d, setting their ends to the next ordinal. The remaining +top is the parent. Append the record and ordinal after charging storage. The +root has depth 0 and nil parent/component. On finish, require complete summary, +visited/emitted == records.count, and close every remaining frame at total count. +No failed or cancelled finish returns an artifact. Empty/scalar roots yield one +record. Metadata and parent links are immutable after initialization. + +## One original admission and source ownership transfer + +Existing JSONReadScope.complete refunds input/source ownership unconditionally. +Returning a source-attached snapshot without extending that accounting would +underreport live output; readmitting the document would duplicate work/quotas. +Use a narrowly package-scoped, one-shot transferDocumentOwnership capability: +check original scope health/owner and charge one work step, then designate the +already-admitted source bytes as result-owned. At complete, refund paths and +transient invocation storage as before, but retain the original rootBytes in the +same report. The context method remains mutating and returns only the canonical +immutable JSONDocument, never scope/state/meter/cursor. No new admission or ledger. + +Default false preserves every existing read/visit/collect/N2 completion statistic. +Only the N3 finish path invokes transfer, after complete structural finalization. +Invalidation always clears scope document/state; returned source is an ordinary +immutable alias. Failure after transfer still returns no report/artifact, and +scope teardown releases its references. Tests must prove exactly one admitted +node/source-map count, captured source cost retained once, N1/N2 parity, and source +release/terminal behavior. If this narrow package helper cannot preserve existing +semantics or the compiler proof, do not implement N3 with a weaker meter. + +## Logical overlay v1 and complexity + +Declare 96 bytes per record (seven scalar/optional/enum slots plus component +storage policy), plus exact UTF-8 key alias bytes, 16 per open ordinal frame and +128 fixed snapshot/report bytes. Reserve each before append. Source ownership is +the original read-scope admission cost, transferred once rather than recomputed. +All record finalizations charge work and check cancellation; pop/refund a frame +only after removal. These versioned logical weights are policy rather than Swift +MemoryLayout, allocator behavior or RSS. No claimed hard physical-memory bound. + +Record retention O(nodes + key bytes), frontier O(depth). The existing walker +constructs absolute hit paths: build work may be O(sum of depths), plus admission, +key processing and finalization. Do not claim linear total construction or a speedup. + +## Separate bounded lookup + +Snapshot-scoped ordinal methods reject negative/out-of-range indices. Each path, +value or span lookup starts an explicitly separate finite core operation, admits +the captured immutable document using JSONReadScope, charges parent-chain work and +temporary/output path retention, then delegates exact selection/source-map lookup +to the core on that original scope. No lookup runs during build. Values are returned +under existing read-value semantics; a returned JSON alias is caller value storage, +not a second serialized graph. Path result ownership remains charged at handoff. +No incomplete snapshot, missing-as-null or negative-query API is available. + +## Proof gate + +Keep N1's 56 public declarations and unmodified 2-positive/20-negative full-codegen +matrix. Preserve I01-I17. Add S01-S24 structural, exact-key/number, source attachment, +quota/cancel and generated depth/breadth tests; run Debug/Release, strict, sanitizers +and qualified platform lanes. Actual source-bound results decide qualification; +this ADR alone qualifies no executable behavior. + +The builder defaults to 100,000 records inclusive. Negative configuration fails +before source admission; zero fails at the physical root with a typed capacity +error. Invalid lookup ordinals precede source admission. Finalized subtree ends +are nonoptional Int: no public or package result can carry an unfinished range. diff --git a/Documentation/Architecture/StructuralIntelligence/SnapshotPromotion.md b/Documentation/Architecture/StructuralIntelligence/SnapshotPromotion.md new file mode 100644 index 0000000..bf57585 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/SnapshotPromotion.md @@ -0,0 +1,63 @@ +# NAV6 snapshot public candidate + +This isolated draft descends from original package-only N3 872b5fd and reconciled +N2 37d84e8 (current main c79d48c). It proposes a public facade inside the existing +JSONInspection product; it creates no additional SwiftPM product. No merge or +release is authorized. Original #41/#42 evidence identities remain unchanged. + +## Accepted interface + +Keep JSONStructuralSnapshotBuilder(document:limits:), build(maximumRecords:), +JSONStructuralSnapshot.document/recordCount, record(at:), location/value/sourceSpan +(for:limits:), JSONStructuralRecord and the two existing typed failure cases. +There are no public record/snapshot initializers or setters. The implementation +remains the original package/private algorithm; no scope, meter, reducer, observer, +borrowed cursor or source-rebinding capability becomes public. + +An ordinal is an integer position local to the snapshot method being called, +never a detached node identity. Two sources may both have position 1 with different +values; explicitly selecting each snapshot selects its own original source. No +method accepts a record, handle or replacement document. The immutable document +attachment keeps old snapshots coherent after other values are patched/replaced. + +## Ownership and errors + +Build performs complete physical preorder under Navigation’s one original scope +and meter. Source ownership transfers once after finalized half-open subtree ends; +ordinary read/visit/collect/inspectArray never transfer and preserve refunds. All +three counters (record count, open depth, subtree end) are bounded by arrays and +admitted nodes. Parent assignment uses existing indices; end equals records.count; +ordinal < maximumRecords is checked before append, so count can at most Int.max +without an increment from Int.max. No end/count arithmetic adds unchecked user +integers. Core retention/work increments use checked UInt64 accounting. Supported +wasm32 capacities retain the existing admission policy; no new narrowing occurs. + +Negative maximumRecords precedes admission; zero refuses the real root. No quota, +cancellation, incomplete traversal or finalization failure returns an artifact. +The exclusive context latches owned failures, including second transfer. A direct +package read-scope transfer guard is an implementation primitive; public N3 +construction reaches it only through that guarded context. Post-transfer caller +errors and quota/cancellation still invalidate the scope and publish no result. + +## Costs and qualification + +Overlay v1 remains 128 fixed logical bytes, 96 per record, edge-key UTF-8 bytes, +16 per open frame, plus original source/path costs. Compact record retention is +O(nodes + key bytes), while existing walker full-path storage and copying remain +depth-sensitive. record(at:) is O(1) trusted-memory metadata access. Each location, +value or span lookup is an explicitly separate finite operation with full source +re-admission and core prefix/path/selection work. K lookups pay K admissions. These +are semantic costs, not new performance claims; logical bytes are not heap or RSS. + +All I01–I17 and S01–S24 identities remain. Three new proofs add an independent +28-record oracle, post-transfer terminal/lifetime cases and concurrent lookup +health isolation. Exact discovery rises to 747 native / 731 Linux / 643 WASI, +with 325/288 parameter invocations unchanged, 44 Inspection functions and 50/49 +shared-state functions. Preserve 49 cap probes, 126 maintenance identities, +2-positive/20-negative full codegen, composition and schema-example lanes. Public +qualification adds fourteen external cases, three executable DocC examples and +18 external full-codegen negative outcomes. Requirements become passes only when +actual source-bound receipts say so; SDK compilation is not device execution. + +Read the [compiled guide](../../../Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md) +and [independent consumer](../../../Tools/SnapshotQualification). diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 02bcb04..5678253 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,8 @@ +# NAV6 snapshot integration + +Retains current main and all N2/N3 cases. Counts below are historical; the +source-bound discovery manifest and receipts determine this candidate’s inventory. + # NAV6 integrated inspection candidate Adds Inspection to schema-example-enabled main: ten products, nine libraries, @@ -21,6 +26,19 @@ The following sections preserve their original increment histories. # NAV5 public inspection candidate +# NAV5 package-only snapshot experiment + +This stacked candidate retains the N2 public surface and adds 24 snapshot cases. +Root function inventories are 646 native, 630 Linux and 542 WASI; parameter case +inventories remain 325 native/Linux and 288 WASI. Inspection has 41 functions; +the focused inspection lane selects inspection|snapshot with exact discovery. +Shared-state inventories add snapshotS19 (45 native, 44 Linux). No cap probe, +restored maintenance identity, product or public declaration is added by N3. +The new package-only source-ownership transfer preserves default input refunds; +full compiler/safety and source-bound receipts remain required. + +## NAV5 public inspection candidate + The candidate adds JSONInspection as the ninth product (eight libraries plus CLI). Its existing I01-I17 identities, root discovery, 49 cap probes and 126 restored maintenance identities are unchanged. Native/Linux invoke the external inspection diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index c55f9b9..65078da 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 17 + "JSONInspectionTests": 44 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -35,7 +35,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 1 + "JSONInspectionTests": 3 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -76,7 +76,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 17 + "JSONInspectionTests": 44 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,7 +89,7 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 17 + "JSONInspectionTests": 44 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -103,7 +103,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 1 + "JSONInspectionTests": 3 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/README.md b/README.md index 73e3f74..0f7d974 100644 --- a/README.md +++ b/README.md @@ -660,3 +660,12 @@ Full source admission still includes the unobserved suffix. Complete row evidenc can have restricted descendant traversal; structural rectangular width is not numeric matrix or schema admission. See the [compiled evidence guide](Sources/JSONInspection/Documentation.docc/ArrayEvidence.md), [public consumer](Tools/InspectionQualification) and [accounting contract](Documentation/Architecture/StructuralIntelligence/Inspection.md). + +The isolated NAV6 candidate also proposes complete source-attached structural +snapshots inside JSONInspection. Read-only parent/edge records preserve physical +preorder and finalized subtree ranges; every value/path/span lookup uses the +captured document and an explicitly separate finite source admission. It adds no +snapshot product, persistent identity or table certification. See the +[snapshot guide](Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md) +and [public consumer](Tools/SnapshotQualification). This API remains an unmerged, +unreleased draft candidate; published products retain their tagged contracts. diff --git a/Scripts/ci.py b/Scripts/ci.py index 4b341c6..606dea7 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -14,6 +14,7 @@ SHARED_TESTS += '|g1SharedPreparedGeneratorHasIndependentConcurrentInvocations|g3SharedTypedGeneratorHasIndependentConcurrentInvocations|g4SharedAdvancedGeneratorHasIndependentConcurrentInvocations|g5SharedPreparationSeparatesConcurrentFailureAndSuccessHealth' SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' +SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -24,9 +25,9 @@ def commands(lane, output): output = output.relative_to(ROOT) if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], - ['swift', 'test', '--filter', 'inspection'], - ['swift', 'test', '-c', 'release', '--filter', 'inspection'], - ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection'], + ['swift', 'test', '--filter', 'inspection|snapshot'], + ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot'], ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/inspection-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')]] @@ -165,6 +166,7 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_ergonomics.py'], ['python3', '-B', 'Scripts/qualify_navigation.py', '--output', str(output / 'navigation')], ['python3', '-B', 'Scripts/qualify_inspection.py', '--output', str(output / 'inspection-consumer')], + ['python3', '-B', 'Scripts/qualify_snapshot.py', '--output', str(output / 'snapshot-consumer')], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], @@ -224,7 +226,7 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 - if 'inspection' in command: + if any(f in command for f in ['inspection', 'inspection|snapshot']): expected = [DISCOVERY['root_functions']['JSONInspectionTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 @@ -298,6 +300,8 @@ def check_result(command, code, log, events=None): passed = passed and 'ERGONOMICS_COMPILER passed commands=8 guide_fences=3' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_snapshot.py']: + passed = passed and 'SNAPSHOT_COMPILER passed configurations=2 consumer_cases=14 address_cases=14 guide_fences=3 negative_probes=18' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_inspection.py']: passed = passed and 'INSPECTION_COMPILER passed configurations=2 consumer_cases=12 address_cases=12 guide_fences=3 negative_probes=8' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation.py']: diff --git a/Scripts/qualify_snapshot.py b/Scripts/qualify_snapshot.py new file mode 100644 index 0000000..903637a --- /dev/null +++ b/Scripts/qualify_snapshot.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Qualify public snapshot imports, semantics and three executable DocC examples.""" +import argparse +import hashlib +import json +import pathlib +import re +import subprocess +import sys +from archive_candidate import files + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKS = {'roots', 'typed-paths', 'unicode', 'numbers', 'duplicate-refusal', + 'preorder-ranges', 'core-differential', 'source-binding', 'bounds', + 'separate-admission', 'capacity-configuration', 'inclusive-budgets', + 'coverage-separation', 'concurrent-lookups'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + output = ROOT / parser.parse_args().output + output.mkdir(parents=True, exist_ok=True) + inputs = [{'path': p.relative_to(ROOT).as_posix(), + 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()] + receipt = dict(status='running', source_inputs=inputs, commands=[], consumer_results={}) + + def save(): + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr + crash = result.returncode < 0 or any(x in text for x in + ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation']) + diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + passed = not crash and (result.returncode == 0 if expected is None else + result.returncode > 0 and any(expected in d for d in diagnostics)) + log = output / (name + '.log') + log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], + exit_code=result.returncode, status='passed' if passed else 'failed', compiler_crash=crash, + expected_diagnostic=expected, primary_diagnostics=diagnostics, + log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: + raise ValueError('Unexpected compiler/consumer outcome: ' + name) + return result.stdout + + def consumer(name, command): + text = run(name, command) + checks = re.findall(r'^SNAPSHOT_CHECK (\S+) passed$', text, re.M) + if len(checks) != 14 or set(checks) != CHECKS or 'SNAPSHOT_CONSUMER passed cases=14' not in text: + raise ValueError('Missing/duplicate inspection consumer checks: ' + name) + resource = re.search(r'^SNAPSHOT_RESOURCE work=(\d+) retained=(\d+) peak=(\d+) nodes=(\d+)$', text, re.M) + if resource is None or not all(int(x) > 0 for x in resource.groups()): + raise ValueError('Missing finite cumulative report: ' + name) + receipt['consumer_results'][name] = dict(cases=checks, work_units=int(resource[1]), + logical_retained_bytes=int(resource[2]), peak_logical_retained_bytes=int(resource[3]), + admitted_nodes=int(resource[4])) + save() + + try: + for config in ['debug', 'release']: + run(config + '-build', ['swift', 'build', '-c', config, '--target', 'JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift', 'build', '-c', config, '--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + objects = [] + for module in ['JSON', 'JSONNavigation', 'JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: + raise ValueError('Missing built objects: ' + module) + objects.extend(str(p) for p in selected) + common = ['swiftc', '-swift-version', '6', '-warnings-as-errors', '-package-name', + 'snapshot_external_consumer', '-I', str(modules), + '-O' if config == 'release' else '-Onone'] + consumer(config, ['swift', 'run', '-c', config, '--package-path', + 'Tools/SnapshotQualification', 'SnapshotConsumer']) + guide = ROOT / 'Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md' + fences = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(fences) != 3: + raise ValueError('Changed three-fence guide inventory') + for index, fence in enumerate(fences): + file = output / ('guide-' + str(index) + '.swift') + file.write_text('import Foundation\nimport JSON\nimport JSONNavigation\nimport JSONInspection\n' + '@main struct Guide { static func main() throws {\n' + fence + '\n} }\n') + binary = output / ('guide-' + config + '-' + str(index)) + run(config + '-guide-' + str(index), common + ['-parse-as-library', str(file), *objects, '-o', str(binary)]) + run(config + '-guide-' + str(index) + '-run', [str(binary)]) + probes = [ + ('core-import-isolation', 'import JSON\nlet x = JSONStructuralSnapshotBuilder.self\n', 'cannot find'), + ('scope-isolation', 'import JSON\nlet x = JSONReadScope.self\n', 'cannot find'), + ('meter-isolation', 'import JSON\nlet x = JSONWorkMeter.self\n', 'cannot find'), + ('capability-isolation', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find'), + ('reducer-isolation', 'import JSONNavigation\nlet x = JSONTraversalOwnedReducer.self\n', 'cannot find'), + ('record-is-readonly', 'import JSONInspection\nfunc change(_ x: inout JSONStructuralRecord) { x.subtreeEndExclusive = 7 }\n', 'setter is inaccessible'), + ('source-is-readonly', 'import JSON\nimport JSONInspection\nfunc change(_ x: inout JSONStructuralSnapshot) { x.document = .init(root: .null) }\n', "'let' constant"), + ('snapshot-is-unforgeable', 'import JSON\nimport JSONInspection\nlet x = JSONStructuralSnapshot(document: .init(root: .null), records: [])\n', 'initializer is inaccessible'), + ('no-source-rebinding', 'import JSON\nimport JSONInspection\nfunc change(_ x: JSONStructuralSnapshot) throws { _ = try x.value(for: 0, document: .init(root: .null)) }\n', "extra argument 'document'"), + ] + for name, source, expected in probes: + file = output / (name + '.swift'); file.write_text(source) + run(config + '-' + name, common + ['-parse-as-library', '-c', str(file), + '-o', str(output / (config + '-' + name + '.o'))], expected) + consumer('address', ['swift', 'run', '-c', 'release', '--sanitize', 'address', + '--scratch-path', '.build/snapshot-consumer-address', '--package-path', + 'Tools/SnapshotQualification', 'SnapshotConsumer']) + receipt['status'] = 'passed' + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + receipt.update(status='failed', error=str(error)) + save() + print('SNAPSHOT_COMPILER ' + receipt['status'] + + ' configurations=2 consumer_cases=14 address_cases=14 guide_fences=3 negative_probes=18') + return 0 if receipt['status'] == 'passed' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 8089704..ea91700 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -35,7 +35,7 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel # Combined-source checks must retain both independently owned optional products. for module, count, command in [ ('JSONNavigationTests', 47, ['swift', 'test', '--filter', 'navigation']), - ('JSONInspectionTests', 17, ['swift', 'test', '--filter', 'inspection']), + ('JSONInspectionTests', 44, ['swift', 'test', '--filter', 'inspection']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, @@ -49,12 +49,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 47) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONInspectionTests'], 17) + self.assertEqual(policy[key]['JSONInspectionTests'], 44) self.assertEqual(policy[key]['JSONIntegrationTests'], 32) for key in ('shared_state_functions', 'portable_shared_state_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 1) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 4) - self.assertEqual(policy[key]['JSONInspectionTests'], 1) + self.assertEqual(policy[key]['JSONInspectionTests'], 3) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def setUp(self): diff --git a/Sources/JSON/Reading/JSONReadScope.swift b/Sources/JSON/Reading/JSONReadScope.swift index 1c9dbef..68f8558 100644 --- a/Sources/JSON/Reading/JSONReadScope.swift +++ b/Sources/JSON/Reading/JSONReadScope.swift @@ -10,6 +10,7 @@ package final class JSONReadScope { private var rootBytes: UInt64 = 0 private var pathBytes: UInt64 = 0 private var retiredPathBytes: UInt64 = 0 + private var resultOwnsDocument = false package init(_ document: JSONDocument, limits: JSONLimits) throws { let state = JSONCodingState(options: .init(limits: limits), meter: try JSONWorkMeter(limits: limits, domain: .core, trackAcceptedCapacities: true)) try JSONAdmission.value(document.root, meter: &state.meter, owned: true) @@ -41,11 +42,23 @@ package final class JSONReadScope { try state.check(); return state } package func invalidate() { document = nil; state = nil } + /// Transfer the original admitted source alias into a source-attached result. + /// Only an owned package reducer uses this one-shot completion designation; + /// ordinary reads/visits retain their original input-refund behavior. + package func transferDocumentOwnership() throws -> JSONDocument { + let state = try check() + try state.meter.work() + guard !resultOwnsDocument, let document else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + resultOwnsDocument = true + return document + } package func complete(_ result: T) throws -> JSONOperationReport { let state = try check() // Paths/root/source cease to be scope-owned. Any escaped path is caller // storage and cannot access this invalidated root or ledger. - try state.meter.release(rootBytes + pathBytes) + try state.meter.release((resultOwnsDocument ? 0 : rootBytes) + pathBytes) let statistics = state.meter.statistics() invalidate() return .init(result: result, statistics: statistics) diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index e13663e..9468a3e 100644 --- a/Sources/JSONCLIEngine/CLIBuildIdentity.swift +++ b/Sources/JSONCLIEngine/CLIBuildIdentity.swift @@ -2,6 +2,6 @@ // Generated by Scripts/generate_cli_identity.py; excludes this file. // SHA-256 of the ordered Package.swift + Sources content manifest, not a Git commit. package enum CLIBuildIdentity { - package static let revision = "sha256:f6d5bbb6deae7bf922c46b72f893412ef83461fa60fb83564db4d7f417587eb6" + package static let revision = "sha256:f128cc89ec2fdc9be025d0628cec84b4629730d58907ef50851f3e928f618419" package static let kind = "source-sha256" } diff --git a/Sources/JSONInspection/Documentation.docc/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md index 41ab5b6..5b58e79 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -24,3 +24,11 @@ or numeric matrix. Sampling does not exempt any source from admission. - ``JSONShapeKindCounts`` - ``JSONColumnObservation`` - ``JSONShapeInspectionFailure`` + +### Capture complete source structure + +- +- ``JSONStructuralSnapshotBuilder`` +- ``JSONStructuralSnapshot`` +- ``JSONStructuralRecord`` +- ``JSONStructuralSnapshotFailure`` diff --git a/Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md b/Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md new file mode 100644 index 0000000..26e8359 --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/StructuralSnapshots.md @@ -0,0 +1,85 @@ +# Index complete physical structure + +Build source-attached metadata without replacing the canonical JSON document. + +## Capture even a scalar root + +```swift +let source = JSONDocument(root: .null) +let report = try JSONStructuralSnapshotBuilder(document: source).build() +let snapshot = report.result +let root = try snapshot.record(at: 0) +precondition(snapshot.recordCount == 1) +precondition(root.parentOrdinal == nil && root.componentFromParent == nil) +precondition(root.depth == 0 && root.childCount == 0 && root.subtreeEndExclusive == 1) +let value = try snapshot.value(for: 0) +precondition(value.result == .null) +``` + +The builder always covers one entire physical root, including null and empty +containers. No filter, prefix, prune, depth selection or caller callback can return +an incomplete snapshot. A negative maximumRecords fails configuration before +admission. Zero refuses the real root with a typed capacity error; there is no +successful empty or partial artifact. Quota exhaustion and cancellation throw. + +## Keep paths and numbers exact + +```swift +let source = try JSONParser().parseDocument(Data(#"{"":null,"0":[9007199254740993,0.0100]}"#.utf8)) +let snapshot = try JSONStructuralSnapshotBuilder(document: source).build().result +let rootPath = try snapshot.location(for: 0).result +let emptyKeyPath = try snapshot.location(for: 1).result +let arrayElementPath = try snapshot.location(for: 3).result +precondition(rootPath.isEmpty && emptyKeyPath == [.key("")]) +precondition(arrayElementPath == [.key("0"), .index(0)]) +let integer = try snapshot.value(for: 3).result.numberValue!.integer(as: UInt64.self) +let lexeme = try snapshot.value(for: 4).result.numberValue!.originalLexeme +precondition(integer == 9_007_199_254_740_993 && lexeme == "0.0100") +let root = try snapshot.record(at: 0) +precondition(root.subtreeEndExclusive == snapshot.recordCount) +``` + +Object members retain insertion order and decoded scalar key identity. Array +indices increase from zero. Root [] differs from an empty key; key "0" differs +from index 0. Parent ordinals are lower than children; finalized half-open subtree +ranges include the node and all its descendants, not merely direct children. +Numbers remain in the exact canonical source, with no binary64 projection. + +## Preserve source binding and explicit lookup budgets + +```swift +let document = try JSONParser().parseDocument(Data(#"{"old":null}"#.utf8)) +let snapshot = try JSONStructuralSnapshotBuilder(document: document).build().result +let replacement = document.replacingRoot(.array([42])) +let newer = try JSONStructuralSnapshotBuilder(document: replacement).build().result +let oldPath = try snapshot.location(for: 1) +let newPath = try newer.location(for: 1) +precondition(oldPath.result == [.key("old")] && newPath.result == [.index(0)]) +let oldSpan = try snapshot.sourceSpan(for: 1) +let newSpan = try newer.sourceSpan(for: 1) +precondition(oldSpan.result != nil && newSpan.result == nil) +precondition(oldPath.statistics[.nodes] == 2 && newPath.statistics[.nodes] == 2) +``` + +Ordinals are positions local to the particular snapshot method being called. +A record carries metadata, not a portable node identity. No method accepts a +replacement source or detached handle. An older snapshot keeps its original +immutable source and coherent map; it never follows another document’s revision. +Only original mapped spans are available. Invalid ordinals throw typed bounds +errors before admission rather than returning null, missing or a default record. + +Construction uses Navigation’s original single cumulative read scope and meter. +It transfers the already-admitted document ownership once after complete range +finalization, retaining one source and parent/edge records, without per-node JSON +values, full paths or source maps. Logical overlay v1 reserves 128 fixed bytes, +96 per record, exact edge-key UTF-8 bytes and 16 per open frame, plus original +source/path costs. These policy weights are not Swift heap layout or process RSS. + +record(at:) is O(1) metadata access without opening a new ledger. Each location, +value or sourceSpan call deliberately opens a separate finite operation and +re-admits the whole captured document, even for root or unmapped-span lookup. +Repeated K lookups pay K admissions; they do not share the builder’s remainder. +Path reconstruction also uses depth-sensitive core prefix copies and selection. +The existing traversal builds full hit paths, so total snapshot build work is +not guaranteed linear in node count. No batch session, persistence, stable source +fingerprint, table certification or new JSON representation is provided. diff --git a/Sources/JSONInspection/JSONStructuralSnapshot.swift b/Sources/JSONInspection/JSONStructuralSnapshot.swift new file mode 100644 index 0000000..52ba154 --- /dev/null +++ b/Sources/JSONInspection/JSONStructuralSnapshot.swift @@ -0,0 +1,240 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Read-only DFS preorder metadata for one physical node in a complete snapshot. +/// Ordinals are local positions in the containing snapshot, not transferable node +/// identities. A record cannot resolve a value or attach to a replacement source. +public struct JSONStructuralRecord: Sendable { + /// Zero-based preorder position within the containing snapshot. + public let ordinal: Int + /// Parent position, strictly lower than this ordinal; nil only for the root. + public let parentOrdinal: Int? + /// Exact key or array index from the parent; nil only for the physical root. + public let componentFromParent: JSONLocationComponent? + /// Canonical JSON kind; no inferred schema, missing or table kind. + public let kind: JSONTraversalKind + /// Number of physical edges from the root, whose depth is zero. + public let depth: Int + /// Number of immediate children; a scalar or empty container has zero. + public let childCount: Int + /// Finalized exclusive preorder end; this node’s subtree is [ordinal, end). + public internal(set) var subtreeEndExclusive: Int +} + +/// A capacity or local-position refusal; no partial snapshot or null substitute. +public enum JSONStructuralSnapshotFailure: Error, Sendable, Equatable { + /// The next physical node exceeds the inclusive record bound. + /// - Parameter limit: Nonnegative maximum configured for this build. + case recordLimitExceeded(limit: Int) + /// The supplied local position is outside 0.. JSONStructuralRecord { + guard ordinal >= 0, ordinal < records.count else { + throw JSONStructuralSnapshotFailure.ordinalOutOfBounds(ordinal) + } + return records[ordinal] + } + + /// Reconstructs an exact typed location in a separate bounded operation. + /// Re-admits the entire captured root/source/map before parent-chain/path work. + /// Core prefix copying can be quadratic in depth; this is not O(1) lookup. + /// - Parameters: + /// - ordinal: Position local to this snapshot; bounds checked before admission. + /// - limits: Finite quotas for this new lookup, independent of construction. + /// - Returns: Exact key/index path and cumulative lookup statistics; [] is root. + /// - Throws: Typed bounds, core resource/configuration failures or cancellation. + public func location(for ordinal: Int, limits: JSONLimits = .balanced) + throws -> JSONOperationReport<[JSONLocationComponent]> { + try lookup(ordinal, limits: limits) { path, state, _ in + // Output path ownership survives scope completion's temporary refund. + for part in path { + try state.meter.work(); try state.meter.retain(JSONLogicalWeights.indexSlot) + if case .key(let key) = part { + for _ in key.string.utf8 { try state.meter.work(); try state.meter.retain(1) } + } + } + return path + } + } + + /// Resolves a value only against the captured source in a new bounded operation. + /// Each call re-admits the full source and reconstructs its typed path before + /// core selection. The result uses existing immutable read-value alias semantics; + /// no JSON subtree is cached in a record or converted through binary floating point. + /// - Parameters: + /// - ordinal: Position local to this snapshot; invalid positions never become null. + /// - limits: Finite quotas for this separate lookup, not the builder’s remainder. + /// - Returns: The canonical value, including explicit null, and lookup statistics. + /// - Throws: Typed bounds, core access/resource/configuration errors or cancellation. + public func value(for ordinal: Int, limits: JSONLimits = .balanced) + throws -> JSONOperationReport { + try lookup(ordinal, limits: limits) { path, state, scope in + try state.meter.work() + return try scope.select(path, state: state) + } + } + + /// Looks up a coherent original value span in a new bounded operation. + /// Full captured-source admission and path reconstruction occur even without a + /// source map. A replacement/unmapped document has no invented historical span. + /// - Parameters: + /// - ordinal: Position local to this snapshot; bounds checked before admission. + /// - limits: Finite quotas for this separate source-map lookup. + /// - Returns: Original mapped span or nil when unavailable, plus lookup statistics. + /// - Throws: Typed bounds, core resource/configuration failures or cancellation. + public func sourceSpan(for ordinal: Int, limits: JSONLimits = .balanced) + throws -> JSONOperationReport { + try lookup(ordinal, limits: limits) { path, state, _ in + try document.sourceMap?.span(at: path, kind: .value, meter: &state.meter) + } + } + + private func lookup(_ ordinal: Int, limits: JSONLimits, + _ body: ([JSONLocationComponent], JSONCodingState, JSONReadScope) throws -> Result) + throws -> JSONOperationReport { + // Invalid ordinals precede admission; never a null/missing default. + _ = try record(at: ordinal) + let scope = try JSONReadScope(document, limits: limits) + defer { scope.invalidate() } + let state = try scope.check() + var reversed: [JSONLocationComponent] = [], temporary: UInt64 = 0 + var current = ordinal + while let parent = records[current].parentOrdinal { + try state.meter.work() + guard parent >= 0, parent < current, let component = records[current].componentFromParent else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try state.meter.retain(JSONLogicalWeights.indexSlot) + temporary = try state.meter.adding(.retainedBytes, temporary, JSONLogicalWeights.indexSlot) + if case .key(let key) = component { + for _ in key.string.utf8 { + try state.meter.work(); try state.meter.retain(1) + temporary = try state.meter.adding(.retainedBytes, temporary, 1) + } + } + reversed.append(component); current = parent + } + var path = JSONReadPath(scope: scope, components: [], bytes: 0) + for component in reversed.reversed() { + try state.meter.work() + path = try scope.append(component, to: path.components, state: state) + _ = try scope.check() + } + reversed = []; try state.meter.release(temporary) + return try scope.complete(scope.run(path.components) { try body(path.components, $0, scope) }) + } +} + +/// Builds complete source-attached metadata with one original cumulative ledger. +/// No filtering, pruning, prefix, callback, per-node JSON copy or second admission. +/// Records retain parent/edge metadata and one immutable document. Existing walker +/// hit paths mean total construction is depth-sensitive, not guaranteed O(nodes). +/// Logical overlay v1 is 128 fixed bytes, 96/record, exact UTF-8 edge-key bytes and +/// 16/live open frame, plus source/path admission. These weights are not heap/RSS. +public struct JSONStructuralSnapshotBuilder: Sendable { + /// Canonical immutable source; admitted once when build begins. + public let document: JSONDocument + /// Finite construction quotas shared by admission, traversal and finalization. + public let limits: JSONLimits + package var checkpointObserver: (@Sendable (UInt64) -> Void)? + package var lifetimeObserver: (@Sendable (Bool, Bool) -> Void)? + /// Captures trusted source and policy; this initializer performs no admission. + /// - Parameters: + /// - document: Physical canonical root, optionally with coherent source mapping. + /// - limits: One finite profile for the complete construction invocation. + public init(document: JSONDocument, limits: JSONLimits = .balanced) { + self.document = document; self.limits = limits + } + /// Returns every physical node with finalized ranges, or throws without an artifact. + /// Negative capacity fails before admission; zero refuses the real root. Even + /// null and empty containers have one record. Source ownership transfers once + /// only after complete traversal; temporary scope references are invalidated. + /// - Parameter maximumRecords: Inclusive nonnegative bound, default 100,000. + /// - Returns: Immutable complete snapshot and cumulative construction statistics. + /// - Throws: Typed capacity refusal, invalid configuration, resource exhaustion + /// or cancellation. No successful partial snapshot or terminal-error override. + public func build(maximumRecords: Int = 100_000) + throws -> JSONOperationReport { + guard maximumRecords >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + var reducer = SnapshotReducer(maximumRecords: maximumRecords) + var traversal = JSONTraversal(document: document, limits: limits) + traversal.checkpointObserver = checkpointObserver + traversal.lifetimeObserver = lifetimeObserver + return try traversal.inspectOwned(reducer: &reducer) + } +} + +/// Logical overlay v1: 128 fixed, 96/record + exact key bytes, 16/open frame. +private struct SnapshotReducer: JSONTraversalOwnedReducer { + let maximumRecords: Int + var records: [JSONStructuralRecord] = [] + var open: [Int] = [] + + mutating func close(_ end: Int, context: inout JSONTraversalContext) throws { + try context.work() + guard let ordinal = open.last, ordinal < end else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + records[ordinal].subtreeEndExclusive = end + open.removeLast(); try context.release(16) + } + + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work() + let ordinal = records.count, depth = hit.relativeDepth + while open.count > depth { try close(ordinal, context: &context) } + guard open.count == depth, ordinal < maximumRecords else { + if ordinal >= maximumRecords { throw JSONStructuralSnapshotFailure.recordLimitExceeded(limit: maximumRecords) } + throw JSONFailure(.configuration, code: .invalidOperation) + } + if ordinal == 0 { try context.retain(128) } + let component = depth == 0 ? nil : hit.absoluteLocation.last + guard (depth == 0) == (component == nil) else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try context.retain(96) + if case .key(let key) = component { try context.retainKey(key) } + let childCount: Int + switch value { case .object(let o): childCount = o.count; case .array(let a): childCount = a.count; default: childCount = 0 } + records.append(.init(ordinal: ordinal, parentOrdinal: open.last, componentFromParent: component, + kind: hit.kind, depth: depth, childCount: childCount, subtreeEndExclusive: 0)) + try context.retain(16); open.append(ordinal) + return .continue + } + + mutating func finish(_ summary: JSONTraversalSummary, + context: inout JSONTraversalContext) throws -> JSONStructuralSnapshot { + guard summary.completion == .complete, + summary.visitedCount == UInt64(records.count), + summary.emittedCount == UInt64(records.count), !records.isEmpty else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + while !open.isEmpty { try close(records.count, context: &context) } + let document = try context.transferDocumentOwnership() + let result = JSONStructuralSnapshot(document: document, records: records) + records = [] + return result + } +} diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift index e075725..a08e58e 100644 --- a/Sources/JSONNavigation/JSONTraversalContext.swift +++ b/Sources/JSONNavigation/JSONTraversalContext.swift @@ -69,4 +69,8 @@ package struct JSONTraversalContext: ~Copyable { try scope.document?.sourceMap?.span(at: path, kind: .value, meter: &state.meter) } } + /// Transfer already-admitted input ownership, without exposing its scope. + package mutating func transferDocumentOwnership() throws -> JSONDocument { + try perform { _ in try scope.transferDocumentOwnership() } + } } diff --git a/Tests/JSONInspectionTests/SnapshotAcceptanceTests.swift b/Tests/JSONInspectionTests/SnapshotAcceptanceTests.swift new file mode 100644 index 0000000..3f4875f --- /dev/null +++ b/Tests/JSONInspectionTests/SnapshotAcceptanceTests.swift @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +import JSONInspection + +private func snapshot(_ text: String) throws -> JSONStructuralSnapshot { + try JSONStructuralSnapshotBuilder(document: JSONParser().parseDocument(Data(text.utf8))).build().result +} +private func snapshotFailure(_ body: () throws -> Void) -> JSONFailure? { + do { try body(); Issue.record("Expected resource failure") } + catch let error as JSONFailure { return error } + catch { Issue.record("Unexpected failure: \(error)") } + return nil +} + +@Test func snapshotS01ScalarAndEmptyRootsAreRealRecords() throws { + for text in ["null", "false", "1", #""s""#, "[]", "{}"] { + let result = try snapshot(text), root = try result.record(at: 0) + #expect(result.recordCount == 1 && root.parentOrdinal == nil && root.componentFromParent == nil) + #expect(root.ordinal == 0 && root.depth == 0 && root.childCount == 0 && root.subtreeEndExclusive == 1) + #expect(try result.location(for: 0).result.isEmpty) + #expect(try result.value(for: 0).result == result.document.root) + } +} + +@Test func snapshotS02RootEmptyAndNumericKeysRemainDistinct() throws { + let result = try snapshot(#"{"":1,"0":2}"#) + #expect(try result.location(for: 0).result == []) + #expect(try result.location(for: 1).result == [.key("")]) + #expect(try result.location(for: 2).result == [.key("0")]) + #expect(try result.record(at: 2).componentFromParent != .index(0)) +} + +@Test func snapshotS03NestedArrayPositionsIncreaseFromZero() throws { + let result = try snapshot("[[null,false],[]]") + let paths = try (0..= threshold { observed.withLock { $0 = true }; withUnsafeCurrentTask { $0?.cancel() } } + } + builder.lifetimeObserver = { rootGone, stateGone in invalidated.withLock { $0 = rootGone && stateGone } } + return try builder.build() + } + do { _ = try await task.value; Issue.record("No artifact may escape cancellation") } + catch is CancellationError { } + #expect(observed.withLock { $0 } && invalidated.withLock { $0 }) + } +} + +@Test func snapshotS19NavigationParityAndIndependentInvocations() async throws { + let document = try JSONParser().parseDocument(Data(#"{"rows":[{"a":null},{"b":1}]}"#.utf8)) + let traversal = JSONTraversal(document: document) + let before = try traversal.collect() + let builder = JSONStructuralSnapshotBuilder(document: document) + let baseline = try builder.build() + try await withThrowingTaskGroup(of: UInt64.self) { group in + for _ in 0..<4 { group.addTask { try builder.build().statistics[.workUnits] } } + for try await work in group { #expect(work == baseline.statistics[.workUnits]) } + } + let after = try traversal.collect() + #expect(before.result.nodes.map(\.absoluteLocation) == after.result.nodes.map(\.absoluteLocation)) + #expect(before.statistics[.workUnits] == after.statistics[.workUnits]) + #expect(before.statistics[.retainedBytes] == after.statistics[.retainedBytes]) + #expect(try JSONShapeInspector(document: document).inspectArray(at: [.key("rows")]).result.rowCoverage == .complete) +} + +@Test func snapshotS20OneOriginalAdmissionAndScopeRelease() throws { + let document = try JSONParser().parseDocument(Data(#"{"a":[1,2]}"#.utf8)) + let ordinary = try JSONTraversal(document: document).visit { _, _ in .continue } + let invalidated = Mutex(false) + var builder = JSONStructuralSnapshotBuilder(document: document) + builder.lifetimeObserver = { rootGone, stateGone in invalidated.withLock { $0 = rootGone && stateGone } } + let result = try builder.build() + #expect(result.statistics[.nodes] == ordinary.statistics[.nodes]) + #expect(result.statistics[.sourceMapEntries] == ordinary.statistics[.sourceMapEntries]) + #expect(invalidated.withLock { $0 }) + #expect(try result.result.value(for: 3).result == JSONParser().parse("2")) +} + +@Test func snapshotS21InclusiveWorkAndRetentionBudgets() throws { + let document = JSONDocument(root: .array([.null,.boolean(false)])) + let baseline = try JSONStructuralSnapshotBuilder(document: document).build() + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + #expect(try JSONStructuralSnapshotBuilder(document: document, limits: limits).build().result.recordCount == 3) + limits[.workUnits] -= 1 + #expect(snapshotFailure { _ = try JSONStructuralSnapshotBuilder(document: document, limits: limits).build() }?.resource?.attemptedUsage == baseline.statistics[.workUnits]) + limits = .balanced; limits[.retainedBytes] = baseline.statistics.peakLogicalRetainedBytes + #expect(try JSONStructuralSnapshotBuilder(document: document, limits: limits).build().statistics.peakLogicalRetainedBytes == limits[.retainedBytes]) + limits[.retainedBytes] -= 1 + #expect(snapshotFailure { _ = try JSONStructuralSnapshotBuilder(document: document, limits: limits).build() }?.resource?.metric == .retainedBytes) +} + +@Test func snapshotS22IterativeDepthAndBreadthStayFinite() throws { + var deep = JSON.null + for _ in 0..<128 { deep = .array([deep]) } + let depth = try JSONStructuralSnapshotBuilder(document: JSONDocument(root: deep)).build() + let wide = try JSONStructuralSnapshotBuilder(document: JSONDocument(root: .array(Array(repeating: .null, count: 1000)))).build() + #expect(depth.result.recordCount == 129 && wide.result.recordCount == 1001) + #expect(try depth.result.location(for: 128).result.count == 128) + #expect(try wide.result.location(for: 1000).result == [.index(999)]) + print("SNAPSHOT_RESOURCE depth=128 nodes=\(depth.result.recordCount) work=\(depth.statistics[.workUnits]) retained=\(depth.statistics[.retainedBytes]) peak=\(depth.statistics.peakLogicalRetainedBytes)") + print("SNAPSHOT_RESOURCE width=1000 nodes=\(wide.result.recordCount) work=\(wide.statistics[.workUnits]) retained=\(wide.statistics[.retainedBytes]) peak=\(wide.statistics.peakLogicalRetainedBytes)") +} + +@Test func snapshotS23SourceTransferIsOneShotAndOrdinaryReadsRefund() throws { + let document = JSONDocument(root: .array([.null])) + let scope = try JSONReadScope(document, limits: .balanced) + let retained = try scope.check().meter.used(.retainedBytes) + let source = try scope.transferDocumentOwnership() + #expect(source.root == document.root) + #expect(throws: JSONFailure.self) { _ = try scope.transferDocumentOwnership() } + #expect(try scope.complete(source).statistics[.retainedBytes] == retained) + #expect(scope.document == nil && scope.state == nil) + #expect(try document.readWithReport { _ in true }.statistics[.retainedBytes] == 0) +} + +@Test func snapshotS24LookupsAreSeparateBoundedOperations() throws { + let result = try snapshot(#"{"a":[{"b":null}]}"#) + let baseline = try result.location(for: 3) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + #expect(try result.location(for: 3, limits: limits).result == [.key("a"),.index(0),.key("b")]) + limits[.workUnits] -= 1 + #expect(snapshotFailure { _ = try result.location(for: 3, limits: limits) }?.resource?.metric == .workUnits) + limits = .balanced; limits[.retainedBytes] = baseline.statistics.peakLogicalRetainedBytes + #expect(try result.location(for: 3, limits: limits).result.count == 3) + limits[.retainedBytes] -= 1 + #expect(snapshotFailure { _ = try result.location(for: 3, limits: limits) }?.resource?.metric == .retainedBytes) + limits = .balanced; limits[.pointerTokens] = 2 + let pathFailure = snapshotFailure { _ = try result.location(for: 3, limits: limits) } + #expect(pathFailure?.resource?.metric == .pointerTokens && pathFailure?.resource?.attemptedUsage == 3) + limits = .balanced; limits[.nodes] = 0 + #expect(snapshotFailure { _ = try result.value(for: 3, limits: limits) }?.resource?.metric == .nodes) + #expect(try result.value(for: 3).result == .null) // Existing explicit null; no absent/default API. + #expect(baseline.statistics[.retainedBytes] > 0) +} diff --git a/Tests/JSONInspectionTests/SnapshotPromotionTests.swift b/Tests/JSONInspectionTests/SnapshotPromotionTests.swift new file mode 100644 index 0000000..9823dae --- /dev/null +++ b/Tests/JSONInspectionTests/SnapshotPromotionTests.swift @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +import JSONInspection + +// Independent NAV6 packet oracle; no expected topology is generated by the walker. +@Test func snapshotV12IndependentMixedOracleMatchesEveryRecord() throws { + let text = "{\n \"\": {\"0\": [null, {\"id\": 9007199254740993, \"amount\": 0.0100}]},\n \"0\": [\"numeric object key, not index\"],\n \"é\": \"composed\",\n \"é\": \"decomposed\",\n \"rows\": [\n {\"z\": null, \"a\": 1},\n {\"a\": 2},\n {},\n {\"b\": 3, \"a\": 4},\n false\n ],\n \"matrix\": [[0.1, 2], [3]]\n}\n" + let result = try JSONStructuralSnapshotBuilder(document: JSONParser().parseDocument(Data(text.utf8))).build().result + let expected: [(Int?, JSONLocationComponent?, JSONTraversalKind, Int, Int, Int)] = [ + (nil, nil, .object, 0, 6, 28), + (0, .key(""), .object, 1, 1, 7), + (1, .key("0"), .array, 2, 2, 7), + (2, .index(0), .null, 3, 0, 4), + (2, .index(1), .object, 3, 2, 7), + (4, .key("id"), .number, 4, 0, 6), + (4, .key("amount"), .number, 4, 0, 7), + (0, .key("0"), .array, 1, 1, 9), + (7, .index(0), .string, 2, 0, 9), + (0, .key("é"), .string, 1, 0, 10), + (0, .key("é"), .string, 1, 0, 11), + (0, .key("rows"), .array, 1, 5, 22), + (11, .index(0), .object, 2, 2, 15), + (12, .key("z"), .null, 3, 0, 14), + (12, .key("a"), .number, 3, 0, 15), + (11, .index(1), .object, 2, 1, 17), + (15, .key("a"), .number, 3, 0, 17), + (11, .index(2), .object, 2, 0, 18), + (11, .index(3), .object, 2, 2, 21), + (18, .key("b"), .number, 3, 0, 20), + (18, .key("a"), .number, 3, 0, 21), + (11, .index(4), .boolean, 2, 0, 22), + (0, .key("matrix"), .array, 1, 2, 28), + (22, .index(0), .array, 2, 2, 26), + (23, .index(0), .number, 3, 0, 25), + (23, .index(1), .number, 3, 0, 26), + (22, .index(1), .array, 2, 1, 28), + (26, .index(0), .number, 3, 0, 28), + ] + #expect(result.recordCount == expected.count) + for ordinal in expected.indices { + let record = try result.record(at: ordinal), want = expected[ordinal] + #expect(record.ordinal == ordinal && record.parentOrdinal == want.0) + #expect(record.componentFromParent == want.1 && record.kind == want.2) + #expect(record.depth == want.3 && record.childCount == want.4 && record.subtreeEndExclusive == want.5) + } +} + +private enum TransferMarker: Error { case afterTransfer } +private struct TransferProbe: JSONTraversalOwnedReducer { + enum Mode: Sendable { case twice, work, caller, cancellation } + let mode: Mode + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { .continue } + mutating func finish(_ summary: JSONTraversalSummary, + context: inout JSONTraversalContext) throws -> JSONDocument { + let document = try context.transferDocumentOwnership() + switch mode { + case .twice: + do { _ = try context.transferDocumentOwnership() } catch { } + case .work: + do { try context.work(64_000_000) } catch { } + case .cancellation: + withUnsafeCurrentTask { $0?.cancel() } + do { try context.checkpoint() } catch { } + case .caller: throw TransferMarker.afterTransfer + } + // Success/replacement cannot mask a terminal failure latched after transfer. + if mode == .twice { return document } + throw TransferMarker.afterTransfer + } +} + +@Test func snapshotV20TransferFailureCannotPublishAndScopeAlwaysInvalidates() async throws { + let document = try JSONParser().parseDocument(Data("[null]".utf8)) + for mode in [TransferProbe.Mode.twice, .work, .caller, .cancellation] { + let invalidated = Mutex(false) + let task = Task { @Sendable [document, mode] in + var walk = JSONTraversal(document: document) + walk.lifetimeObserver = { rootGone, stateGone in invalidated.withLock { $0 = rootGone && stateGone } } + var probe = TransferProbe(mode: mode) + return try walk.inspectOwned(reducer: &probe) + } + do { _ = try await task.value; Issue.record("No post-transfer failure may publish a report") } + catch let error as JSONFailure { + if mode == .twice { #expect(error.category == .configuration && error.code == .invalidOperation) } + else { #expect(mode == .work && error.resource?.metric == .workUnits) } + } + catch is CancellationError { #expect(mode == .cancellation) } + catch is TransferMarker { #expect(mode == .caller) } + #expect(invalidated.withLock { $0 }) + } + // Ordinary read/visit/N2 have no ownership transfer: their refunds are unchanged. + #expect(try document.readWithReport { _ in true }.statistics[.retainedBytes] == 0) + #expect(try JSONTraversal(document: document).visit { _, _ in .continue }.statistics[.retainedBytes] == 0) + let shape = try JSONShapeInspector(document: document).inspectArray() + #expect(shape.statistics[.retainedBytes] == 256) +} + +@Test func snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth() async throws { + let document = try JSONParser().parseDocument(Data(#"{"x":[null,9007199254740993]}"#.utf8)) + let snapshot = try JSONStructuralSnapshotBuilder(document: document).build().result + let baseline = try snapshot.value(for: 3) + try await withThrowingTaskGroup(of: UInt64.self) { group in + for ordinal in 0..<16 { group.addTask { + if ordinal.isMultiple(of: 2) { + var limits = JSONLimits.balanced; limits[.nodes] = 0 + do { _ = try snapshot.value(for: 3, limits: limits); Issue.record("Expected source-admission refusal") } + catch let error as JSONFailure { #expect(error.resource?.metric == .nodes) } + } + let value = try snapshot.value(for: 3) + #expect(value.result == baseline.result) + #expect(value.statistics[.nodes] == UInt64(snapshot.recordCount)) + #expect(try snapshot.location(for: 3).result == [.key("x"), .index(1)]) + #expect(try snapshot.sourceSpan(for: 3).result != nil) + return value.statistics[.workUnits] + } } + for try await work in group { #expect(work == baseline.statistics[.workUnits]) } + } +} diff --git a/Tools/SnapshotQualification/Package.swift b/Tools/SnapshotQualification/Package.swift new file mode 100644 index 0000000..0fec6e8 --- /dev/null +++ b/Tools/SnapshotQualification/Package.swift @@ -0,0 +1,12 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package( + name: "SnapshotQualification", + platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "SnapshotConsumer", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), + .product(name: "JSONNavigation", package: "Swift-JSON"), + .product(name: "JSONInspection", package: "Swift-JSON")])], + swiftLanguageModes: [.v6]) diff --git a/Tools/SnapshotQualification/README.md b/Tools/SnapshotQualification/README.md new file mode 100644 index 0000000..818e0df --- /dev/null +++ b/Tools/SnapshotQualification/README.md @@ -0,0 +1,10 @@ +# Public snapshot consumer + +This independent SwiftPM package imports only public JSON, JSONNavigation and +JSONInspection APIs. Run `swift run -c release --package-path +Tools/SnapshotQualification SnapshotConsumer` on a qualified Swift 6.4 platform. +Its fourteen named checks exercise exact source-bound metadata and separate +bounded lookups, including negative capacity, bounds and quota cases. It does +not establish a release or physical-device qualification. The associated +`Scripts/qualify_snapshot.py` executes all three DocC examples and full-codegen +external-access rejections, with source-bound receipts. diff --git a/Tools/SnapshotQualification/Sources/SnapshotConsumer/Main.swift b/Tools/SnapshotQualification/Sources/SnapshotConsumer/Main.swift new file mode 100644 index 0000000..13b0590 --- /dev/null +++ b/Tools/SnapshotQualification/Sources/SnapshotConsumer/Main.swift @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONNavigation +import JSONInspection + +private func expect(_ condition: @autoclosure () throws -> Bool) throws { + let satisfied = try condition(); precondition(satisfied) +} +private func check(_ name: String, _ body: () throws -> Void) rethrows { + try body(); print("SNAPSHOT_CHECK \(name) passed") +} +private func failure(_ body: () throws -> Void) -> JSONFailure { + do { try body(); preconditionFailure("Expected failure") } + catch let error as JSONFailure { return error } + catch { preconditionFailure("Unexpected error: \(error)") } +} +private func sendable(_ value: T) { _ = value } + +@main struct SnapshotConsumer { + static func main() async throws { + let source = #"{"":null,"0":[9007199254740993,0.1,0.0100],"é":true,"e\u0301":false,"/":{"~":[]}}"# + let document = try JSONParser().parseDocument(Data(source.utf8)) + let builder = JSONStructuralSnapshotBuilder(document: document) + let report = try builder.build() + let snapshot = report.result + sendable(builder); sendable(snapshot); sendable(try snapshot.record(at: 0)) + try check("roots") { + for text in ["null", "false", "1", #""s""#, "{}", "[]"] { + let one = try JSONStructuralSnapshotBuilder(document: .init(root: JSON.parse(text))).build().result + try expect(one.recordCount == 1) + let root = try one.record(at: 0) + try expect(root.ordinal == 0 && root.parentOrdinal == nil && root.componentFromParent == nil) + try expect(root.childCount == 0 && root.depth == 0 && root.subtreeEndExclusive == 1) + } + } + try check("typed-paths") { + try expect(try snapshot.location(for: 0).result == []) + try expect(try snapshot.location(for: 1).result == [.key("")]) + try expect(try snapshot.location(for: 2).result == [.key("0")]) + try expect(try snapshot.location(for: 3).result == [.key("0"), .index(0)]) + try expect(try snapshot.record(at: 2).componentFromParent != .index(0)) + } + try check("unicode") { + try expect(try snapshot.record(at: 6).componentFromParent == .key("é")) + try expect(try snapshot.record(at: 7).componentFromParent == .key("e\u{301}")) + try expect(try snapshot.record(at: 6).componentFromParent != snapshot.record(at: 7).componentFromParent) + } + try check("numbers") { + try expect(try snapshot.value(for: 3).result.numberValue?.integer(as: UInt64.self) == 9_007_199_254_740_993) + try expect(try snapshot.value(for: 4).result.numberValue == JSONNumber(parsing: "0.1")) + try expect(try snapshot.value(for: 5).result.numberValue?.originalLexeme == "0.0100") + try expect(try snapshot.value(for: 1).result == .null) + } + try check("duplicate-refusal") { + let error = failure { _ = try JSONParser().parse(#"{"a":0,"\u0061":1}"#) } + try expect(error.code == .duplicateKey) + } + try check("preorder-ranges") { + let expected = [10,2,6,4,5,6,7,8,10,10] + try expect(snapshot.recordCount == expected.count) + for ordinal in 0.. 0) + } + try expect(report.statistics[.nodes] == UInt64(snapshot.recordCount)) + var noNodes = JSONLimits.balanced; noNodes[.nodes] = 0 + try expect(failure { _ = try snapshot.value(for: 3, limits: noNodes) }.resource?.metric == .nodes) + print("SNAPSHOT_RESOURCE work=\(report.statistics[.workUnits]) retained=\(report.statistics[.retainedBytes]) peak=\(report.statistics.peakLogicalRetainedBytes) nodes=\(report.statistics[.nodes])") + } + try check("capacity-configuration") { + try expect(try builder.build(maximumRecords: snapshot.recordCount).result.recordCount == snapshot.recordCount) + for cap in [0, snapshot.recordCount-1] { + do { _ = try builder.build(maximumRecords: cap); preconditionFailure("No partial success") } + catch let error as JSONStructuralSnapshotFailure { try expect(error == .recordLimitExceeded(limit: cap)) } + } + var limits = JSONLimits.balanced; limits[.nodes] = 0 + try expect(failure { _ = try JSONStructuralSnapshotBuilder(document: document, limits: limits).build(maximumRecords: -1) }.category == .configuration) + } + try check("inclusive-budgets") { + var limits = JSONLimits.balanced; limits[.workUnits] = report.statistics[.workUnits] + try expect(try JSONStructuralSnapshotBuilder(document: document, limits: limits).build().result.recordCount == snapshot.recordCount) + limits[.workUnits] -= 1 + try expect(failure { _ = try JSONStructuralSnapshotBuilder(document: document, limits: limits).build() }.resource?.metric == .workUnits) + limits = .balanced; limits[.retainedBytes] = report.statistics.peakLogicalRetainedBytes + _ = try JSONStructuralSnapshotBuilder(document: document, limits: limits).build() + limits[.retainedBytes] -= 1 + try expect(failure { _ = try JSONStructuralSnapshotBuilder(document: document, limits: limits).build() }.resource?.metric == .retainedBytes) + } + try check("coverage-separation") { + let rows = try JSON.parse(#"[{"a":null},{"b":1},false]"#) + let evidence = try JSONShapeInspector(document: .init(root: rows)).inspectArray(sampling: .prefix(maximumRows: 2)).result + try expect(evidence.rowCoverage == .prefix(examined: 2, total: 3)) + try expect(evidence.observedAllObjects == true) + try expect(try JSONStructuralSnapshotBuilder(document: .init(root: rows)).build().result.recordCount == 6) + } + try await withThrowingTaskGroup(of: UInt64.self) { group in + for _ in 0..<4 { group.addTask { try snapshot.value(for: 3).statistics[.workUnits] } } + let expected = try snapshot.value(for: 3).statistics[.workUnits] + for try await work in group { try expect(work == expected) } + } + print("SNAPSHOT_CHECK concurrent-lookups passed") + print("SNAPSHOT_CONSUMER passed cases=14") + } +}