diff --git a/AGENTS.md b/AGENTS.md index 63617e4..e5e6be1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ Documentation/Architecture before changing behavior. JSONBridges and the swift-json executable. JSONFilterRuntime and JSONCLIEngine are package-private. JSONHTML and opt-in JSONBridges depend on JSON only; core never re-exports the bridges. The approved CLI and E2 additions preserve historical product contracts. + JSONNavigation is experimental without a public product until its N1 release gate. JSONRegexRuntime is package-private. Root package dependencies remain empty. - Preserve exact bounded decimals, decoded Unicode scalar identity, ordered objects with order-independent equality, one strict UTF-8 grammar, native diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 10849c5..4db2208 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -260,7 +260,8 @@ "CLI foundation, argv parser, POSIX adapters, prepaid bounded diagnostics and shared-meter package adapters are original Apache-2.0 Swift code. No legacy filter engine or jq source is copied. See Release1.4/Foundation.md for native test-harness reuse provenance.", "Tests/Fixtures/CLI/NewCases.json is the byte-identical 111-case authored CLI briefing fixture, SHA-256 6f863f3e98d2dea12e59018e7cf3a074ae0ebee5f1ab950a651df72e5eb18a4c. Fixture expected JSON texts use native exact comparison, not a floating-point oracle. The query compiler/VM and lifetime ledger are original Apache-2.0 implementations; no legacy eager evaluator or jq implementation is copied.", "JSONHTML typed annotations, iterative exact-path binding, static/interactive attachment and coherent JSONDocument span presentation are original Apache-2.0 code. No schema engine, third-party renderer, source archive or new dependency is adapted.", - "Structured forms/cards, finite prefix/preview staging, closed inline scanner, atomic mini-math MathML emitter, canonical PNG/JPEG header admission and line/media-bounded PDF pagination are original Apache-2.0 code. The approved structured concept informed the fixed CSS/JS; no demo Site content or external parser/renderer/codec engine is distributed." + "Structured forms/cards, finite prefix/preview staging, closed inline scanner, atomic mini-math MathML emitter, canonical PNG/JPEG header admission and line/media-bounded PDF pagination are original Apache-2.0 code. The approved structured concept informed the fixed CSS/JS; no demo Site content or external parser/renderer/codec engine is distributed.", + "Experimental N0/N1 JSONNavigation is original Apache-2.0 Swift: iterative traversal and explicit overlap policy reuse unchanged core read-scope/admission/path/meter authorities. No historical navigation/query algorithm or additional dependency is adapted. Three maintainer-provided synthetic S1 JSON fixtures are copied unchanged; their packet and file SHA-256 pins are in Tests/JSONNavigationTests/Fixtures/Provenance.json. Public product promotion remains gated." ], "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies.", "original_notes": [ diff --git a/Documentation/Architecture/StructuralIntelligence/API-DECISIONS.md b/Documentation/Architecture/StructuralIntelligence/API-DECISIONS.md new file mode 100644 index 0000000..ade93f7 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/API-DECISIONS.md @@ -0,0 +1,106 @@ +# N1 exact traversal decisions + +Scope is N0/N1 only. JSONNavigation depends exclusively on JSON and currently has +no public SwiftPM product. The seven existing products and empty external graph +remain the manifest acceptance contract. Core, schema, Patch, streaming, bridge, +filter/query and renderer behavior is unchanged. The generated CLI source-identity +stamp must refresh when Package.swift/Sources change; this mechanical integration +update adds no CLI behavior, command, language or version change. + +## Actual experimental declarations + +- `JSONTraversal(document:limits:)` captures one immutable JSONDocument. +- `visit(scope:options:_:)` takes `(JSONTraversalHit, JSON) throws -> JSONTraversalAction` + and returns `JSONOperationReport`. +- `collect(scope:options:maximumRecords:)` uses that same internal execution authority + and returns `JSONOperationReport`. +- `JSONTraversalScope.subtree(at:)` and `.virtual(roots:overlap:)` select absolute + existing typed paths. Virtual defaults to `.rejectAmbiguous`. +- `JSONScopeOverlapPolicy`: rejectAmbiguous, orderedOccurrences, uniquePhysicalNodes. +- `JSONTraversalAction`: continue, skipDescendants, stop. +- `JSONTraversalOptions`: depthLimit, includeScopeRoot, emitKinds, includeSourceSpans. +- `JSONTraversalHit`: absoluteLocation, scopeRootIndex, occurrenceIndex, relativeDepth, + kind and optional coherent sourceSpan. It contains no JSON subtree or cursor. +- `JSONTraversalSummary`: visitedCount, emittedCount, maximumDepthVisited, + scopeRootCount, deduplicatedCount, prunedSubtrees, depthLimitedSubtrees, completion. +- `JSONTraversalCompletion`: complete, restricted, visitorStopped. +- `JSONTraversalFailure`: collectionLimitExceeded(limit:), ambiguousRoots(first:second:). + +The [DocC guide](../../../Sources/JSONNavigation/Documentation.docc/Traversal.md) +contains three externally compiled/executed before/after examples. The actual +[consumer](../../../Tools/NavigationQualification/Consumer.swift) uses public JSON +and Navigation declarations, not package meters. Its external-module compiler +probes check core import isolation, hidden meters, non-Sendable cursor rejection +and absence of a fabricated missing kind in both build configurations. + +## Exactness and coverage + +DFS preorder is the only order offered: object insertion and increasing array +positions. Depth counts edges from each selected root. Full source admission is +mandatory even for a scalar selection, depth zero, filtered-empty selection or +early stop. Root omission/kind filtering changes emission only. Pruning/depth +omission marks coverage restricted, and explicit stop marks visitorStopped. Errors +and cancellation throw. Reports are conservative per-occurrence coverage, not a +union-coverage proof or a global absence query. + +All virtual roots resolve before any callback. Reject mode then checks every pair +for equal or ancestor/descendant typed prefixes. Ordered mode preserves root order, +then each subtree's DFS, including repeats. Unique mode remembers **first examined** +typed paths, even excluded/filtered roots, preserving their first depth/origin. It +still explores revisited descendants under the current occurrence's depth policy. +Two equal-valued siblings or scalar-distinct keys never merge. A virtual selection +does not invent a new source root, array index, durable ID or revision fingerprint. + +Mapped spans are opt-in core lookups, never synthesized. An unchanged captured old +document continues to describe its old root after another traversal captures a +replacement. No record resolves against a caller-supplied newer root automatically. + +## Finite policy and implementation costs + +All 49 core metrics/profiles/weights remain unchanged. Existing core read-scope +admission covers root, source bytes/label and source maps. Path records reuse its +pointer, diagnostic and retention policies. The N1 logical overlay reserves 128 +bytes for summary/staging, 16 for root-list staging, 64 per planned root or frontier +frame, 64 per ephemeral hit or collected record, and 16 per unique-path reference. +Retained collected paths additionally charge the published 32-byte indexSlot per +component and its key UTF-8 bytes. These are logical weights, not allocation/RSS +measurements; caller-retained callback copies remain caller storage. + +Collection has an independent inclusive default of 100,000 records; zero prohibits +emission but allows a genuinely empty result. Negative bounds/depth reject as +configuration errors. Its next-record refusal throws a typed traversal capacity +error and publishes no collection. Source admission uses existing node capacities; +visits do not misleadingly re-charge source nodes, and work stays cumulative across +roots, pair scans, dedup, emitted paths, source-span lookups, output and cleanup. + +Initial reject/unique modes use exact metered linear comparisons. Pair validation +and unique-node scans can be quadratic. Core path snapshots along a deep frontier +can also use quadratic path metadata. The prototype intentionally makes no +linear-total-time, indexed-lookup, zero-copy or speed claim. Every owned comparison +loop is cancellable/budgeted, so exhaustion is an operational error, never a partial +success. A future compact representation/index needs its own measured review. + +The invocation and its synchronous scope never escape into a callback. JSONTraversal, +metadata/options and collected values are checked Sendable; each simultaneous call +creates its own scope. A package-only nil-by-default checkpoint observer supports +owned-loop cancellation qualification; no public consumer can install it. + +## Qualification and promotion boundary + +The reviewed new inventory is 37 nonparameterized Navigation tests, including the +three pinned synthetic packet documents. Existing function identities and all 325 +native/Linux parameter invocations remain; WASI retains its separate 288. Full roots +become 583 native, 567 Linux and 479 WASI, and shared-state counts 42/41. All 126 +maintenance identities, corpus policies and 49 cap probes remain required. + +`Scripts/ci.py --lane navigation` checks exact focused discovery, Debug/Release, +explicit strict-memory compilation and external compiler/consumer/DocC probes. +Native/Linux run the new consumer and full existing suites; safety includes Navigation +ASan plus its shared-root TSan function. WASI uses the updated exact root inventory. +Current commands/receipts, failures and platform gaps accompany the return; this +decision document does not assert a pending platform run passed. Full release-gate +qualification and explicit product review precede promotion to an installable product. + +BFS, scalar-sorted order, prefix snapshots, JSONInspection, classifiers, snapshots, +semantic/durable digests, projections, editing, retrieval, actors, rendering and CLI +verbs are explicitly deferred. No EngineBench implementation or performance claim. diff --git a/Documentation/Architecture/StructuralIntelligence/Progress.json b/Documentation/Architecture/StructuralIntelligence/Progress.json new file mode 100644 index 0000000..d2c8313 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/Progress.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "research_base": "507dbf9ad4b545a5d60d7d6acf6b814795ec3099", + "implementation_base": "037e735dc6224789e4d04d2c18d15b8d27440198", + "packet_sha256": "d758f75d84271a97da515bf7b7ef70de7a61e91fdf949a511fe0fca2708156f8", + "N0": "source authorities reconciled; core-only seven-witness consumer and package-scoped one-ledger composition compiled/executed", + "N1": "experimental physical/virtual DFS, bounded collection and 37 behavior functions implemented; functional native/Linux/WASI proofs passed at 6e5e5fa; public product promotion remains gated", + "public_product": false, + "public_products_preserved": 7, + "core_semantic_changes": false, + "deferred": [ + "BFS", + "N2", + "N3", + "P1", + "P2", + "R1", + "R2", + "actors", + "persistence", + "query grammar", + "rendering", + "CLI verbs", + "EngineBench" + ], + "qualification": { + "evidence_source": "6e5e5fa7d8c4c7e8ce1c4fe14bb3323f1ddc7ac8", + "native_run": 38781500802, + "native_boundary": "native/safety/docs/archive qualification commands passed; native/archive artifact upload failed on raw DocC colon filenames; preserved as a failed workflow", + "linux_run": 38781500823, + "linux_boundary": "both architectures qualification and safety passed", + "wasi_boundary": "77-command full Node/limited WasmKit lane passed with unchanged source; supplementary 37-function Navigation Debug/Release runs passed in WasmKit", + "mobile_boundary": "iPhoneOS/iPhoneSimulator 27.2 target compilation passed, no device/runtime claim", + "transport_successor": "native catalogue is packaged byte-for-byte into a verified tar/manifest before upload; fresh hosted outcomes and materialized evidence are supplied by PR 34 and the return packet", + "product_review": "experimental target only; no public product or merge/release action" + } +} diff --git a/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md b/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md new file mode 100644 index 0000000..de008a4 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md @@ -0,0 +1,64 @@ +# N0 source authority + +The research baseline is `507dbf9ad4b545a5d60d7d6acf6b814795ec3099`. +The reconciled implementation base is main +`037e735dc6224789e4d04d2c18d15b8d27440198`, tree +`5c53288c50ae8a1999d889f0a8c4f0cb14dca913`. Origin was fetched before edits. +PR #33 forward-ports maintenance repairs after the research snapshot; no reset, +old scaffold or historical navigation implementation is used. + +At reconnaissance all nine main checks passed: Native CI 38735450692 and Linux +CI 38735450703. Those are baseline results, not qualification of this increment. +The latest published release observed was 1.5.0; main's `1.6.0-dev` CLI metadata +does not establish a publication. Draft #31 owns opt-in value/schema DSLs, and a +separate schema-example worktree has active edits. Neither is imported or modified. +The primary checkout stays on main; implementation uses +`codex/structural-navigation-n0-n1` in an isolated existing feature worktree. + +## Semantic delegates and owners + +Paths below identify code ownership, not a second resolver API. All new owned +phases use the **same** read-scope state and its noncopyable core-domain meter. + +| New operation or fact | Existing authority / owner | Budget boundary | +| --- | --- | --- | +| Captured canonical source | [JSONDocument](../../../Sources/JSON/Document/JSONDocument.swift) and JSON | Constructor captures trusted immutable values; each invocation admits its full root/source/map | +| Admission and source lifetime | [JSONReadScope.init/check/complete/invalidate](../../../Sources/JSON/Reading/JSONReadScope.swift) | Exactly one root/source admission and terminal ledger; complete releases scope ownership | +| Exact keys / insertion order | [JSONObject](../../../Sources/JSON/Value/JSONObject.swift), JSONKey | Visit ordinal members; no String sort, dictionary conversion or duplicate index | +| Typed physical selection | JSONReadScope.select inside run | Strict key/index parent checks and existing JSONAccessFailure; no Pointer-token reinterpretation | +| Requested scope-key admission | JSONAdmission.text(key: true), as in JSONReadCursor's JSONKey subscript | Caller query keys consume the same cumulative key/text capacities before path staging or lookup | +| Typed path staging | JSONReadScope.append and JSONReadPath | Existing pointerTokens/pointerBytes/diagnosticBytes checks, path copy/UTF-8 work and live refunds | +| Meter/operational failure | [JSONWorkMeter](../../../Sources/JSON/Limits/JSONWorkMeter.swift), JSONCodingState | Shared cumulative work, capacity/retention, cancellation and terminal health; never reset | +| DFS child enumeration | JSON array indices / JSONObject ordinal collection access | New iterative frontier; each node/edge/frame/callback handoff checked and charged | +| Overlap / unique-path comparison | [JSONComparison.textEquals](../../../Sources/JSON/Value/JSONComparison.swift) + typed integer components | New metered component/prefix loops; no Set, unmetered path hash, or value-based dedup | +| Decimal and text values given to visitor | JSON/JSONNumber/JSONKey | Original source value; no conversion or re-admission per hit | +| Pointer serialization / differential oracle | [JSONPointer](../../../Sources/JSON/Pointer/JSONPointer.swift) | Explicit separate caller operation; walker never calls a public helper to start a fresh owned quota | +| Native projection / presence oracle | [JSONReadCursor](../../../Sources/JSON/Reading/JSONReadCursor.swift), JSONPresence | Compiler/behavior witnesses only; no cursor is a traversal result or task argument | +| Optional value spans | [JSONSourceMap.span](../../../Sources/JSON/Document/JSONSourceMap.swift) | Same meter, only coherent captured maps; replacingRoot supplies no map | +| Successful resource reporting | JSONOperationReport / JSONOperationStatistics | Existing accountingVersion/domain/counts/peak convention; no successful report after error | +| Mutations | JSONPatch / JSONMergePatch | Unchanged and outside N1; traversal has no edits or writable value view | + +`JSONReadScope` and `JSONWorkMeter` are already `package`, so no core SPI, +public meter protocol or package helper change is needed. Actual target compilation +and the inclusive work-budget tests prove this access/composition boundary. The +public N0 consumer compiled and ran seven core-only witnesses before N1: root versus +empty key, typed key/index, exact Unicode keys, presence, native projection and +invalid array-index rejection. Compiler receipts retain the actual source/toolchain. + +## Collision and dialect reconciliation + +| Historical/proposed surface | Disposition | +| --- | --- | +| JSONCursor / JSONNavigationCursor | Rejected: guarded core read cursor already owns synchronous one-node access | +| JSONNodePath / dotted flattening | Rejected: canonical components are JSONLocationComponent; root [] and empty key differ | +| New JSON. facade / JSONPath grammar | Rejected: neither necessary nor an RFC 9535 implementation | +| JSONFilterRuntime paths/getpath/walk | Existing private query dialect can accept negative indices and language-level missing behavior; not a strict typed scope delegate | +| JSONHTML exact paths and previews | Existing rendering authority; no renderer import/edit or duplicate preview logic | +| JSONTraversal and metadata names | New target-qualified names; no existing core/sibling definition found at the recorded base | +| Sketch BFS / inspection / snapshots / projections | Deferred; no reserved BFS label, parallel model, digest or classifier added | + +The packet's declarations were illustrative. N1 returns existing +JSONOperationReport envelopes, passes the source value separately to a synchronous +visitor, and keeps records metadata-only. Its module is experimental; **no public +SwiftPM product is added**. API shape and all remaining release gates are recorded +in [API-DECISIONS](API-DECISIONS.md). diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index c30e5da..932a9f0 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,5 +1,17 @@ # Native CI +## Experimental N1 branch + +The Structural Intelligence branch adds exactly 37 Navigation functions and one +shared-root safety function, keeping every existing identity/case. Root expectations +are 583 native/567 Linux with 325 parameter invocations; TSan is 42/41. WASI's +separate core scope is 479 functions/288 parameters. No new public product is exposed. +`--lane navigation` checks focused discovery, Debug/Release, explicit strict-memory +compilation and the external compiler/consumer/experimental DocC audit. Native/Linux +include that consumer alongside their existing gates; all 49 core cap outcomes and +126 restored maintenance identities remain required. The following earlier counts +retain their original source context; actual source-bound receipts establish passes. + This main-based forward-port retains seven products and restores the entire 1.2.1 regression inventory. Current native root discovery is 546 functions and 325 parameter invocations; Linux is 530/325. Full ASan uses the same roots; diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 6fd5f94..40b0c2c 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -9,7 +9,8 @@ "JSONIntegrationTests": 20, "JSONHTMLTests": 97, "JSONBridgesTests": 10, - "JSONCLITests": 88 + "JSONCLITests": 88, + "JSONNavigationTests": 37 }, "parameterized_invocations": 325, "qa_functions": { @@ -28,7 +29,8 @@ "JSONIntegrationTests": 10, "JSONHTMLTests": 5, "JSONBridgesTests": 1, - "JSONCLITests": 1 + "JSONCLITests": 1, + "JSONNavigationTests": 1 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -52,7 +54,8 @@ "JSONBridges", "JSONFilterRuntime", "JSONCLIEngine", - "JSONCLI" + "JSONCLI", + "JSONNavigation" ], "portable_root_functions": { "JSONTests": 190, @@ -63,7 +66,8 @@ "JSONIntegrationTests": 20, "JSONHTMLTests": 81, "JSONBridgesTests": 10, - "JSONCLITests": 88 + "JSONCLITests": 88, + "JSONNavigationTests": 37 }, "wasi_root_functions": { "JSONTests": 190, @@ -73,7 +77,8 @@ "JSONPatchTests": 16, "JSONIntegrationTests": 20, "JSONHTMLTests": 81, - "JSONBridgesTests": 10 + "JSONBridgesTests": 10, + "JSONNavigationTests": 37 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -84,7 +89,8 @@ "JSONIntegrationTests": 10, "JSONHTMLTests": 4, "JSONBridgesTests": 1, - "JSONCLITests": 1 + "JSONCLITests": 1, + "JSONNavigationTests": 1 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/Documentation/README.md b/Documentation/README.md index c1b2b26..2c7ee0d 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -41,6 +41,8 @@ Online references track main; tagged releases include matching compiled catalogu ## Consumers, qualification and maintenance +- [Experimental N0/N1 source authority](Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md) and [gated traversal decisions](Architecture/StructuralIntelligence/API-DECISIONS.md); JSONNavigation is not a released product. + - [Six-library compiled example](../Examples/Consumer/README.md) - [Downstream JSON/JSONSchema interface](ConsumerInterface.md) and [runnable public consumer](../Tools/MCPReadiness/README.md) - [Independent-suite policies and qualification](Qualification/README.md) diff --git a/Package.swift b/Package.swift index c494a96..4170c9c 100644 --- a/Package.swift +++ b/Package.swift @@ -16,6 +16,9 @@ let package = Package( ], targets: [ .target(name: "JSON"), + // Experimental N1 target. Product promotion requires separate release-gate review. + .target(name: "JSONNavigation", dependencies: ["JSON"]), + .testTarget(name: "JSONNavigationTests", dependencies: ["JSONNavigation", "JSON"], exclude: ["Fixtures"]), .target(name: "JSONBridges", dependencies: ["JSON"]), .testTarget(name: "JSONIntegrationTests", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch"]), .testTarget(name: "JSONBridgesTests", dependencies: ["JSONBridges", "JSON"]), diff --git a/README.md b/README.md index bbd9c15..4005f31 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ Maintained by [Thorsten Fuchs](https://github.com/ThorFuchs). Swift-JSON follows semantic versioning. Use a released version and retain `Package.resolved` for reproducible builds; `main` tracks ongoing development. +This feature branch also contains an [experimental structural traversal target](Documentation/Architecture/StructuralIntelligence/API-DECISIONS.md). +It preserves the seven released products. JSONNavigation product promotion remains +gated; the [compiled traversal guide](Sources/JSONNavigation/Documentation.docc/Traversal.md) +describes its many-node scope, exact locations, finite accounting and coverage. + Published **1.5.0** provides six libraries and the CLI. This development branch retains main’s unreleased structured HTML previews and forward-ports the 1.5.1 maintenance repairs: best-effort schema diagnostics, the complete 1.2.1 regression diff --git a/Scripts/README.md b/Scripts/README.md index f491eb5..26c3e7a 100644 --- a/Scripts/README.md +++ b/Scripts/README.md @@ -9,6 +9,7 @@ separate from the dependency-free library. Native lanes fail on unsupported host | `python3 Scripts/preflight.py` | Actual native OS/compiler/SDK receipt | | `python3 Scripts/ci.py --lane native` | Manifests, builds, Swift Testing, consumers and suites | | `python3 Scripts/ci.py --lane safety` | Address/Thread Sanitizer and strict memory safety | +| `python3 Scripts/ci.py --lane navigation` | Experimental N1 exact discovery, Debug/Release, strict-memory and external compiler/consumer/DocC proof; no product promotion | | `python3 Scripts/ci.py --lane linux` | Actual Linux preflight, all seven products, exact root/cap/corpus/consumer counts | | `python3 Scripts/ci.py --lane linux-safety` | Linux Address/Thread Sanitizer and strict memory safety | | `python3 Scripts/ci.py --lane wasm` | Matched Swift 6.4 WASI debug/release, exact root/corpus/cap/consumer counts, two-host public parity and structured resource failures | diff --git a/Scripts/ci.py b/Scripts/ci.py index e0c0dfe..8d7c650 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -10,6 +10,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes|sha256InputCapacityAndConcurrentInvocationsRemainIndependent|sharedFormattedHTMLRendererHasIndependentConcurrentInvocations|sharedAnnotatedDocumentRendererHasIndependentConcurrentInvocations|sharedExportPreparationsHaveIndependentConcurrentState|applePDFSimultaneousJobsHaveIndependentStateAndCancellation|sharedFilterPlanHasIndependentConcurrentInvocations|structuredSharedRendererPlansHaveInvocationOwnedState|sharedProviderBridgeHasIndependentConcurrentInvocations|e2SharedBridgeInputsHaveInvocationOwnedState|e2SharedCompiledPlanFacadeHasIndependentInvocationState' SHARED_TESTS += '|qaStreaming|qaE2E(SharedPlan|RootImport|NDJSONImport|AwaitedSink|ExactWorkflow)' +SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -18,6 +19,13 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + if lane == 'navigation': + return [['python3', '-B', 'Scripts/public_check.py'], + ['swift', 'test', '--filter', 'navigation'], + ['swift', 'test', '-c', 'release', '--filter', 'navigation'], + ['swift', 'build', '--target', 'JSONNavigation', '--scratch-path', '.build/navigation-strict', + '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], + ['python3', '-B', 'Scripts/qualify_navigation.py', '--output', str(output / 'consumer')]] if lane == 'archive': return [['python3', '-B', 'Scripts/public_check.py'], ['python3', '-B', 'Scripts/qualify_native_reproduction.py', '--output', str(output / 'reproduction')]] @@ -98,6 +106,7 @@ def commands(lane, output): ['swift', 'run', '-c', 'release', '--package-path', 'Examples/Consumer', 'Consumer'], ['python3', '-B', 'Scripts/qualify_distribution.py', '--output', str(output / 'distribution')], ['python3', '-B', 'Scripts/qualify_ergonomics.py'], + ['python3', '-B', 'Scripts/qualify_navigation.py', '--output', str(output / 'navigation')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -146,6 +155,10 @@ def check_result(command, code, log, events=None): if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: expected = [88] if 'cli|sharedFilterPlan' in command else sorted(DISCOVERY['export_functions'].values()) if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else sorted(DISCOVERY['qa_functions'].values()) 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 sorted(SHARED_COUNTS.values()) if 'thread' in command else sorted(ROOT_COUNTS.values()) + if 'navigation' in command: + 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 passed = passed and sorted(counts) == expected if 'qa' in command and events is not None: passed = passed and events['module_function_counts'] == DISCOVERY['qa_functions'] and check_restored(events) @@ -181,7 +194,7 @@ def check_result(command, code, log, events=None): passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming', 'swift-json'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] passed = passed and {p['name']: p['targets'] for p in d['products']} == dict({name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges']}, **{'swift-json': ['JSONCLI']}) targets = {t['name']: t for t in d['targets']} - passed = passed and set(targets) == {'JSON', 'JSONIntegrationTests', 'JSONBridges', 'JSONBridgesTests', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONBridges']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' + passed = passed and set(targets) == {'JSON', 'JSONNavigation', 'JSONNavigationTests', 'JSONIntegrationTests', 'JSONBridges', 'JSONBridgesTests', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONBridges']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONNavigation']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONNavigationTests']['dependencies'] == [{'byName': ['JSONNavigation', None]}, {'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' if command == ['swift', 'package', 'show-dependencies', '--format', 'json'] and passed: passed = json.loads(log)['dependencies'] == [] if command[:2] == ['swift', 'run'] and 'ProviderConsumer' in command: @@ -192,6 +205,8 @@ def check_result(command, code, log, events=None): passed = passed and 'TESTING_EVENT_RECEIPTS passed cases=19' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_ergonomics.py']: passed = passed and 'ERGONOMICS_COMPILER passed commands=8 guide_fences=3' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation.py']: + passed = passed and 'NAVIGATION_COMPILER passed configurations=2 consumer_cases=18 negative_probes=8' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/verify_html_assets.py']: passed = passed and 'JSONHTML_ASSETS passed count=2' in log.splitlines() if 'BrowserFixtures' in command: @@ -204,7 +219,7 @@ def check_result(command, code, log, events=None): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) + parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive', 'navigation'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) diff --git a/Scripts/qualify_navigation.py b/Scripts/qualify_navigation.py new file mode 100644 index 0000000..f1245ff --- /dev/null +++ b/Scripts/qualify_navigation.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Compile/run the unpublished N1 module as an external consumer, including negative compiler probes.""" +import argparse +import gzip +import hashlib +import io +import json +import pathlib +import re +import shutil +import subprocess +import sys +import tarfile + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKS = {'root-empty-key', 'typed-key-index', 'presence', 'scalar-keys', 'native-exact', + 'pointer-parity', 'metadata-collection', 'ordered-occurrences', 'unique-paths', + 'default-overlap-rejection', 'typed-missing', 'complete-empty-filter', + 'collection-limit', 'cumulative-report', 'coherent-spans', + 'replacement-invalidates-spans', 'intentional-stop', 'cumulative-quota'} + + +def pack_catalog(catalog, output): + """Preserve native DocC names inside a verified portable transport envelope.""" + if catalog.is_symlink() or catalog.parent.resolve() != output.resolve(): + raise ValueError('Unexpected generated catalogue location') + records = [] + for path in sorted(catalog.rglob('*')): + if path.is_symlink(): + raise ValueError('Unexpected generated catalogue symlink') + if path.is_file(): + records.append((path.relative_to(output).as_posix(), path.read_bytes())) + if not records: + raise ValueError('Empty experimental DocC catalogue') + archive = output / 'JSONNavigation-DocC.tar.gz' + manifest = output / 'JSONNavigation-DocC.MANIFEST.sha256' + manifest.write_text(''.join(hashlib.sha256(data).hexdigest() + ' ' + name + '\n' for name, data in records)) + with archive.open('wb') as stream: + with gzip.GzipFile(fileobj=stream, mode='wb', filename='', mtime=0) as zipped: + with tarfile.open(fileobj=zipped, mode='w', format=tarfile.GNU_FORMAT) as tar: + for name, data in records: + entry = tarfile.TarInfo(name); entry.size = len(data); entry.mode = 0o644 + tar.addfile(entry, io.BytesIO(data)) + with tarfile.open(archive) as tar: + members = tar.getmembers() + if len(members) != len(records) or len({m.name for m in members}) != len(records): + raise ValueError('DocC transport inventory differs') + for member, (name, data) in zip(members, records): + if not member.isfile() or member.name != name or tar.extractfile(member).read() != data: + raise ValueError('DocC transport bytes differ') + # Only this helper's verified generated tree is removed. Native ':' filenames + # remain intact inside the tar; GitHub artifact transport never sees raw paths. + shutil.rmtree(catalog) + return {'archive': archive.relative_to(ROOT).as_posix(), 'sha256': hashlib.sha256(archive.read_bytes()).hexdigest(), + 'bytes': archive.stat().st_size, 'files': len(records), 'manifest': manifest.relative_to(ROOT).as_posix(), + 'manifest_sha256': hashlib.sha256(manifest.read_bytes()).hexdigest(), 'all_bytes_verified': True} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + args = parser.parse_args() + output = ROOT / args.output + output.mkdir(parents=True, exist_ok=True) + receipt = {'schema_version': 1, 'status': 'running', 'commands': [], + 'scope': 'External Swift module API proof; JSONNavigation is not a released SwiftPM product', + 'consumer_sha256': hashlib.sha256((ROOT / 'Tools/NavigationQualification/Consumer.swift').read_bytes()).hexdigest()} + + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + log = output / (name + '.log') + log.write_text((result.stdout + result.stderr).replace(str(ROOT), '.')) + passed = result.returncode == 0 if expected is None else result.returncode != 0 and expected in result.stderr + receipt['commands'].append({'name': name, 'command': [x.replace(str(ROOT), '.') for x in command], + 'exit_code': result.returncode, 'status': 'passed' if passed else 'failed', + 'expected_diagnostic': expected, 'log': log.relative_to(ROOT).as_posix(), + 'log_sha256': hashlib.sha256(log.read_bytes()).hexdigest()}) + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + if not passed: + raise ValueError('Compiler/consumer outcome failed: ' + name) + return result.stdout + + try: + for config in ['debug', 'release']: + run(config + '-build', ['swift', 'build', '-c', config, '--target', 'JSONNavigation']) + # Build settings are interrogated, never inferred from a user's checkout name. + 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']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: + raise ValueError('Missing built module objects: ' + module) + objects.extend(str(p) for p in selected) + common = ['swiftc', '-swift-version', '6', '-warnings-as-errors', '-I', str(modules), '-package-name', 'navigation_external_consumer'] + executable = output / ('consumer-' + config) + run(config + '-consumer-compile', common + ['-parse-as-library', str(ROOT / 'Tools/NavigationQualification/Consumer.swift'), *objects, '-o', str(executable)]) + text = run(config + '-consumer-run', [str(executable)]) + observed = re.findall(r'^NAVIGATION_CHECK (\S+) passed$', text, re.M) + if len(observed) != 18 or set(observed) != CHECKS or 'NAVIGATION_CONSUMER passed cases=18' not in text: + raise ValueError('Missing/duplicate public consumer outcomes') + resource = re.search(r'^NAVIGATION_RESOURCE work=(\d+) retained=(\d+) peak=(\d+) nodes=(\d+)$', text, re.M) + if resource is None or int(resource[4]) != 10 or not all(int(n) > 0 for n in resource.groups()): + raise ValueError('Missing cumulative resource observations') + receipt.setdefault('consumer_results', {})[config] = {'cases': observed, 'work_units': int(resource[1]), + 'logical_retained_bytes': int(resource[2]), 'peak_logical_retained_bytes': int(resource[3]), 'admitted_nodes': int(resource[4])} + guide = ROOT / 'Sources/JSONNavigation/Documentation.docc/Traversal.md' + fences = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(fences) != 3: + raise ValueError('Changed three-fence compiled guide inventory') + for index, fence in enumerate(fences): + file = output / ('guide-' + str(index) + '.swift') + file.write_text('import JSON\nimport JSONNavigation\n@main struct Guide { static func main() throws {\n' + fence + '\n} }\n') + binary = output / ('guide-' + config + '-' + str(index)) + run(config + '-guide-' + str(index) + '-compile', 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 = JSONTraversal.self\n', 'cannot find'), + ('private-meter-isolation', 'import JSON\nlet x = JSONWorkMeter.self\n', 'cannot find'), + ('cursor-cross-task-rejection', 'import JSON\nfunc cross(_ cursor: JSONReadCursor) { let f: @Sendable () throws -> JSON = { try cursor.requiredValue() }; _ = f }\n', 'non-Sendable'), + ('no-missing-kind', 'import JSONNavigation\nlet x = JSONTraversalKind.missing\n', 'has no member'), + ] + for name, source, expected in probes: + file = output / (name + '.swift') + file.write_text(source) + run(config + '-' + name, common + ['-typecheck', str(file)], expected) + if sys.platform == 'darwin': + graphs = output / 'symbols'; graphs.mkdir(exist_ok=True) + # Standalone extraction from an optimized serialized module omits source + # locations. SwiftPM emits source-aware graphs during real compilation. + run('symbol-graph', ['swift', 'package', 'dump-symbol-graph', '--minimum-access-level', 'public']) + for path in (ROOT / '.build/out/symbolgraph').glob('JSONNavigation*.symbols.json'): + shutil.copyfile(path, graphs / path.name) + import documentation_catalog + # This local audit does not promote an experimental module to the public site. + documentation_catalog.PRODUCTS = ('JSONNavigation',) + coverage = documentation_catalog.verify_comments(documentation_catalog.public_symbols(graphs)) + receipt['experimental_api_coverage'] = coverage + if coverage['errors']: + raise ValueError('Undocumented experimental API: ' + str(coverage['errors'])) + run('docc', ['xcrun', 'docc', 'convert', str(ROOT / 'Sources/JSONNavigation/Documentation.docc'), + '--additional-symbol-graph-dir', str(graphs), '--output-dir', str(output / 'JSONNavigation.doccarchive'), + '--fallback-bundle-identifier', 'org.swift-json.experimental.JSONNavigation', '--warnings-as-errors']) + receipt['compiled_docc'] = pack_catalog(output / 'JSONNavigation.doccarchive', output) + receipt['status'] = 'passed' + except (OSError, ValueError, tarfile.TarError, subprocess.TimeoutExpired) as error: + receipt['status'] = 'failed'; receipt['error'] = str(error) + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + print('NAVIGATION_COMPILER ' + receipt['status'] + ' configurations=2 consumer_cases=18 negative_probes=8') + return 0 if receipt['status'] == 'passed' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 9641f33..2df1adb 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:b3147f90fc72f25bd4507ddb859ed94f9f1dde3ea7b365d8a15212532b802ff4" + package static let revision = "sha256:4bf62947afb2d30e64351ea20495cc7d6cef296975bfe2e0564c544ae8b6aaf4" package static let kind = "source-sha256" } diff --git a/Sources/JSONNavigation/Documentation.docc/JSONNavigation.md b/Sources/JSONNavigation/Documentation.docc/JSONNavigation.md new file mode 100644 index 0000000..9e65c2d --- /dev/null +++ b/Sources/JSONNavigation/Documentation.docc/JSONNavigation.md @@ -0,0 +1,32 @@ +# ``JSONNavigation`` + +Exact, synchronous, bounded exploration of many nodes in one canonical JSON source. + +This is an experimental N1 target, **not a released SwiftPM product**. The package's +seven public products remain unchanged. Product promotion requires the recorded +correctness, resource, compiler, platform and API review gate. + +The core JSON module continues to own one-node lookup, exact numbers and keys, +presence and RFC 6901 Pointer. Use ``JSONTraversal`` for deterministic DFS preorder, +physical subtrees and explicitly ordered virtual selections. One call admits the +captured source once and shares its core read-scope meter across all phases. + +## Topics + +### Exact structural exploration + +- +- ``JSONTraversal`` +- ``JSONTraversalScope`` +- ``JSONTraversalOptions`` +- ``JSONScopeOverlapPolicy`` + +### Observation and coverage + +- ``JSONTraversalHit`` +- ``JSONTraversalKind`` +- ``JSONTraversalAction`` +- ``JSONTraversalSummary`` +- ``JSONTraversalCompletion`` +- ``JSONTraversalCollection`` +- ``JSONTraversalFailure`` diff --git a/Sources/JSONNavigation/Documentation.docc/Traversal.md b/Sources/JSONNavigation/Documentation.docc/Traversal.md new file mode 100644 index 0000000..b04f9bc --- /dev/null +++ b/Sources/JSONNavigation/Documentation.docc/Traversal.md @@ -0,0 +1,76 @@ +# Exact traversal and explicit coverage + +N1 adds many-node observation to the existing exact model. These three examples +are compiled and executed as external Swift 6 modules in Debug and Release by +`Scripts/qualify_navigation.py`. They assume explicit `import JSON` and +`import JSONNavigation`; installing a public Navigation product remains gated. + +Core access remains the appropriate interface for one node: + +```swift +let root = try JSON.parse(#"{"items":[1,2],"note":null}"#) +let selected = try JSONPointer(parsing: "/items/0").resolve(in: root) +let absent = try root.presence(of: "missing") +let null = try root.presence(of: "note") +precondition(selected == 1 && absent == .absent && null == .null) +``` + +The structural visitor explores object insertion order and ascending array indices. +Its paths remain absolute typed components; numeric keys remain keys. Root is `[]` +and empty-key is `[.key("")]`; Pointer text should be produced by the core Pointer. + +```swift +let root = try JSON.parse(#"{"items":[1,2]}"#) +var locations: [[JSONLocationComponent]] = [] +let report = try JSONTraversal(document: .init(root: root)).visit { hit, _ in + locations.append(hit.absoluteLocation) + return .continue +} +precondition(locations == [[], [.key("items")], [.key("items"), .index(0)], [.key("items"), .index(1)]]) +precondition(report.result.completion == .complete && report.statistics[.nodes] == 4) +``` + +Virtual scopes select existing source nodes without materializing a new JSON array. +The default rejects duplicate and ancestor/descendant roots before any callback. + +```swift +let root = try JSON.parse(#"{"items":[1,2],"tail":null}"#) +let selection = JSONTraversalScope.virtual(roots: [[.key("items")], [.key("tail")]]) +let report = try JSONTraversal(document: .init(root: root)).collect(scope: selection, maximumRecords: 4) +precondition(report.result.nodes.count == 4 && report.result.summary.scopeRootCount == 2) +precondition(report.result.summary.completion == .complete) +``` + +`.orderedOccurrences` preserves repeated selections and their root index, occurrence +ordinal and relative depth. `.uniquePhysicalNodes` deduplicates exact typed paths, +including filtered/excluded roots: the first examined occurrence wins. Equal values +at different paths remain different nodes. Revisited paths still explore descendants +under their current relative depth; deduplication is never automatic subtree pruning. + +Depth zero permits only a selected root. `includeScopeRoot: false` suppresses its +emission; `depthLimit: 1` then gives children-only traversal. Kind filtering suppresses +emission, never descendant exploration. A filtered empty result can be complete; +a missing scope throws. Pruning/depth omission is `.restricted`, explicit `.stop` is +`.visitorStopped`; neither establishes absence outside observed coverage. Coverage is +conservative per occurrence, not a proof that overlapping selections repaired an +earlier pruned region. Only DFS is implemented; no BFS label or prefix-snapshot API. + +Collection defaults to 100,000 records and throws on the next record; its bound is +inclusive and zero is meaningful. Root/source admission, scope resolution, exact +overlap comparisons, path creation, frontier, source-span lookup and records share +one finite core ledger. Source spans are opt-in and come only from the captured +coherent document. `replacingRoot` supplies none. No source text or subtree appears +in a hit, and no read cursor crosses the synchronous visitor boundary. + +Caller visitor computation and caller-retained copies are outside owned instruction +and retention quotas. Reports account for logical ownership, not process RSS or +allocator capacity. Unexpected quotas and cancellation throw and return no successful +report, even if cancellation occurs during a callback returning `.stop`. Earlier +callback effects cannot be recalled. Core access/resource errors retain their types; +collection capacity and ambiguous roots use ``JSONTraversalFailure``. + +The initial overlap/dedup implementation uses metered linear comparisons rather than +an unqualified hash index. Complete path copies are bounded by the existing read +scope; deep frontiers can use quadratic path metadata. No EngineBench speed, linear +total-time, zero-copy or fixed-memory claim is made. Classification, snapshots, +projections, persistence, async actors, query syntax and rendering are later gates. diff --git a/Sources/JSONNavigation/JSONTraversal.swift b/Sources/JSONNavigation/JSONTraversal.swift new file mode 100644 index 0000000..97228ed --- /dev/null +++ b/Sources/JSONNavigation/JSONTraversal.swift @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Synchronous exact structural exploration of one immutable canonical document. +/// +/// Each call admits the complete captured root/source once, then uses the existing +/// read scope's single core-domain meter for selection, paths, frontier and output. +/// No cursor escapes. Caller visitor computation and copies retained by the caller +/// are outside owned instruction/retention quotas; logical bytes are not RSS. +public struct JSONTraversal: Sendable { + /// The immutable source to which every emitted absolute path belongs. + public let document: JSONDocument + /// Finite admission, work, path and logical-retention limits for each invocation. + public let limits: JSONLimits + /// Invocation-local rendezvous for qualification, absent from public consumers. + package var checkpointObserver: (@Sendable (UInt64) -> Void)? + + /// Captures a document without admission or scanning; each operation admits it. + /// - Parameters: + /// - document: An immutable canonical source, optionally with coherent mapping. + /// - limits: Finite cumulative limits, validated at operation entry. + public init(document: JSONDocument, limits: JSONLimits = .balanced) { + self.document = document; self.limits = limits + } + + /// Visits DFS preorder with object insertion order and ascending array indices. + /// - Parameters: + /// - scope: Strict absolute subtree selection, including scalar roots. + /// - options: Explicit depth, root-emission, kind and source-span policies. + /// - visitor: Synchronous caller work receiving metadata and the unchanged value. + /// - Returns: Coverage and all successful cumulative core statistics. + /// - Throws: Typed core access errors, invalid configuration, resource/caller errors or cancellation. + public func visit(scope: JSONTraversalScope = .subtree(at: []), + options: JSONTraversalOptions = .init(), + _ visitor: (JSONTraversalHit, JSON) throws -> JSONTraversalAction) + throws -> JSONOperationReport { + let report = try execute(scope: scope, options: options, collectionLimit: nil, visitor) + return .init(result: report.result.summary, statistics: report.statistics) + } + + /// Collects finite metadata through the same visitor engine and the same ledger. + /// - Parameters: + /// - scope: Strict absolute subtree selection. + /// - options: Explicit exploration and emission policies. + /// - maximumRecords: Nonnegative inclusive record bound; defaults to 100,000. + /// - Returns: Records, coverage and cumulative admission/traversal/output statistics. + /// - Throws: Collection-capacity refusal, typed access/configuration/resource errors or cancellation. + public func collect(scope: JSONTraversalScope = .subtree(at: []), + options: JSONTraversalOptions = .init(), maximumRecords: Int = 100_000) + throws -> JSONOperationReport { + try execute(scope: scope, options: options, collectionLimit: maximumRecords) { _, _ in .continue } + } + + private struct Frame { + let value: JSON + let path: JSONReadPath + var nextChild = 0 + var entered = false + } + + private struct Root { + let value: JSON + let path: JSONReadPath + } + + private func execute(scope selection: JSONTraversalScope, options: JSONTraversalOptions, + collectionLimit: Int?, + _ visitor: (JSONTraversalHit, JSON) throws -> JSONTraversalAction) + throws -> JSONOperationReport { + let scope = try JSONReadScope(document, limits: limits) + defer { scope.invalidate() } + let state = try scope.check() + state.meter.checkpointObserver = checkpointObserver + guard options.depthLimit.map({ $0 >= 0 }) ?? true, + collectionLimit.map({ $0 >= 0 }) ?? true else { + throw JSONFailure(.configuration, code: .invalidLimits) + } + try state.meter.retain(128) // fixed invocation summary/staging + let locations: [[JSONLocationComponent]], overlap: JSONScopeOverlapPolicy + try state.meter.retain(16) + switch selection { + case .subtree(let path): locations = [path]; overlap = .orderedOccurrences + case .virtual(let roots, let policy): locations = roots; overlap = policy + } + var roots: [Root] = [] + for location in locations { + try state.meter.work() + try state.meter.require(.pointerTokens, UInt64(location.count)) + var path = JSONReadPath(scope: scope, components: [], bytes: 0) + for part in location { + // Caller-supplied query keys have the same admission as a typed + // read-cursor key; generated child keys were admitted with the root. + if case .key(let key) = part { + try JSONAdmission.text(key.string, key: true, meter: &state.meter) + } + path = try scope.append(part, to: path.components, state: state) + _ = try scope.check() + } + let value = try scope.run(path.components) { try scope.select(path.components, state: $0) } + try state.meter.retain(64); roots.append(.init(value: value, path: path)) + } + if overlap == .rejectAmbiguous { + for second in roots.indices { + for first in 0.. 0 { + if action == .skipDescendants { + summary.prunedSubtrees += 1; frames[position].nextChild = childCount + summary.completion = .restricted + } else if options.depthLimit.map({ depth >= $0 }) ?? false { + summary.depthLimitedSubtrees += 1; frames[position].nextChild = childCount + summary.completion = .restricted + } + } + } + if frames[position].nextChild < childCount { + let ordinal = frames[position].nextChild + frames[position].nextChild += 1 + try state.meter.work() + let (component, child) = try Self.child(value, at: ordinal) + let path = try scope.append(component, to: frames[position].path.components, state: state) + try state.meter.retain(64) + frames.append(.init(value: child, path: path)) + } else { + frames.removeLast(); try state.meter.release(64) + } + } + // Refund live frontier only; consumed admission/path/visit work never resets. + while !frames.isEmpty { frames.removeLast(); try state.meter.release(64); try state.meter.work() } + if summary.completion == .visitorStopped { break } + } + while !seen.isEmpty { seen.removeLast(); try state.meter.release(16); try state.meter.work() } + while !roots.isEmpty { roots.removeLast(); try state.meter.release(64); try state.meter.work() } + try state.meter.release(144) + return try scope.complete(JSONTraversalCollection(nodes: records, summary: summary)) + } + + /// Exact metered prefix comparison; no String Set, value equality or unmetered hashing. + private static func path(_ lhs: [JSONLocationComponent], overlaps rhs: [JSONLocationComponent], + state: JSONCodingState) throws -> Bool { + for index in 0.. Int { + switch value { case .object(let o): return o.count; case .array(let a): return a.count; default: return 0 } + } + + private static func child(_ value: JSON, at index: Int) throws -> (JSONLocationComponent, JSON) { + switch value { + case .object(let object): return (.key(object[index].key), object[index].value) + case .array(let array): return (.index(index), array[index]) + default: throw JSONFailure(.configuration, code: .invalidOperation) + } + } + + private static func retainRecord(_ hit: JSONTraversalHit, state: JSONCodingState) throws { + try state.meter.retain(64) + for part in hit.absoluteLocation { + 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) } + } + } + } +} diff --git a/Sources/JSONNavigation/JSONTraversalTypes.swift b/Sources/JSONNavigation/JSONTraversalTypes.swift new file mode 100644 index 0000000..329ff20 --- /dev/null +++ b/Sources/JSONNavigation/JSONTraversalTypes.swift @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// A source value's existing JSON kind; absence is never a value kind. +public enum JSONTraversalKind: Sendable, Hashable, CaseIterable { + /// An ordered object. + case object + /// An array. + case array + /// An exact scalar-sequence string. + case string + /// An exact bounded decimal. + case number + /// A Boolean. + case boolean + /// Explicit JSON null. + case null + + internal init(_ value: JSON) { + switch value { + case .object: self = .object + case .array: self = .array + case .string: self = .string + case .number: self = .number + case .boolean: self = .boolean + case .null: self = .null + } + } +} + +/// An intentional synchronous visitor decision, never an operational-error substitute. +public enum JSONTraversalAction: Sendable { + /// Explore descendants normally. + case `continue` + /// Prune only this occurrence's descendants. + case skipDescendants + /// End this invocation with explicitly incomplete coverage. + case stop +} + +/// Explicit duplicate/ancestor treatment for an ordered selection of physical roots. +public enum JSONScopeOverlapPolicy: Sendable { + /// Reject repeated or ancestor/descendant root selections before any visitor call. + case rejectAmbiguous + /// Visit each selected subtree independently, including repeated occurrences. + case orderedOccurrences + /// Emit a physical typed path at most once; its first examined occurrence wins. + case uniquePhysicalNodes +} + +/// Existing physical subtrees in the captured canonical source, never a synthetic JSON array. +public enum JSONTraversalScope: Sendable { + /// A strict absolute typed path. An empty path selects the root, of any kind. + /// - Parameter at: Exact absolute key/index components. + case subtree(at: [JSONLocationComponent]) + /// Ordered roots, all strictly resolved before traversal. Empty selection is valid. + /// - Parameters: + /// - roots: Ordered absolute typed paths into the captured source. + /// - overlap: Explicit rejection, occurrence or exact-path deduplication policy. + case virtual(roots: [[JSONLocationComponent]], overlap: JSONScopeOverlapPolicy = .rejectAmbiguous) +} + +/// DFS emission and exploration policy. Admission always checks the entire captured source. +public struct JSONTraversalOptions: Sendable { + /// Maximum edges from a selected root, or nil for all admitted descendants. + public var depthLimit: Int? + /// Whether to emit each scope root; excluded roots are still visited. + public var includeScopeRoot: Bool + /// Kinds to emit, or nil for all six kinds. Filtering never prunes descendants. + public var emitKinds: Set? + /// Whether emitted hits include coherent original value spans, when available. + public var includeSourceSpans: Bool + + /// Configures deterministic insertion-order, depth-first preorder traversal. + /// - Parameters: + /// - depthLimit: Nonnegative edge bound; nil still uses finite core limits. + /// - includeScopeRoot: Emit roots as well as their descendants. + /// - emitKinds: Emission-only filter; an empty set emits no hits. + /// - includeSourceSpans: Request metered lookup of original value spans. + public init(depthLimit: Int? = nil, includeScopeRoot: Bool = true, + emitKinds: Set? = nil, includeSourceSpans: Bool = false) { + self.depthLimit = depthLimit; self.includeScopeRoot = includeScopeRoot + self.emitKinds = emitKinds; self.includeSourceSpans = includeSourceSpans + } +} + +/// Metadata for an existing node, without a retained subtree or an escaping cursor. +/// Paths address the traversal's captured document, not an arbitrary replacement root. +public struct JSONTraversalHit: Sendable, Equatable { + /// Absolute exact key/index components; [] is root and [.key("")] is not. + public let absoluteLocation: [JSONLocationComponent] + /// Zero-based root selection index; zero for a physical subtree. + public let scopeRootIndex: Int + /// Zero-based visited occurrence ordinal, including filtered nodes. + public let occurrenceIndex: UInt64 + /// Edges from this occurrence's selected scope root. + public let relativeDepth: Int + /// The unchanged source kind. + public let kind: JSONTraversalKind + /// An original value span only when requested from a coherent mapped document. + public let sourceSpan: JSONSourceSpan? +} + +/// Structural exploration coverage, independent of emission filters or source-map availability. +public enum JSONTraversalCompletion: Sendable { + /// All selected descendants were explored, even if filters emitted no hits. + case complete + /// A depth policy or visitor pruning omitted descendants. + case restricted + /// The visitor explicitly ended the walk; this never establishes global absence. + case visitorStopped +} + +/// Successful exploration facts. Resource failures and cancellation produce no summary. +public struct JSONTraversalSummary: Sendable { + /// Examined occurrences, including filtered nodes and explicitly repeated roots. + public internal(set) var visitedCount: UInt64 = 0 + /// Hits handed to the visitor or retained by collect. + public internal(set) var emittedCount: UInt64 = 0 + /// Maximum relative depth actually examined, zero for an empty selection. + public internal(set) var maximumDepthVisited: Int = 0 + /// Number of validated root selections. + public internal(set) var scopeRootCount: Int = 0 + /// Re-examined typed paths omitted under uniquePhysicalNodes; descendants are still explored. + public internal(set) var deduplicatedCount: UInt64 = 0 + /// Containers whose descendants were explicitly pruned by the visitor. + public internal(set) var prunedSubtrees: UInt64 = 0 + /// Containers whose descendants were omitted by the depth policy. + public internal(set) var depthLimitedSubtrees: UInt64 = 0 + /// Whether exploration was complete, policy-restricted or explicitly stopped. + public internal(set) var completion: JSONTraversalCompletion = .complete +} + +/// Bounded metadata records and their independent exploration coverage. +public struct JSONTraversalCollection: Sendable { + /// Source-ordered hits; no JSON subtrees or generated previews are retained. + public let nodes: [JSONTraversalHit] + /// Coverage and observed counts for the same invocation. + public let summary: JSONTraversalSummary +} + +/// A traversal-specific explicit capacity refusal, never a successful truncated result. +public enum JSONTraversalFailure: Error, Sendable, Equatable { + /// The next collected record would exceed the caller's finite inclusive bound. + /// - Parameter limit: The nonnegative inclusive maximum record count. + case collectionLimitExceeded(limit: Int) + /// Two validated virtual roots are identical or one is an ancestor of the other. + /// - Parameters: + /// - first: The earlier zero-based root selection index. + /// - second: The later zero-based root selection index. + case ambiguousRoots(first: Int, second: Int) +} diff --git a/Tests/JSONNavigationTests/Fixtures/Provenance.json b/Tests/JSONNavigationTests/Fixtures/Provenance.json new file mode 100644 index 0000000..cd3195e --- /dev/null +++ b/Tests/JSONNavigationTests/Fixtures/Provenance.json @@ -0,0 +1,19 @@ +{ + "origin": "Maintainer-provided Structural Intelligence S1 synthetic fixtures", + "packet_sha256": "d758f75d84271a97da515bf7b7ef70de7a61e91fdf949a511fe0fca2708156f8", + "adapted_algorithm": false, + "fixtures": [ + { + "path": "paths.json", + "sha256": "57ad0216911c5c581c6370500ea9564c6b0f906f4fb01c3e60d8789c4bf0ce26" + }, + { + "path": "structures.json", + "sha256": "756312755ad347361be8d7330e213c71c9c0e3faf4b4b1d9cdfa523cb0681945" + }, + { + "path": "source-revisions.json", + "sha256": "a28438f83a85c40ddcc69b174f517403ba6eb8dc94f3adf2bfadcd418878fe81" + } + ] +} diff --git a/Tests/JSONNavigationTests/Fixtures/paths.json b/Tests/JSONNavigationTests/Fixtures/paths.json new file mode 100644 index 0000000..8248119 --- /dev/null +++ b/Tests/JSONNavigationTests/Fixtures/paths.json @@ -0,0 +1,15 @@ +{ + "": "empty-key", + "0": "numeric-object-key", + "a/b": "slash-key", + "~": "tilde-key", + "a.b": "dotted-key", + "é": "composed", + "é": "decomposed", + "items": [ + "first-array-item", + { + "0": "nested-numeric-object-key" + } + ] +} diff --git a/Tests/JSONNavigationTests/Fixtures/source-revisions.json b/Tests/JSONNavigationTests/Fixtures/source-revisions.json new file mode 100644 index 0000000..8abe1ec --- /dev/null +++ b/Tests/JSONNavigationTests/Fixtures/source-revisions.json @@ -0,0 +1,18 @@ +{ + "rev1": { + "name": "sample", + "rows": [ + 1, + 2, + 3 + ] + }, + "rev2": { + "name": "sample", + "rows": [ + 1, + 2, + 4 + ] + } +} diff --git a/Tests/JSONNavigationTests/Fixtures/structures.json b/Tests/JSONNavigationTests/Fixtures/structures.json new file mode 100644 index 0000000..ed7cc0c --- /dev/null +++ b/Tests/JSONNavigationTests/Fixtures/structures.json @@ -0,0 +1,56 @@ +{ + "tree": { + "left": { + "leaf": 1 + }, + "right": [ + 2, + { + "leaf": 3 + } + ] + }, + "matrix": [ + [ + 0.1, + 9007199254740993 + ], + [ + -0.5, + 1.0 + ] + ], + "ragged": [ + [ + 1, + 2 + ], + [ + 3 + ] + ], + "graph": { + "nodes": [ + { + "id": "A" + }, + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + } + ] + }, + "virtual": [ + { + "x": 1 + }, + { + "x": 1 + } + ] +} diff --git a/Tests/JSONNavigationTests/NavigationBoundaryTests.swift b/Tests/JSONNavigationTests/NavigationBoundaryTests.swift new file mode 100644 index 0000000..8668968 --- /dev/null +++ b/Tests/JSONNavigationTests/NavigationBoundaryTests.swift @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONNavigation + +private func navigationFailure(_ body: () throws -> Void) -> JSONFailure? { + do { try body(); Issue.record("Expected a structured core failure"); return nil } + catch let error as JSONFailure { return error } + catch let error as JSONAccessFailure { return error.cause } + catch { Issue.record("Unexpected failure type: \(type(of: error))"); return nil } +} + +@Test func navigationAllScalarAndEmptyContainerRoots() throws { + for value: JSON in [.null, .boolean(true), .number(1), .string(""), .array([]), .object(try JSONObject())] { + let walk = JSONTraversal(document: .init(root: value)) + let report = try walk.collect() + #expect(report.result.nodes.count == 1 && report.result.nodes[0].absoluteLocation.isEmpty) + #expect(report.result.summary.completion == .complete) + #expect(try walk.collect(options: .init(includeScopeRoot: false)).result.nodes.isEmpty) + } +} + +@Test func navigationDepthZeroChildrenOnlyAndLeafCoverage() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":{"b":1},"c":2}"#))) + let zero = try walk.collect(options: .init(depthLimit: 0)) + #expect(zero.result.nodes.count == 1 && zero.result.summary.depthLimitedSubtrees == 1) + #expect(zero.result.summary.completion == .restricted) + let children = try walk.collect(options: .init(depthLimit: 1, includeScopeRoot: false)) + #expect(children.result.nodes.map(\.absoluteLocation) == [[.key("a")], [.key("c")]]) + let leaf = try walk.visit(scope: .subtree(at: [.key("c")]), options: .init(depthLimit: 0)) { _, _ in .skipDescendants } + #expect(leaf.result.completion == .complete && leaf.result.prunedSubtrees == 0) +} + +@Test func navigationStrictFailureCategoriesAndNoCallbacks() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":[null]}"#))) + let cases: [([JSONLocationComponent], JSONFailure.Category, JSONFailure.Code)] = [ + ([.key("missing")], .missingValue, .missingMember), + ([.index(0)], .typeMismatch, .wrongKind), + ([.key("a"), .key("0")], .typeMismatch, .wrongKind), + ([.key("a"), .index(-1)], .configuration, .invalidIndex), + ([.key("a"), .index(1)], .missingValue, .missingTarget), + ([.key("a"), .index(0), .key("x")], .typeMismatch, .wrongKind) + ] + for (path, category, code) in cases { + var calls = 0 + let failure = navigationFailure { _ = try walk.visit(scope: .subtree(at: path)) { _, _ in calls += 1; return .continue } } + #expect(failure?.category == category && failure?.code == code && calls == 0) + } +} + +@Test func navigationEmptyEmissionIsCompleteAndNotMissing() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":[1,null]}"#))) + let report = try walk.collect(options: .init(emitKinds: []), maximumRecords: 0) + #expect(report.result.nodes.isEmpty && report.result.summary.visitedCount == 4) + #expect(report.result.summary.completion == .complete) + #expect(navigationFailure { _ = try walk.collect(scope: .subtree(at: [.key("missing")]), options: .init(emitKinds: [])) }?.category == .missingValue) +} + +@Test func navigationInsertionOrderIsIndependentOfValueEquality() throws { + let first = try JSON.parse(#"{"z":1,"a":2}"#), second = try JSON.parse(#"{"a":2,"z":1}"#) + #expect(first == second) + let a = try JSONTraversal(document: .init(root: first)).collect().result.nodes.map(\.absoluteLocation) + let b = try JSONTraversal(document: .init(root: second)).collect().result.nodes.map(\.absoluteLocation) + #expect(a == [[], [.key("z")], [.key("a")]]) + #expect(b == [[], [.key("a")], [.key("z")]]) +} + +@Test func navigationPreservesExactDecimalsAndText() throws { + let root = try JSON.parse(#"[9007199254740993,0.1,1,1.0,1e0,"é","e\u0301"]"#) + var values: [JSON] = [] + _ = try JSONTraversal(document: .init(root: root)).visit(options: .init(includeScopeRoot: false)) { _, value in values.append(value); return .continue } + #expect(values[0].numberValue == (try JSONNumber(parsing: "9007199254740993"))) + #expect(values[1].numberValue == (try JSONNumber(parsing: "0.1"))) + #expect(values[2] == values[3] && values[3] == values[4]) + #expect(values[3].numberValue?.originalLexeme == "1.0") + #expect(values[4].numberValue?.originalLexeme == "1e0") + #expect(values[5] != values[6]) +} + +@Test func navigationSubtreeMetamorphismAndDeterminism() throws { + let root = try JSON.parse(#"{"x":[{"":"a","a/b":[]},{"~":{},".[]":0}],"y":1}"#) + let walk = JSONTraversal(document: .init(root: root)) + let all = try walk.collect().result.nodes + for path: [JSONLocationComponent] in [[], [.key("x")], [.key("x"), .index(0)], [.key("x"), .index(1), .key("~")]] { + let subset = try walk.collect(scope: .subtree(at: path)).result.nodes + let expected = all.filter { $0.absoluteLocation.starts(with: path) }.map(\.absoluteLocation) + #expect(subset.map(\.absoluteLocation) == expected) + #expect(subset.map(\.relativeDepth) == subset.map { $0.absoluteLocation.count - path.count }) + #expect(try walk.collect(scope: .subtree(at: path)).result.nodes == subset) + } +} + +@Test func navigationMappedAndReplacedSourceCoherence() throws { + let source = try JSONParser().parseDocument(Data(#"{"a/b": [1.0, null], "é":2,"e\u0301":3}"#.utf8), source: "synthetic") + let original = JSONTraversal(document: source) + let mapped = try original.collect(options: .init(includeSourceSpans: true)) + for hit in mapped.result.nodes { + #expect(hit.sourceSpan == (try source.sourceMap?.span(at: hit.absoluteLocation))) + #expect(hit.sourceSpan != nil) + } + let rewritten = source.replacingRoot(.array([.null])) + let new = try JSONTraversal(document: rewritten).collect(options: .init(includeSourceSpans: true)) + #expect(new.result.nodes.allSatisfy { $0.sourceSpan == nil }) + #expect(original.document.root == source.root && original.document.sourceBytes != nil) + #expect(rewritten.sourceMap == nil && rewritten.sourceBytes == nil && rewritten.source == nil) + #expect(try original.collect(options: .init(includeSourceSpans: true)).result.nodes == mapped.result.nodes) +} + +@Test func navigationDeepIterativeWalkAndFullAdmission() throws { + var value = JSON.null + for _ in 0..<200 { value = .array([value]) } + var limits = JSONLimits.balanced; limits[.maxDepth] = 256; limits[.pointerTokens] = 256 + let report = try JSONTraversal(document: .init(root: value), limits: limits).collect() + #expect(report.result.nodes.count == 201 && report.result.summary.maximumDepthVisited == 200) + let failure = navigationFailure { _ = try JSONTraversal(document: .init(root: value)).collect(options: .init(depthLimit: 0)) } + #expect(failure?.resource?.metric == .maxDepth) +} + +@Test func navigationWideObjectDoesNotSortOrLoseKeys() throws { + let members = (0..<1000).map { JSONMember(JSONKey("k\(999 - $0)"), value: .number(JSONNumber(integer: $0))) } + let root = JSON.object(try JSONObject(members: members)) + let report = try JSONTraversal(document: .init(root: root)).collect() + #expect(report.result.nodes.dropFirst().map(\.absoluteLocation) == members.map { [.key($0.key)] }) + #expect(report.result.summary.visitedCount == 1001 && report.statistics[.nodes] == 1001) +} + +@Test func navigationCallerErrorsPropagateAndCopiesAreCallerOwned() throws { + enum Caller: Error { case witness } + let walk = JSONTraversal(document: .init(root: .array([1,2]))) + #expect(throws: Caller.witness) { try walk.visit { _, _ in throw Caller.witness } } + var retained: [JSONTraversalHit] = [] + let report = try walk.visit { hit, _ in retained.append(hit); return .continue } + #expect(retained.count == 3 && report.statistics[.retainedBytes] == 0) +} + +@Test func navigationCollectionBoundaryAndInvalidPolicies() throws { + let walk = JSONTraversal(document: .init(root: .array([1,2]))) + #expect(try walk.collect(maximumRecords: 3).result.nodes.count == 3) + #expect(throws: JSONTraversalFailure.collectionLimitExceeded(limit: 2)) { try walk.collect(maximumRecords: 2) } + #expect(throws: JSONTraversalFailure.collectionLimitExceeded(limit: 0)) { try walk.collect(maximumRecords: 0) } + #expect(navigationFailure { _ = try walk.collect(maximumRecords: -1) }?.category == .configuration) + #expect(navigationFailure { _ = try walk.collect(options: .init(depthLimit: -1)) }?.category == .configuration) +} + +@Test func navigationRetainedOutputAndInclusivePeak() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"long/key":[1,2]}"#))) + let report = try walk.collect() + let expected: UInt64 = report.result.nodes.reduce(0) { total, hit in + total + 64 + hit.absoluteLocation.reduce(UInt64(0)) { n, part in + switch part { case .key(let key): return n + 32 + UInt64(key.string.utf8.count); case .index: return n + 32 } + } + } + #expect(report.statistics[.retainedBytes] == expected) + var limits = JSONLimits.balanced; limits[.retainedBytes] = report.statistics.peakLogicalRetainedBytes + #expect(try JSONTraversal(document: walk.document, limits: limits).collect().statistics.peakLogicalRetainedBytes == limits[.retainedBytes]) + limits[.retainedBytes] -= 1 + #expect(navigationFailure { _ = try JSONTraversal(document: walk.document, limits: limits).collect() }?.resource?.metric == .retainedBytes) +} + +@Test func navigationCorePathNodeAndWorkBounds() throws { + let document = JSONDocument(root: .array([.null])) + var limits = JSONLimits.balanced; limits[.pointerTokens] = 0 + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect() }?.resource?.metric == .pointerTokens) + #expect(try JSONTraversal(document: document, limits: limits).collect(options: .init(depthLimit: 0)).result.nodes.count == 1) + limits = .balanced; limits[.pointerBytes] = 1 + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect() }?.resource?.metric == .pointerBytes) + limits = .balanced; limits[.nodes] = 1 + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect(options: .init(depthLimit: 0)) }?.resource?.metric == .nodes) + limits = .balanced; limits[.workUnits] = 0 + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect() }?.resource?.metric == .workUnits) +} + +@Test func navigationCancellationBeforeAdmission() async { + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + return try JSONTraversal(document: .init(root: .null)).collect() + } + do { _ = try await task.value; Issue.record("Cancelled admission succeeded") } + catch { #expect(error is CancellationError) } +} + +@Test func navigationCancellationAfterVisitorCannotBecomeStop() async { + let task = Task { + try JSONTraversal(document: .init(root: .array([1,2]))).visit { _, _ in + withUnsafeCurrentTask { $0?.cancel() } + return .stop + } + } + do { _ = try await task.value; Issue.record("Cancellation became successful stop") } + catch { #expect(error is CancellationError) } +} + +@Test func navigationCancellationInsideOwnedFilteredLoop() async throws { + let document = JSONDocument(root: .array(Array(repeating: .null, count: 1000))) + let admitted = try document.readWithReport { _ in () }.statistics[.workUnits] + let task = Task { + var walk = JSONTraversal(document: document) + walk.checkpointObserver = { work in + if work >= admitted + 100 { withUnsafeCurrentTask { $0?.cancel() } } + } + return try walk.collect(options: .init(emitKinds: [])) + } + do { _ = try await task.value; Issue.record("Owned filtered loop ignored cancellation") } + catch { #expect(error is CancellationError) } +} + +@Test func navigationSharedImmutableRootIndependentInvocations() async throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"é":[1,2],"e\u0301":[3]}"#))) + let expected = try walk.collect().result.nodes + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0..<12 { + group.addTask { () async throws -> Void in + let actual = try walk.collect().result.nodes + #expect(actual == expected) + } + } + try await group.waitForAll() + } +} + +@Test func navigationDocumentSourceAndSpanWorkAreCumulative() throws { + let document = try JSONParser().parseDocument(Data(#"{"a":[1,2]}"#.utf8)) + let basic = try JSONTraversal(document: document).collect() + let mapped = try JSONTraversal(document: document).collect(options: .init(includeSourceSpans: true)) + #expect(mapped.statistics[.workUnits] > basic.statistics[.workUnits]) + var limits = JSONLimits.balanced; limits[.inputBytes] = UInt64(document.sourceBytes!.count) + #expect(try JSONTraversal(document: document, limits: limits).collect().result.nodes.count == 4) + limits[.inputBytes] -= 1 + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect() }?.resource?.metric == .inputBytes) + limits = .balanced; limits[.workUnits] = basic.statistics[.workUnits] + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect(options: .init(includeSourceSpans: true)) }?.resource?.metric == .workUnits) +} + +@Test func navigationFailedInvocationDoesNotPoisonFreshWork() throws { + let document = JSONDocument(root: .array([1,2])) + var limits = JSONLimits.balanced; limits[.workUnits] = 1 + #expect(navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect() }?.category == .resource) + #expect(try JSONTraversal(document: document).collect().result.summary.completion == .complete) +} + +@Test func navigationRequestedKeyAdmissionMatchesCoreCursor() throws { + let document = JSONDocument(root: .object(try JSONObject())) + for metric: JSONResourceMetric in [.keyBytes, .aggregateTextBytes] { + var limits = JSONLimits.balanced; limits[metric] = 1 + let key = JSONKey("long") + let core = navigationFailure { _ = try document.read(limits: limits) { try $0[key].requiredValue() } } + #expect(core?.category == .resource && core?.resource?.metric == metric) + for selection: JSONTraversalScope in [.subtree(at: [.key(key)]), .virtual(roots: [[.key(key)]])] { + let failure = navigationFailure { _ = try JSONTraversal(document: document, limits: limits).collect(scope: selection) } + #expect(failure == core) + } + } +} diff --git a/Tests/JSONNavigationTests/PacketWitnessTests.swift b/Tests/JSONNavigationTests/PacketWitnessTests.swift new file mode 100644 index 0000000..4dd4afe --- /dev/null +++ b/Tests/JSONNavigationTests/PacketWitnessTests.swift @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONNavigation + +private func navigationFixture(_ name: String) throws -> JSON { + let folder = URL(fileURLWithPath: #filePath).deletingLastPathComponent().appendingPathComponent("Fixtures") + return try JSONParser().parse(Data(contentsOf: folder.appendingPathComponent(name))) +} + +@Test func navigationPinnedPacketPathsAndUnevenDFS() throws { + let root = try navigationFixture("paths.json") + var witnessed = 0 + _ = try JSONTraversal(document: .init(root: root)).visit { hit, value in + let tokens = hit.absoluteLocation.map { p in + switch p { case .key(let k): return k.string; case .index(let i): return String(i) } + } + let resolved = try JSONPointer(tokens: tokens).resolve(in: root) + #expect(resolved == value) + witnessed += 1 + return .continue + } + #expect(witnessed == 12) + let tree = try navigationFixture("structures.json") + let paths = try JSONTraversal(document: .init(root: tree)).collect(scope: .subtree(at: [.key("tree")])).result.nodes.map(\.absoluteLocation) + #expect(paths == [[.key("tree")], [.key("tree"), .key("left")], [.key("tree"), .key("left"), .key("leaf")], [.key("tree"), .key("right")], [.key("tree"), .key("right"), .index(0)], [.key("tree"), .key("right"), .index(1)], [.key("tree"), .key("right"), .index(1), .key("leaf")]]) +} + +@Test func navigationCapturedRevisionNeverRebinds() throws { + let revisions = try navigationFixture("source-revisions.json") + let old = try revisions.requiredMember("rev1"), new = try revisions.requiredMember("rev2") + let oldTraversal = JSONTraversal(document: .init(root: old)) + let newTraversal = JSONTraversal(document: oldTraversal.document.replacingRoot(new)) + var oldTail = JSON.null, newTail = JSON.null + _ = try oldTraversal.visit { hit, value in if hit.absoluteLocation == [.key("rows"), .index(2)] { oldTail = value }; return .continue } + _ = try newTraversal.visit { hit, value in if hit.absoluteLocation == [.key("rows"), .index(2)] { newTail = value }; return .continue } + #expect(oldTail == 3 && newTail == 4 && oldTraversal.document.root != newTraversal.document.root) +} diff --git a/Tests/JSONNavigationTests/PhysicalTraversalTests.swift b/Tests/JSONNavigationTests/PhysicalTraversalTests.swift new file mode 100644 index 0000000..fc48974 --- /dev/null +++ b/Tests/JSONNavigationTests/PhysicalTraversalTests.swift @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +import JSONNavigation + +@Test func navigationPhysicalPreorderAndTypedAuthority() throws { + let root = try JSON.parse(#"{"":"empty","0":"key","items":[{"a/b":9007199254740993},null],"é":1,"e\u0301":2}"#) + let walk = JSONTraversal(document: .init(root: root)) + var paths: [[JSONLocationComponent]] = [] + let report = try walk.visit { hit, value in + paths.append(hit.absoluteLocation) + let tokens = hit.absoluteLocation.map { part in + switch part { case .key(let k): return k.string; case .index(let i): return String(i) } + } + #expect(try JSONPointer(tokens: tokens).resolve(in: root) == value) + let read = try root.read { cursor in + var selected = cursor + for part in hit.absoluteLocation { + switch part { case .key(let k): selected = try selected[k]; case .index(let i): selected = try selected[i] } + } + return try selected.requiredValue() + } + #expect(read == value) + return .continue + } + #expect(paths == [[], [.key("")], [.key("0")], [.key("items")], [.key("items"), .index(0)], [.key("items"), .index(0), .key("a/b")], [.key("items"), .index(1)], [.key("é")], [.key("e\u{301}")]]) + #expect(report.result.completion == .complete) + #expect(report.result.visitedCount == 9) + #expect(report.statistics[.nodes] == 9) // exactly one root admission + #expect(report.statistics[.retainedBytes] == 0) +} + +@Test func navigationPhysicalScalarAndStrictFailures() throws { + let scalar = JSONTraversal(document: .init(root: .null)) + #expect(try scalar.collect().result.nodes.count == 1) + #expect(try scalar.collect(options: .init(includeScopeRoot: false)).result.nodes.isEmpty) + let root = try JSON.parse(#"{"x":null,"a":[1]}"#) + let walk = JSONTraversal(document: .init(root: root)) + #expect(try walk.collect(scope: .subtree(at: [.key("x")])).result.nodes.first?.kind == .null) + for path: [JSONLocationComponent] in [[.key("missing")], [.key("a"), .key("0")], [.key("a"), .index(-1)], [.key("a"), .index(1)], [.key("x"), .key("z")]] { + #expect(throws: (any Error).self) { try walk.collect(scope: .subtree(at: path)) } + } +} + +@Test func navigationPhysicalPoliciesDoNotConflateCoverage() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":[1,2],"b":3}"#))) + let filtered = try walk.collect(options: .init(emitKinds: [.number])) + #expect(filtered.result.summary.visitedCount == 5 && filtered.result.nodes.count == 3) + #expect(filtered.result.summary.completion == .complete) + let shallow = try walk.collect(options: .init(depthLimit: 1, includeScopeRoot: false)) + #expect(shallow.result.nodes.count == 2 && shallow.result.summary.completion == .restricted) + let stopped = try walk.visit { _, _ in .stop } + #expect(stopped.result.completion == .visitorStopped && stopped.result.visitedCount == 1) + let pruned = try walk.visit { hit, _ in hit.absoluteLocation == [.key("a")] ? .skipDescendants : .continue } + #expect(pruned.result.visitedCount == 3 && pruned.result.prunedSubtrees == 1) +} + +@Test func navigationOneLedgerAdmissionSelectionAndCollection() throws { + let root = try JSON.parse(#"{"a":[1,2]}"#) + let full = try JSONTraversal(document: .init(root: root)).collect(scope: .subtree(at: [.key("a")])) + let used = full.statistics[.workUnits] + var exact = JSONLimits.balanced; exact[.workUnits] = used + #expect(try JSONTraversal(document: .init(root: root), limits: exact).collect(scope: .subtree(at: [.key("a")])).statistics[.workUnits] == used) + exact[.workUnits] = used - 1 + #expect(throws: JSONFailure.self) { try JSONTraversal(document: .init(root: root), limits: exact).collect(scope: .subtree(at: [.key("a")])) } + #expect(throws: JSONTraversalFailure.collectionLimitExceeded(limit: 2)) { try JSONTraversal(document: .init(root: root)).collect(maximumRecords: 2) } + #expect(full.statistics[.nodes] == 4 && full.statistics[.retainedBytes] > 0) +} diff --git a/Tests/JSONNavigationTests/VirtualScopeTests.swift b/Tests/JSONNavigationTests/VirtualScopeTests.swift new file mode 100644 index 0000000..fc5addd --- /dev/null +++ b/Tests/JSONNavigationTests/VirtualScopeTests.swift @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +import JSONNavigation + +@Test func navigationVirtualRejectsAmbiguityBeforeCallbacks() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":{"b":1},"c":1}"#))) + for roots: [[JSONLocationComponent]] in [[[.key("a")], [.key("a")]], [[.key("a")], [.key("a"), .key("b")]], [[], [.key("c")]]] { + var calls = 0 + #expect(throws: JSONTraversalFailure.ambiguousRoots(first: 0, second: 1)) { + try walk.visit(scope: .virtual(roots: roots)) { _, _ in calls += 1; return .continue } + } + #expect(calls == 0) + } +} + +@Test func navigationVirtualOrderedRepeatsRetainOriginAndDepth() throws { + let document = JSONDocument(root: try JSON.parse(#"{"a":{"b":1},"c":1}"#)) + let report = try JSONTraversal(document: document).collect(scope: .virtual(roots: [[.key("a")], [.key("a"), .key("b")], [.key("a")]], overlap: .orderedOccurrences)) + #expect(report.result.nodes.map(\.scopeRootIndex) == [0,0,1,2,2]) + #expect(report.result.nodes.map(\.relativeDepth) == [0,1,0,0,1]) + #expect(report.result.nodes.map(\.occurrenceIndex) == [0,1,2,3,4]) + #expect(report.result.summary.visitedCount == 5 && report.result.summary.deduplicatedCount == 0) + #expect(report.statistics[.nodes] == 4) // input admission is not restarted for each root +} + +@Test func navigationVirtualUniqueUsesPathsRatherThanEqualValues() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":1,"b":1,"é":1,"e\u0301":1}"#))) + let report = try walk.collect(scope: .virtual(roots: [[.key("a")], [], [.key("a")]], overlap: .uniquePhysicalNodes)) + #expect(report.result.nodes.map(\.absoluteLocation) == [[.key("a")], [], [.key("b")], [.key("é")], [.key("e\u{301}")]]) + #expect(report.result.nodes.map(\.scopeRootIndex) == [0,1,1,1,1]) + #expect(report.result.summary.deduplicatedCount == 2 && report.result.summary.visitedCount == 7) + #expect(report.result.summary.completion == .complete) +} + +@Test func navigationVirtualExactUnicodeAndSiblingRootsAreUnambiguous() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"é":1,"e\u0301":2,"0":3,"a":[4]}"#))) + let report = try walk.collect(scope: .virtual(roots: [[.key("é")], [.key("e\u{301}")], [.key("0")], [.key("a"), .index(0)]])) + #expect(report.result.nodes.count == 4 && report.result.summary.completion == .complete) +} + +@Test func navigationVirtualValidatesEveryRootBeforeAnyCallback() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":1}"#))) + for policy: JSONScopeOverlapPolicy in [.rejectAmbiguous, .orderedOccurrences, .uniquePhysicalNodes] { + var calls = 0 + do { + _ = try walk.visit(scope: .virtual(roots: [[.key("a")], [.key("missing")]], overlap: policy)) { _, _ in calls += 1; return .continue } + Issue.record("Missing virtual root accepted") + } catch let error as JSONAccessFailure { #expect(error.cause.category == .missingValue) } + #expect(calls == 0) + } +} + +@Test func navigationVirtualEmptySelectionIsCompleteAfterAdmission() throws { + let report = try JSONTraversal(document: .init(root: .array([1,2]))).collect(scope: .virtual(roots: []), maximumRecords: 0) + #expect(report.result.nodes.isEmpty && report.result.summary.scopeRootCount == 0) + #expect(report.result.summary.completion == .complete && report.statistics[.nodes] == 3) +} + +@Test func navigationVirtualUniqueDoesNotPruneRevisitedFrontier() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":{"b":{"c":1}}}"#))) + let report = try walk.collect(scope: .virtual(roots: [[], [.key("a")]], overlap: .uniquePhysicalNodes), options: .init(depthLimit: 2)) + #expect(report.result.nodes.map(\.absoluteLocation) == [[], [.key("a")], [.key("a"), .key("b")], [.key("a"), .key("b"), .key("c")]]) + #expect(report.result.nodes.last?.relativeDepth == 2 && report.result.nodes.last?.scopeRootIndex == 1) + #expect(report.result.summary.depthLimitedSubtrees == 1 && report.result.summary.completion == .restricted) +} + +@Test func navigationVirtualComparisonAndCollectionShareExactBudget() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":[1,2],"b":[3,4]}"#))) + let selection = JSONTraversalScope.virtual(roots: [[.key("a")], [.key("b")]]) + let baseline = try walk.collect(scope: selection) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + #expect(try JSONTraversal(document: walk.document, limits: limits).collect(scope: selection).statistics == baseline.statistics) + limits[.workUnits] -= 1 + do { _ = try JSONTraversal(document: walk.document, limits: limits).collect(scope: selection); Issue.record("Work quota reset") } + catch let error as JSONFailure { #expect(error.resource?.metric == .workUnits) } + #expect(try walk.collect(scope: selection).result.nodes == baseline.result.nodes) +} + +@Test func navigationVirtualUniqueFilteredRootFirstOccurrenceWins() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"a":1}"#))) + let report = try walk.collect(scope: .virtual(roots: [[.key("a")], []], overlap: .uniquePhysicalNodes), options: .init(includeScopeRoot: false)) + #expect(report.result.nodes.isEmpty && report.result.summary.deduplicatedCount == 1) + #expect(report.result.summary.completion == .complete) +} + +@Test func navigationVirtualOverlapComparisonCannotHideWork() throws { + let keys = (0..<60).map { JSONKey(String(repeating: "x", count: 120) + String($0)) } + let members = keys.map { JSONMember($0, value: .null) } + let document = JSONDocument(root: .object(try JSONObject(members: members))) + let roots = keys.map { [JSONLocationComponent.key($0)] } + let options = JSONTraversalOptions(depthLimit: 0, emitKinds: []) + let baseline = try JSONTraversal(document: document).collect(scope: .virtual(roots: roots, overlap: .orderedOccurrences), options: options) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + do { + _ = try JSONTraversal(document: document, limits: limits).collect(scope: .virtual(roots: roots), options: options) + Issue.record("Pairwise exact comparisons escaped the cumulative quota") + } catch let error as JSONFailure { #expect(error.resource?.metric == .workUnits) } +} diff --git a/Tools/NavigationQualification/Consumer.swift b/Tools/NavigationQualification/Consumer.swift new file mode 100644 index 0000000..0859268 --- /dev/null +++ b/Tools/NavigationQualification/Consumer.swift @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// Compiled as an external module by Scripts/qualify_navigation.py; no private imports. +import Foundation +import JSON +import JSONNavigation + +@main struct NavigationConsumer { + static func main() throws { + var checks = 0 + func check(_ id: String, _ condition: Bool) { + precondition(condition, id); checks += 1 + print("NAVIGATION_CHECK \(id) passed") + } + let text = #"{"":"empty","0":"key","items":[{"n":9007199254740993},null],"é":1,"e\u0301":2,"null":null}"# + let root = try JSONParser().parse(text) + // BEFORE: the core continues to own one-node lookup and native projection. + let whole = try JSONPointer(parsing: "").resolve(in: root) + let empty = try JSONPointer(parsing: "/").resolve(in: root) + check("root-empty-key", whole == root && empty == .string("empty")) + let keyPath: [JSONLocationComponent] = [.key("0")] + let indexPath: [JSONLocationComponent] = [.index(0)] + check("typed-key-index", keyPath != indexPath) + let presence = try root.read { cursor in + (try cursor.presence(of: "null"), try cursor.presence(of: "absent")) + } + check("presence", presence.0 == .null && presence.1 == .absent) + check("scalar-keys", root.objectValue!.count == 6) + let count = try root.read { try $0["items"][0]["n"].requiredInteger(as: UInt64.self) } + check("native-exact", count == 9_007_199_254_740_993) + + // AFTER: one many-node operation, exact metadata and unchanged source values. + let traversal = JSONTraversal(document: JSONDocument(root: root)) + var parity = true + let visit = try traversal.visit { hit, value in + let tokens = hit.absoluteLocation.map { part in + switch part { case .key(let key): return key.string; case .index(let i): return String(i) } + } + let selected = try JSONPointer(tokens: tokens).resolve(in: root) + parity = parity && selected == value + return .continue + } + check("pointer-parity", parity && visit.result.visitedCount == 10) + let collected = try traversal.collect() + check("metadata-collection", collected.result.nodes.count == 10 && collected.result.summary.completion == .complete) + let path: [JSONLocationComponent] = [.key("items"), .index(0)] + let repeated = try traversal.collect(scope: .virtual(roots: [path,path], overlap: .orderedOccurrences)) + check("ordered-occurrences", repeated.result.nodes.map(\.scopeRootIndex) == [0,0,1,1]) + let unique = try traversal.collect(scope: .virtual(roots: [path,path], overlap: .uniquePhysicalNodes)) + check("unique-paths", unique.result.nodes.count == 2 && unique.result.summary.deduplicatedCount == 2) + var ambiguous = false + do { _ = try traversal.collect(scope: .virtual(roots: [path,path])) } + catch JSONTraversalFailure.ambiguousRoots(first: 0, second: 1) { ambiguous = true } + check("default-overlap-rejection", ambiguous) + var missing = false + do { _ = try traversal.collect(scope: .subtree(at: [.key("missing")])) } + catch let error as JSONAccessFailure { missing = error.cause.category == .missingValue } + check("typed-missing", missing) + let filtered = try traversal.collect(options: .init(emitKinds: []), maximumRecords: 0) + check("complete-empty-filter", filtered.result.nodes.isEmpty && filtered.result.summary.completion == .complete && filtered.result.summary.visitedCount == 10) + var capacity = false + do { _ = try traversal.collect(maximumRecords: 0) } + catch JSONTraversalFailure.collectionLimitExceeded(limit: 0) { capacity = true } + check("collection-limit", capacity) + check("cumulative-report", collected.statistics[.nodes] == 10 && collected.statistics[.workUnits] > 0 && collected.statistics.domain == .core) + let document = try JSONParser().parseDocument(Data(text.utf8)) + let spans = try JSONTraversal(document: document).collect(options: .init(includeSourceSpans: true)) + check("coherent-spans", spans.result.nodes.allSatisfy { $0.sourceSpan != nil }) + let replaced = try JSONTraversal(document: document.replacingRoot(.null)).collect(options: .init(includeSourceSpans: true)) + check("replacement-invalidates-spans", replaced.result.nodes[0].sourceSpan == nil) + let stop = try traversal.visit { _, _ in .stop } + check("intentional-stop", stop.result.completion == .visitorStopped && stop.result.visitedCount == 1) + var limits = JSONLimits.balanced; limits[.workUnits] = collected.statistics[.workUnits] - 1 + var quota = false + do { _ = try JSONTraversal(document: traversal.document, limits: limits).collect() } + catch let error as JSONFailure { quota = error.resource?.metric == .workUnits } + check("cumulative-quota", quota) + print("NAVIGATION_RESOURCE work=\(collected.statistics[.workUnits]) retained=\(collected.statistics[.retainedBytes]) peak=\(collected.statistics.peakLogicalRetainedBytes) nodes=\(collected.statistics[.nodes])") + print("NAVIGATION_CONSUMER passed cases=\(checks)") + } +}