From 917dab492883178b0ea9b57bce3a526b597e6373 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:30:17 +0200 Subject: [PATCH 01/25] feat: integrate opt-in exact value and schema DSLs Keep trusted authored staging separate from finite iterative finalization. Reuse one engine ledger through schema document admission; preserve exact keys, numbers, omission and native schema authority. Add real-engine semantic, lifecycle, compiler and public consumer qualification. --- AGENTS.md | 4 +- CHANGELOG.md | 5 + Documentation/Architecture/API.md | 28 +- .../Architecture/DualDSL/Decisions.md | 87 +++ Documentation/Architecture/DualDSL/Plan.md | 39 + .../Architecture/DualDSL/Progress.json | 18 + .../Architecture/ExtractionManifest.json | 54 +- Documentation/Architecture/ResourceLimits.md | 11 + Documentation/CI/README.md | 19 +- Documentation/DocumentationTooling.md | 10 +- Documentation/Qualification/DualDSL/README.md | 24 + Documentation/README.md | 8 + Examples/Consumer/Package.swift | 4 +- .../Sources/Consumer/READMEExamples.swift | 18 + Examples/DualDSL/Package.swift | 15 + Examples/DualDSL/README.md | 11 + .../Sources/DualDSLConsumer/main.swift | 62 ++ Package.swift | 8 +- README.md | 45 +- Scripts/build_documentation.py | 6 +- Scripts/build_documentation_site.py | 14 +- Scripts/ci.py | 36 +- Scripts/documentation_catalog.py | 10 +- Scripts/qualify_dual_dsl.py | 167 ++++ Scripts/wasm_support.py | 4 +- .../Documentation.docc/JSONDSLCore.md | 38 + Sources/JSONDSLCore/JSONContent.swift | 545 ++++++++++++++ Sources/JSONDSLCore/JSONDSLNode.swift | 172 +++++ .../Documentation.docc/AuthoredSchemas.md | 54 ++ .../Documentation.docc/JSONSchemaDSL.md | 40 + Sources/JSONSchemaDSL/JSONSchemaDSL.swift | 711 ++++++++++++++++++ .../Documentation.docc/AuthoredValues.md | 43 ++ .../Documentation.docc/JSONValueDSL.md | 28 + Sources/JSONValueDSL/JSONValueDSL.swift | 187 +++++ .../DualDSL/DefinitionInsideProperties.swift | 2 + .../DualDSL/ElementInsideObject.swift | 3 + .../DualDSL/FieldInsideSchema.swift | 3 + .../DualDSL/ImplicitFloatingPoint.swift | 2 + .../DualDSL/PropertyInsideDefinitions.swift | 2 + .../DualDSL/SchemaClauseInsideArray.swift | 3 + Tests/JSONDSLTests/DSLPrototypeTests.swift | 298 ++++++++ Tests/JSONDSLTests/IntegrationTests.swift | 36 + .../JSONDSLTests/ResourceLifecycleTests.swift | 220 ++++++ Tests/JSONDSLTests/SemanticTests.swift | 479 ++++++++++++ Tests/JSONDSLTests/StressTypeCheckTests.swift | 259 +++++++ 45 files changed, 3794 insertions(+), 38 deletions(-) create mode 100644 Documentation/Architecture/DualDSL/Decisions.md create mode 100644 Documentation/Architecture/DualDSL/Plan.md create mode 100644 Documentation/Architecture/DualDSL/Progress.json create mode 100644 Documentation/Qualification/DualDSL/README.md create mode 100644 Examples/DualDSL/Package.swift create mode 100644 Examples/DualDSL/README.md create mode 100644 Examples/DualDSL/Sources/DualDSLConsumer/main.swift create mode 100644 Scripts/qualify_dual_dsl.py create mode 100644 Sources/JSONDSLCore/Documentation.docc/JSONDSLCore.md create mode 100644 Sources/JSONDSLCore/JSONContent.swift create mode 100644 Sources/JSONDSLCore/JSONDSLNode.swift create mode 100644 Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md create mode 100644 Sources/JSONSchemaDSL/Documentation.docc/JSONSchemaDSL.md create mode 100644 Sources/JSONSchemaDSL/JSONSchemaDSL.swift create mode 100644 Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md create mode 100644 Sources/JSONValueDSL/Documentation.docc/JSONValueDSL.md create mode 100644 Sources/JSONValueDSL/JSONValueDSL.swift create mode 100644 Tests/CompileNegative/DualDSL/DefinitionInsideProperties.swift create mode 100644 Tests/CompileNegative/DualDSL/ElementInsideObject.swift create mode 100644 Tests/CompileNegative/DualDSL/FieldInsideSchema.swift create mode 100644 Tests/CompileNegative/DualDSL/ImplicitFloatingPoint.swift create mode 100644 Tests/CompileNegative/DualDSL/PropertyInsideDefinitions.swift create mode 100644 Tests/CompileNegative/DualDSL/SchemaClauseInsideArray.swift create mode 100644 Tests/JSONDSLTests/DSLPrototypeTests.swift create mode 100644 Tests/JSONDSLTests/IntegrationTests.swift create mode 100644 Tests/JSONDSLTests/ResourceLifecycleTests.swift create mode 100644 Tests/JSONDSLTests/SemanticTests.swift create mode 100644 Tests/JSONDSLTests/StressTypeCheckTests.swift diff --git a/AGENTS.md b/AGENTS.md index 03c4ab6..fbc61dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,11 @@ Swift-JSON requires Swift tools/compiler 6.4+, Swift language mode 6 and macOS/SDK 27+. Read README.md, CONTRIBUTING.md and the relevant contracts in Documentation/Architecture before changing behavior. -- Keep six public products: JSON, JSONSchema, JSONStreaming, JSONPatch, JSONHTML and JSONBridges. +- Keep eight public products: JSON, JSONSchema, JSONStreaming, JSONPatch, JSONHTML, JSONBridges, JSONValueDSL and JSONSchemaDSL. JSONHTML depends on JSON only; the maintainer-approved 1.3 addition preserves historical four-product releases. The E2-approved opt-in JSONBridges depends on JSON only and is never re-exported by core. + The P1.2-approved optional DSL products use JSONDSLCore without a standalone core product. + Trusted authored DSL staging is outside finalization quotas; never imply a staging/RSS bound. 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/CHANGELOG.md b/CHANGELOG.md index 0b102a8..f3e03c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add opt-in JSONValueDSL and JSONSchemaDSL products for typed, trusted authored + values and Draft 2020-12 declarations. Exact values and schema semantics remain + owned by the existing engine. Finalization uses one finite iterative ledger; + authored staging and callbacks are explicitly outside its resource accounting. + - Add E2's shallow JSON parsing, checked object construction, decimal-text and pre-parsed Pointer entry points; exact-key membership and three-state presence composition; and direct validation from an immutable compiled schema plan. diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index bd17ce9..9d9d73c 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -1,7 +1,7 @@ # Public API contract This document describes the implemented public interface. Native compiler symbol -graphs supply the complete declarations in the six DocC catalogs. Examples +graphs supply the complete declarations in the eight product DocC catalogs and the shared DSL structure reference. Examples in the [README](../../README.md) are compiled and executed by the public consumer. ## Products and ownership @@ -9,6 +9,8 @@ in the [README](../../README.md) are compiled and executed by the public consume | Product | Main entry points | Owned implementation boundary | | --- | --- | --- | | JSON | JSON, JSONNumber, JSONObject, JSONParser, JSONWriter, JSONValueEncoder, JSONValueDecoder, JSONPointer, JSONDocument | Shared strict codec, exact decimal model, bounded traversal and native coding | +| JSONValueDSL | JSONValueDSL.object/array, Field, Element, Elements | Trusted authored staging; finite iterative exact finalization | +| JSONSchemaDSL | JSONSchemaDSL.document/boolean, Schema, Property, Definition, PatternProperty, DependentSchema, RequiredDependency, Rule | Trusted authored schema declarations; existing document/compiler/validator authority | | JSONBridges | BinaryInteger.jsonNumber, FixedWidthInteger/Double/Decimal.init(jsonNumber:), Double.jsonNumberFromRoundTripText | Optional one-delegate numeric conversions; explicit fidelity, no new authority | | JSONSchema | JSONSchemaDocument, JSONSchemaRegistry, JSONSchemaCompiler, CompiledJSONSchema, JSONSchemaValidator | Offline immutable plan, invocation-owned evaluation and private native regex | | JSONStreaming | JSONEventSequence, JSONValueSequence, NDJSONValueSequence, JSONTopLevelArraySequence, JSONAsyncWriter | Exclusive pull state, shared codec, strict framing and awaited handoffs | @@ -16,7 +18,7 @@ in the [README](../../README.md) are compiled and executed by the public consume | JSONHTML | JSONHTMLRenderer, JSONHTMLRenderingOptions, JSONHTMLLimits, JSONHTMLRenderReport, JSONHTMLAnnotation, JSONExportDocument, JSONExportOptions, JSONExportReport | Exact HTML, native Apple transfer, immutable prepared JSON/text/Markdown/static HTML/opt-in styled RTF and cumulative budgets | JSON imports no siblings. JSONSchema depends on JSON and package-private -JSONRegexRuntime. Streaming, Patch, HTML and Bridges depend on JSON. There are six public +JSONRegexRuntime. Streaming, Patch, HTML and Bridges depend on JSON. There are eight public products and no external root-package dependencies. Runtime code uses no HTTP, UI, MCP or documentation-tool dependencies. @@ -211,3 +213,25 @@ JSONHTMLRenderer also accepts JSONDocument in its four rendering/report methods. Caller annotations use exact typed paths, explicit missing-parent attachment and coherent value byte spans without schema execution or source-byte archival. See [diagnostic rendering](Release1.3/DiagnosticAnnotations.md). + +## Optional Dual DSL P1.2 + +The two DSL products do not change or re-export the existing core entry points. +JSONDSLCore has no standalone product; its public structural builder types occur +in the optional generic signatures. See the [decisions](DualDSL/Decisions.md). +Callbacks, content emission and staging are trusted and unmetered. Finalization +uses one finite invocation ledger through iterative construction and, for schemas, +document admission. This does not imply whole-pipeline cumulative billing, staging +retention, caller instruction or RSS bounds. No whole-DSL report is supplied. + +Exact duplicate names throw JSONFailure/duplicateKey. Decoded scalar identity and +order are preserved. Missing declarations emit nothing; null, empty containers and +Boolean schemas differ. Array flattening requires Elements(contentsOf:). Implicit +numbers accept BinaryInteger only, with explicit JSONNumber for decimals. E2 and +JSONBridges remain unchanged; DSL targets import neither bridge nor framework DSL. + +Schema rawKeyword preserves declarations for existing compiler classification. +No requiredness/default/format/reference/annotation policy is inferred. Invalid +instances return false; malformed schemas, offline missing resources, nonproductive +cycles, resource failures and cancellation remain operational errors. Factories +never validate or publish a candidate schema as already compiled. diff --git a/Documentation/Architecture/DualDSL/Decisions.md b/Documentation/Architecture/DualDSL/Decisions.md new file mode 100644 index 0000000..c99cd0c --- /dev/null +++ b/Documentation/Architecture/DualDSL/Decisions.md @@ -0,0 +1,87 @@ +# Dual DSL decisions + +## D1 — Trusted authored staging, finite finalization + +P1.2 adopts the explicitly permitted authored-configuration boundary. Result-builder +callbacks, wrappers, collection emission, IR assembly and integer literal construction +are trusted, unmetered staging. They can allocate or run caller computation before +finalization. This API is not an untrusted-input entry point, an instruction quota, +a staging-retention guarantee, an RSS bound or a whole-pipeline cumulative report. +Use existing bounded parsers/accumulators for dynamic untrusted input. + +Finalization is a separate checked phase. One invocation-owned JSONWorkMeter covers +iterative lowering, exact key/index admission, output slots, literal payload traversal, +work/cancellation and, for schemas, JSONSchemaDocument admission. Existing package +hooks are reused; JSON, JSONSchema and JSONBridges source need no changes. No helper +restarts this finalization ledger. A caught owned failure remains terminal, including +at the package continuation boundary. No partially finalized value is returned. +There is deliberately no public whole-DSL `WithReport` API. Tests distinguish the +finalization counters from unaccounted staging; existing 49 baseline probes remain. + +## D2 — Flat staging lifetime and iterative lowering + +The prototype's recursive indirect enum and per-container checked materialization +are replaced with a flat private token sequence. The public node's object/array/literal +factory spellings remain, but it is a struct rather than a pattern-matchable enum. +Nested typed declarations assemble these tokens without flattening JSON array values. +Finalization uses explicit frames and the engine's exact-key index and admission helpers. +IR teardown does not recursively own child IR nodes. Literal JSON retains its existing +engine lifetime semantics. Depth failures use JSONFailure/resource/maxDepth, replacing +the prototype-only JSONDSLBuildFailure; no second resource-error model is added. + +Qualification must cover default-cap rejection of deeply staged inputs, accepted depth +boundaries, cancellation in owned finalization and terminal errors. Arbitrary caller- +configured depths and callback recursion are not implied by a finite tested depth. + +## D3 — Separate products and semantic authorities + +JSONValueDSL and JSONSchemaDSL add no dependency to core or each other. Shared typed +builder protocols are public declarations in JSONDSLCore because optional public generic +signatures reference them; the helper module has no independently selectable product. +Objects use ordered exact-key semantics, arrays preserve nesting, omission emits nothing, +and duplicate keywords/properties/definitions fail. There is no String dictionary merge. + +The schema DSL emits ordinary JSONSchemaDocument. The existing compiler and validator +alone govern dialect, vocabularies, regex, references, annotations, requiredness and +invalidity versus operational errors. Explicit offline registry resources use existing +JSONSchemaDocument/JSONSchemaRegistry APIs; no ID-to-retrieval inference or network resolver. + +## D4 — E2 reconciliation and minimal naming + +Merged E2/JSONBridges remains unchanged and opt-in. DSL implicit numeric inputs remain +BinaryInteger only; exact decimals require explicit JSONNumber. No Float/Double overload, +Optional bridge, floating literal, coercion or automatic requiredness is added. +JSONValueDSL and JSONSchemaDSL remain distinct discovery namespaces. An umbrella product, +typed ObjectSchema/StringSchema/ArraySchema synonyms, automatic references/derivation and +new Boolean-schema overload aliases are deferred: existing Rule(Boolean), table Boolean +initializers and rawKeyword preserve the complete declaration without another surface. +The identical prototype rawSchemaKeyword synonym is removed in favor of rawKeyword. + +## D5 — Compiler strategy (pending measurement) + +The initial integration retains variadic buildBlock and conditional context conformances. +G4 records actual Swift 6.4 timings, diagnostics and memory observations before selecting +it over a pairwise buildPartialBlock alternative. Measurements under concurrent host load +are observations, not a quiet-host or broad compiler-throughput guarantee. + +## D6 — Empty enum is distinct from malformed schema + +The initial integration test incorrectly grouped an empty enum with malformed +length/count declarations. Actual compiler behavior and the pinned validation +meta-schema accept an enum array with no entries. Draft 2020-12 validation +[section 6.1.2](https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#name-enum) +requires an array and recommends a nonempty one. The corrected case compiles +both DSL and handwritten declarations and proves ordinary invalidity for all +six JSON kinds. Negative bounds, zero multipleOf, empty oneOf and duplicate +required names remain explicit compilation failures. No engine behavior changed. + +## D7 — Real module qualification instead of facade shadowing + +A native Swift 6.4 consumer with its own Field, Element, Schema and Property +declarations cannot spell JSONValueDSL.Field or JSONSchemaDSL.Schema when the +prototype facade enums shadow their modules. P1.2 therefore uses module-level +object/array/document/boolean factories. Existing authored calls retain the same +JSONValueDSL.object and JSONSchemaDSL.document spelling, while declaration types +can be qualified with their actual module. No umbrella, alias or synonym is added. +This is an unreleased prototype declaration-shape change, not an engine API change. +The compiler harness retains explicit collision cases in both configurations. diff --git a/Documentation/Architecture/DualDSL/Plan.md b/Documentation/Architecture/DualDSL/Plan.md new file mode 100644 index 0000000..0b2eb2a --- /dev/null +++ b/Documentation/Architecture/DualDSL/Plan.md @@ -0,0 +1,39 @@ +# Dual DSL P1.2 integration + +This is an unreleased integration on `codex/dual-dsl-p1.2`, based on current main +`e8c3e4bd9ae40d797f07a527ea98879acee3c057`. The supplied packet inspected +`9517233ef0a3c1b961b738df58fe3ee4081d780c`; the intervening E2 merge adds entry points +and JSONBridges. Existing six products and core implementation remain authoritative. +The requested JSONValueDSL and JSONSchemaDSL are additive opt-in products. JSONDSLCore +has no standalone product. Root dependencies, platforms and all 49 profiles remain unchanged. + +The maintainer-supplied packet SHA-256 is +`a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588`. +Its 32-file manifest verified. Only its three DSL source roots and six negative +fixtures are adapted. Neither signature shim is an engine input. Its sixteen +positive cases are ported from XCTest to Swift Testing and real JSONLimits. +Original and adaptation contributions retain Apache-2.0. + +## Gates and reviewable increments + +1. **G0 baseline:** pin source/SDK/compiler; record manifest and file collisions; + run production native baseline in an untouched detached worktree. +2. **G1/G3 integration:** add opt-in targets, iterative flat staging/lowering, + exact admission and shared finalization/document ledger. Compile the real consumer. +3. **G2 semantics:** port all sixteen cases, implement V-01–V-20 parity against + exact manual JSON and hand-written schemas; retain unchanged official suite policies. +4. **G4 compiler qualification:** exact-domain negative fixtures and measured + 16/64/128/256 blocks, nesting, choices, loops and mixed schema contexts; compare + pairwise composition when the observed scaling warrants it. +5. **G5 API/docs:** public comments and DocC, compiled README and guides; record + accepted/rejected namespace and sugar decisions against merged E2. +6. **G6 final qualification:** exact-count native/safety/docs and real Linux lanes; + public consumer, strict concurrency, source/diff/receipts and remaining limitations. + +Each coherent increment is committed locally. No push, PR, merge, tag or publication +is authorized by this integration request. Existing release and sibling worktrees +are preserved. A passing shim or typecheck is never a semantic qualification claim. + +Private input and raw receipts live in ignored `Artifacts/DualDSLP1.2`; source identity, +commands, exits, toolchains, suite counts and source hashes accompany actual outcomes. +[Progress](Progress.json) records incomplete gates without rewriting older release history. diff --git a/Documentation/Architecture/DualDSL/Progress.json b/Documentation/Architecture/DualDSL/Progress.json new file mode 100644 index 0000000..899b6d9 --- /dev/null +++ b/Documentation/Architecture/DualDSL/Progress.json @@ -0,0 +1,18 @@ +{ + "wave": "P1.2", + "base_commit": "e8c3e4bd9ae40d797f07a527ea98879acee3c057", + "packet_baseline": "9517233ef0a3c1b961b738df58fe3ee4081d780c", + "packet_sha256": "a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588", + "branch": "codex/dual-dsl-p1.2", + "release_ready": false, + "gates": { + "P1.2-G0": "in_progress", + "P1.2-G1": "in_progress", + "P1.2-G2": "in_progress", + "P1.2-G3": "in_progress", + "P1.2-G4": "pending", + "P1.2-G5": "pending", + "P1.2-G6": "pending" + }, + "scope": "Trusted authored staging; finite iterative shared-ledger finalization; no staging/RSS/caller-work claim" +} diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 735d569..f61232c 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -17,7 +17,7 @@ "blob_sha": "25cb8eab982ed0fc41e5f1dde5abeab18113f55e", "destination": "Sources/JSON/Value/JSON.swift", "disposition": "Six enum case concepts selectively adapted; equality/hash traversal and exact object/key storage rewritten.", - "attribution": "Original Thor Fuchs 2024\u20132026 header retained; Reference LICENSE and NOTICE preserved.", + "attribution": "Original Thor Fuchs 2024–2026 header retained; Reference LICENSE and NOTICE preserved.", "validation": [ "Tests/JSONTests/ValueTests.swift", "Tests/JSONTests/PresenceTests.swift" @@ -163,7 +163,7 @@ "blob_sha": null, "destination": "Sources/JSONRegexRuntime/RegexUnicodeTables.swift", "disposition": "407 immutable tables generated by original deterministic Python generator from fixed Unicode data; no generated Regress table copied. Regeneration compares bytes and provenance.", - "attribution": "\u00a9 2020 Unicode, Inc.; Unicode-3.0 copyright/permission notice retained in Licenses/Unicode-3.0.txt.", + "attribution": "© 2020 Unicode, Inc.; Unicode-3.0 copyright/permission notice retained in Licenses/Unicode-3.0.txt.", "validation": "Byte-equal regeneration and native exact-alias/GC/binary/Script/Script_Extensions/IdentifierName vectors", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" }, @@ -174,7 +174,7 @@ "blob_sha": null, "destination": "Sources/JSONSchema/Formats/JSONIDNAData.swift and Tests/Fixtures/Unicode/NormalizationTest-13.0.0.txt", "disposition": "Original generator derives RFC5892/5893, NFC and UTS46 nontransitional STD3 tables; normalization fixture copied byte-for-byte. No external engine or system Unicode authority.", - "attribution": "\u00a9 2019\u20132020 Unicode, Inc.; Unicode-3.0 notice preserved in Licenses/Unicode-3.0.txt.", + "attribution": "© 2019–2020 Unicode, Inc.; Unicode-3.0 notice preserved in Licenses/Unicode-3.0.txt.", "validation": "18853 normalization cases (94265 native comparisons); 874 independent optional format cases; byte-equal regeneration", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" }, @@ -188,6 +188,54 @@ "attribution": "BSD-3-Clause alternative; Licenses/json-schema-spec-BSD-AFL.txt", "validation": "Tests/JSONSchemaTests/EvaluationOutputTests.swift", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" + }, + { + "repository": "Maintainer-supplied SwiftJSON-DualDSL-Prototypes-P1 packet", + "commit": null, + "source_archive_sha256": "a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588", + "original_path": "Sources/JSONDSLCore/JSONContent.swift", + "original_sha256": "278865ff3843c3bc3f697f3ce56329c35418ae6598201aa743effa3b75c1fa4a", + "destination": "Sources/JSONDSLCore", + "disposition": "Typed authored DSL adapted against actual Swift 6.4 JSON/JSONSchema; flat iterative IR and shared finalization meter replace prototype recursive lowering. Neither shim copied.", + "attribution": "SPDX Apache-2.0 prototype notices retained; integration is original Apache-2.0 code.", + "validation": [ + "Tests/JSONDSLTests", + "Tests/CompileNegative/DualDSL", + "Examples/DualDSL" + ], + "evidence": "Scripts/ci.py: actual native, safety, docs and supported platform receipts; no shim qualification claim" + }, + { + "repository": "Maintainer-supplied SwiftJSON-DualDSL-Prototypes-P1 packet", + "commit": null, + "source_archive_sha256": "a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588", + "original_path": "Sources/JSONValueDSL/JSONValueDSL.swift", + "original_sha256": "7a846ed54455977a585fb53f5646795e5dbcae3a949a0e8be43f0b333f2a025e", + "destination": "Sources/JSONValueDSL", + "disposition": "Typed authored DSL adapted against actual Swift 6.4 JSON/JSONSchema; flat iterative IR and shared finalization meter replace prototype recursive lowering. Neither shim copied.", + "attribution": "SPDX Apache-2.0 prototype notices retained; integration is original Apache-2.0 code.", + "validation": [ + "Tests/JSONDSLTests", + "Tests/CompileNegative/DualDSL", + "Examples/DualDSL" + ], + "evidence": "Scripts/ci.py: actual native, safety, docs and supported platform receipts; no shim qualification claim" + }, + { + "repository": "Maintainer-supplied SwiftJSON-DualDSL-Prototypes-P1 packet", + "commit": null, + "source_archive_sha256": "a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588", + "original_path": "Sources/JSONSchemaDSL/JSONSchemaDSL.swift", + "original_sha256": "852ec7d7c9f6d5957fa3711c3e1b5ea3daeadf0ea67241ceb90ed1511bbd2f48", + "destination": "Sources/JSONSchemaDSL", + "disposition": "Typed authored DSL adapted against actual Swift 6.4 JSON/JSONSchema; flat iterative IR and shared finalization meter replace prototype recursive lowering. Neither shim copied.", + "attribution": "SPDX Apache-2.0 prototype notices retained; integration is original Apache-2.0 code.", + "validation": [ + "Tests/JSONDSLTests", + "Tests/CompileNegative/DualDSL", + "Examples/DualDSL" + ], + "evidence": "Scripts/ci.py: actual native, safety, docs and supported platform receipts; no shim qualification claim" } ], "original_implementation_notes": [ diff --git a/Documentation/Architecture/ResourceLimits.md b/Documentation/Architecture/ResourceLimits.md index 5a5c9c0..b497955 100644 --- a/Documentation/Architecture/ResourceLimits.md +++ b/Documentation/Architecture/ResourceLimits.md @@ -121,3 +121,14 @@ so retained reusable scratch is zero. Live owned capacities still charge retainedBytes. Scalar scanning and immutable plan-local regex sharing remain the qualified choices; no SIMD or prefilter optimization is claimed. Native performance observations are separate from the logical policy in [performance notes](../Performance/README.md). + +## Optional authored DSL boundary (P1.2) + +JSONValueDSL and JSONSchemaDSL explicitly accept trusted authored staging. Their +callbacks, content collection and flat IR assembly are outside the checked phase; +there is no staging-retention, caller-instruction, RSS or whole-DSL cumulative +claim. This is an authoring exception, not a new untrusted-input operation. +The iterative finalizer shares one existing ledger through schema-document +admission, without nested resets. Returned values satisfy their finite profile. +These additional operations do not change the 49 metrics, weights or defaults. +See [decisions and qualification](DualDSL/Decisions.md). diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 15091c0..b533eef 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -8,12 +8,12 @@ are recorded; macOS/SDK 27 and Swift 6.4 remain minimums. Pushes to main, pull requests and manual dispatch run three lanes: - Native: public inventory, manifest/dependency graph, release build, debug and - release Swift Testing, five-product example, rooted JSON/JSONSchema consumer, + release Swift Testing, public multi-product example, rooted JSON/JSONSchema consumer, public cap tests and pinned independent suites under an external timeout. - Safety: full Address Sanitizer tests, targeted shared-state Thread Sanitizer tests, and strict memory safety compilation with warnings as errors. - Documentation: public symbol graphs, complete authored API/parameter comments, - executed README examples, five native DocC catalogs with warnings as errors, + executed README examples, eight product DocC catalogs plus the shared DSL structure reference with warnings as errors, native static-hosting transforms and site route/asset checks. Actions are pinned by commit. Jobs have read-only repository permissions and @@ -109,3 +109,18 @@ regex 12, streaming 14, patch 11 and portable HTML 61. Its public consumer requi remain required. The first failed inherited expectations are retained; no blanket nonzero/exit-zero replacement is used. Real receiving-app and physical-device UI observations remain separate from automated SDK/API and portable execution. + +## Dual DSL development qualification + +`python3 Scripts/ci.py --lane dsl` runs all 47 actual-engine DSL Swift Testing +functions in Debug and Release, six intended-domain compiler rejections in both +configurations, qualified-name collision probes, native compiler scaling experiments +and the separate public consumer in both configurations. Native/Linux lanes also +require the DSL imports and consumer. Root discovery is 343 functions on Mac and +329 on Linux/WASI, in eight exact bundles; all 173 parameter invocations and 49 +baseline metric probes remain unchanged. Native shared-state TSan discovery is +13 functions (12 portable), including independent DSL finalization with shared plans. + +The P1.2 compiler experiment builds local variadic and pairwise builder modules +against the same real engine. Measurements retain host load and compiler RSS; +these do not imply runtime, staging-retention or caller-work guarantees. diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index b46024c..ffe2a07 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -1,7 +1,7 @@ # Building and maintaining the DocC catalogs Swift source comments are the API documentation authority. Native compiler symbol -graphs supply the declarations and links for six Swift DocC catalogs. Each catalog +graphs supply the declarations and links for eight public-product Swift DocC catalogs plus a shared DSL structure reference. Each catalog combines the product reference with curated Topics and a focused usage guide. See the [Swift DocC documentation](https://www.swift.org/documentation/docc/). @@ -19,7 +19,7 @@ python3 Scripts/ci.py --lane docs The lane records native preflight, extracts fresh public symbol graphs, requires summaries and named-parameter documentation on every authored public declaration, checks README Swift fences against their executable consumer source, runs that -consumer, and compiles all six DocC catalogs with warnings as errors. Zero public +consumer, and compiles all nine DocC catalogs with warnings as errors. Zero public symbols, missing comments, stale examples or a failed catalog fail the lane. | Output | Purpose | @@ -33,9 +33,9 @@ symbols, missing comments, stale examples or a failed catalog fail the lane. | `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | | `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | | `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | -| `.build/handoff/docc/compiled-catalogs.tar.gz` | All six compiled catalogs with their exact native filenames | +| `.build/handoff/docc/compiled-catalogs.tar.gz` | All nine compiled catalogs with their exact native filenames | | `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Independently verifiable hashes for every archived catalog file | -| `.build/handoff/pages/site/` | Static site, landing page and all six references under the project base path | +| `.build/handoff/pages/site/` | Static site, landing page and eight public-product references and the supporting DSL reference under the project base path | | `.build/handoff/pages/receipt.json` | Native transformations, input/output hashes and route/asset verification | Open a `.doccarchive` in Xcode for the compiled reference. To preview a source @@ -64,7 +64,7 @@ The manifest and receipt preserve file-level and archive-level digests. The docs lane uses native `docc process-archive transform-for-static-hosting` to prepare each qualified archive under `/Swift-JSON/`. A shared landing -page links all six references and guides. The build verifies product/guide routes, +page links eight public-product references and the supporting DSL reference and guides. The build verifies product/guide routes, local HTML links and assets, rejects private-path/credential markers and symlinks, and records a file manifest. Only the generated site is packaged for Pages; source graphs, command logs and private maintainer records are not website content. diff --git a/Documentation/Qualification/DualDSL/README.md b/Documentation/Qualification/DualDSL/README.md new file mode 100644 index 0000000..22f78af --- /dev/null +++ b/Documentation/Qualification/DualDSL/README.md @@ -0,0 +1,24 @@ +# Dual DSL P1.2 qualification + +This development wave qualifies actual engine behavior; the input's Linux 6.2.1 shims +are neither production source nor conformance evidence. See the +[plan](../../Architecture/DualDSL/Plan.md), [decisions](../../Architecture/DualDSL/Decisions.md) +and [progress](../../Architecture/DualDSL/Progress.json). + +| Contract | Regression evidence | +| --- | --- | +| Exact number/scalar authority and no silent merge | JSONDSLTests dslV01/V02/V04/V05; actual core constructors/comparisons | +| Omission, null, empty and Boolean schemas | dslV06/V07/V08And09; public compiler/evaluator | +| No inferred schema semantics | dslV10–V17, manual raw/annotation/verdict parity | +| Whole-root finalization ceilings and terminal failure | ResourceLifecycleTests; schema admission shares the same finalizer | +| Entry/inner cancellation and no successful publication | deterministic dslV19 tests with no sleeps | +| Iterative staging lifetime and depth boundary | 5000 staged levels rejected at finite depth; accepted depth 128 | +| Immutable independent finalization/plan invocations | dslSharedFinalizationAndPlansAreInvocationOwned | +| Compile-time domain exclusions and scaling | six original negative fixtures plus the G4 native harness | +| Public module direction, docs and consumer | production manifest checks, native DocC and Examples/DualDSL | + +V-03 is a compiler-negative assertion, not a runtime test. V-18/V-19 are actual +resource/cancellation tests. V-20 is repeated exact raw, verdict and annotation parity +across manual and DSL paths. Official JSON/schema suites and their policy exclusions +remain unchanged. Test counts, toolchains, source pins and receipts are reported after +execution, not inferred from this mapping. No gate is closed by this document alone. diff --git a/Documentation/README.md b/Documentation/README.md index 44892ae..d603e2f 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -51,3 +51,11 @@ Original code is Apache-2.0. See the [license](../LICENSE), [notice](../NOTICE), [adaptation manifest](Architecture/ExtractionManifest.json) and [pinned inputs](Architecture/UpstreamPins.json). Qualification inputs are separate from the empty library dependency graph. + +## Optional authored DSLs (P1.2 development) + +- [Integration plan and source boundary](Architecture/DualDSL/Plan.md) +- [Resource, naming and compiler decisions](Architecture/DualDSL/Decisions.md) +- [Real-engine acceptance mapping](Qualification/DualDSL/README.md) +- [Typed value guide](../Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md) +- [Typed schema guide](../Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md) diff --git a/Examples/Consumer/Package.swift b/Examples/Consumer/Package.swift index 29ce69a..a8c55af 100644 --- a/Examples/Consumer/Package.swift +++ b/Examples/Consumer/Package.swift @@ -13,7 +13,9 @@ let package = Package( .product(name: "JSONStreaming", package: "Swift-JSON"), .product(name: "JSONPatch", package: "Swift-JSON"), .product(name: "JSONHTML", package: "Swift-JSON"), - .product(name: "JSONBridges", package: "Swift-JSON") + .product(name: "JSONBridges", package: "Swift-JSON"), + .product(name: "JSONValueDSL", package: "Swift-JSON"), + .product(name: "JSONSchemaDSL", package: "Swift-JSON") ])], swiftLanguageModes: [.v6] ) diff --git a/Examples/Consumer/Sources/Consumer/READMEExamples.swift b/Examples/Consumer/Sources/Consumer/READMEExamples.swift index 124f41e..c9007d1 100644 --- a/Examples/Consumer/Sources/Consumer/READMEExamples.swift +++ b/Examples/Consumer/Sources/Consumer/READMEExamples.swift @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // Compiled README examples; checked by the documentation lane. import JSONBridges +import JSONValueDSL +import JSONSchemaDSL import JSON import Foundation import JSONSchema @@ -41,6 +43,22 @@ func runREADMEExamples() throws { let bridgeRounded = try Double(jsonNumber: bridgeTenth, policy: .rounded) precondition(bridgeRounded == 0.1) + let authoredTenth = try JSONNumber(parsing: "0.1") + let authoredJSON = try JSONValueDSL.object { + Field("amount", exactNumber: authoredTenth) + Field.array("nested") { Element.array { Element(1); Element(2) } } + } + let authoredSchema = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("amount") { Schema.type(.number); Schema.minimum(authoredTenth) } + } + Schema.required("amount") + } + let authoredPlan = try JSONSchemaCompiler().compile(authoredSchema) + let authoredResult = try JSONSchemaValidator().validate(authoredJSON, against: authoredPlan) + precondition(authoredResult.isValid) + let parser = JSONParser() let value = try parser.parse(#"{"count":9007199254740993,"note":null}"#) let count = try JSONPointer(parsing: "/count").resolve(in: value) diff --git a/Examples/DualDSL/Package.swift b/Examples/DualDSL/Package.swift new file mode 100644 index 0000000..24ca6df --- /dev/null +++ b/Examples/DualDSL/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package( + name: "DualDSLConsumer", + platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "DualDSLConsumer", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), + .product(name: "JSONSchema", package: "Swift-JSON"), + .product(name: "JSONValueDSL", package: "Swift-JSON"), + .product(name: "JSONSchemaDSL", package: "Swift-JSON") + ])], + swiftLanguageModes: [.v6] +) diff --git a/Examples/DualDSL/README.md b/Examples/DualDSL/README.md new file mode 100644 index 0000000..f2f9704 --- /dev/null +++ b/Examples/DualDSL/README.md @@ -0,0 +1,11 @@ +# Actual-engine Dual DSL consumer + +Run `swift run --package-path Examples/DualDSL DualDSLConsumer` on the supported +Swift 6.4 toolchain. This separate package imports only public products, not +JSONDSLCore, private meters or either signature shim. It checks exact integers, +explicit decimals, scalar-distinct keys, nested arrays, an explicitly selected +local definition, positive/negative verdicts and missing-resource operational failure. + +The builder is for trusted authored configuration. Staging and caller computation +are not budgeted; finalization and schema-document admission use finite limits. +This consumer is not a whole-pipeline resource or downstream protocol claim. diff --git a/Examples/DualDSL/Sources/DualDSLConsumer/main.swift b/Examples/DualDSL/Sources/DualDSLConsumer/main.swift new file mode 100644 index 0000000..3ebd16c --- /dev/null +++ b/Examples/DualDSL/Sources/DualDSLConsumer/main.swift @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONSchema +import JSONSchemaDSL +import JSONValueDSL + +let tenth = try JSONNumber(parsing: "0.1") +let instance = try JSONValueDSL.object { + Field("id", UInt64(9_007_199_254_740_993)) + Field("amount", exactNumber: tenth) + Field("é", 1) + Field("e\u{301}", 2) + Field.array("nested") { + Element.array { + Element(1) + Element(2) + } + } +} +let schema = try JSONSchemaDSL.document { + Schema.schemaURI("https://json-schema.org/draft/2020-12/schema") + Schema.type(.object) + Schema.properties { + Property("id") { Schema.type(.integer) } + Property("amount") { Schema.reference("#/$defs/Amount") } + } + Schema.required("id", "amount") + Schema.definitions { + Definition("Amount") { + Schema.type(.number) + Schema.minimum(tenth) + Schema.multipleOf(tenth) + } + } +} +let plan = try JSONSchemaCompiler().compile(schema) +let validator = JSONSchemaValidator() +let validResult = try validator.validate(instance, against: plan) +precondition(validResult.isValid) +let elevenHundredths = try JSONNumber(parsing: "0.11") +let invalid = try JSONValueDSL.object { + Field("id", 1) + Field("amount", exactNumber: elevenHundredths) +} +let invalidResult = try validator.validate(invalid, against: plan) +precondition(!invalidResult.isValid) +let absent = try JSONValueDSL.object { Field("id", 1) } +let absentResult = try validator.validate(absent, against: plan) +precondition(!absentResult.isValid) +precondition(instance.objectValue?["é"] == 1 && instance.objectValue?["e\u{301}"] == 2) +precondition(instance.objectValue?["nested"]?.arrayValue?.count == 1) +let missing = try JSONSchemaDSL.document { Schema.reference("https://not-supplied.invalid/schema") } +var observedOperational = false +do { _ = try JSONSchemaCompiler().compile(missing) } catch let error as JSONSchemaCompilationFailure { + precondition(error.category == .missingResource) + observedOperational = true +} +precondition(observedOperational) +let bytes = try JSONWriter().write(instance) +let restored = try JSONParser().parse(bytes) +precondition(restored == instance) +print("DUAL_DSL_CONSUMER passed valid=1 invalid=2 operational=1 exact_keys=2 nested_arrays=1") diff --git a/Package.swift b/Package.swift index e8a0209..8aa14d1 100644 --- a/Package.swift +++ b/Package.swift @@ -11,10 +11,16 @@ let package = Package( .library(name: "JSONStreaming", targets: ["JSONStreaming"]), .library(name: "JSONPatch", targets: ["JSONPatch"]), .library(name: "JSONHTML", targets: ["JSONHTML"]), - .library(name: "JSONBridges", targets: ["JSONBridges"]) + .library(name: "JSONBridges", targets: ["JSONBridges"]), + .library(name: "JSONValueDSL", targets: ["JSONValueDSL"]), + .library(name: "JSONSchemaDSL", targets: ["JSONSchemaDSL"]) ], targets: [ .target(name: "JSON"), + .target(name: "JSONDSLCore", dependencies: ["JSON"]), + .target(name: "JSONValueDSL", dependencies: ["JSON", "JSONDSLCore"]), + .target(name: "JSONSchemaDSL", dependencies: ["JSON", "JSONSchema", "JSONDSLCore"]), + .testTarget(name: "JSONDSLTests", dependencies: ["JSON", "JSONSchema", "JSONDSLCore", "JSONValueDSL", "JSONSchemaDSL"]), .target(name: "JSONBridges", dependencies: ["JSON"]), .testTarget(name: "JSONBridgesTests", dependencies: ["JSONBridges", "JSON"]), .target(name: "JSONRegexRuntime", dependencies: ["JSON"]), diff --git a/README.md b/README.md index 5c37627..a370dee 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Choose the products your application needs: | **JSONSchema** | Draft 2020-12 compilation/evaluation, offline registries, bounded regex, annotations and raw declaration inspection | | **JSONStreaming** | Pull syntax events and values, strict NDJSON, root-array elements and awaited output chunks | | **JSONHTML** | Exact HTML, offline exploration and immutable native transfer/export (1.3 development) | +| **JSONValueDSL** | Opt-in typed authored objects/arrays with exact values and finite finalization (P1.2 development) | +| **JSONSchemaDSL** | Opt-in typed authored Draft 2020-12 declarations using the existing engine (P1.2 development) | | **JSONBridges** | Opt-in native integer, Double and Foundation Decimal conversions (E2 development) | | **JSONPatch** | Staged RFC 6902 JSON Patch and RFC 7396 Merge Patch | @@ -100,7 +102,7 @@ Then add products to your target dependencies: Add `JSONStreaming`, `JSONPatch` and the 1.3 development product `JSONHTML` in the same way when needed. A local checkout can use `.package(path: "../Swift-JSON")`. The requirement above accepts compatible 1.x -releases. See the [six-product consumer](Examples/Consumer) for a complete buildable +releases. See the [eight-product consumer](Examples/Consumer) for a complete buildable manifest. The website tracks `main`; release assets include compiled DocC catalogues for the exact released source. @@ -137,6 +139,47 @@ text is distinct from its exact binary rational. Read the [numeric fidelity guide](Sources/JSONBridges/Documentation.docc/NumericFidelity.md) and [before/after crosswalk](Documentation/Ergonomics/README.md). +## Typed authored DSLs (P1.2 development) + +Select the optional `JSONValueDSL` and `JSONSchemaDSL` products explicitly. They add +syntax over the same exact model and schema engine; neither is imported by core. +These APIs are development additions on main's feature line, not additions to older tags. + +```swift +import JSONValueDSL +import JSONSchemaDSL + +let authoredTenth = try JSONNumber(parsing: "0.1") +let authoredJSON = try JSONValueDSL.object { + Field("amount", exactNumber: authoredTenth) + Field.array("nested") { Element.array { Element(1); Element(2) } } +} +let authoredSchema = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("amount") { Schema.type(.number); Schema.minimum(authoredTenth) } + } + Schema.required("amount") +} +let authoredPlan = try JSONSchemaCompiler().compile(authoredSchema) +let authoredResult = try JSONSchemaValidator().validate(authoredJSON, against: authoredPlan) +precondition(authoredResult.isValid) +``` + +This syntax is for **trusted authored configuration**. Callbacks, wrapper collection +and staging are unmetered. Only iterative finalization and schema-document admission +share a finite ledger; there is no staging/RSS bound or whole-DSL cumulative report. +Use checked accumulators or the parser for dynamic untrusted input. Exact duplicates +throw, scalar-distinct keys remain distinct, omitted branches emit nothing, and arrays +expand only through explicit `Elements(contentsOf:)`. There is no implicit Float/Double +bridge, requiredness inference, default application or custom schema validator. + +See the [value guide](Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md), +[schema guide](Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md), +[public-only consumer](Examples/DualDSL) and [qualification scope](Documentation/Qualification/DualDSL). +The shared JSONDSLCore module has no standalone product; its typed composition contracts +support the two optional APIs. Typed schema synonyms and an umbrella import remain deferred. + ## Parse, inspect and write exact values ```swift diff --git a/Scripts/build_documentation.py b/Scripts/build_documentation.py index 779a5a3..f0ab717 100644 --- a/Scripts/build_documentation.py +++ b/Scripts/build_documentation.py @@ -28,7 +28,7 @@ def run(command, log, timeout): def package_catalogs(output): """Preserve native DocC filenames inside an artifact with a portable outer name.""" records = [] - for product in documentation_catalog.PRODUCTS: + for product in documentation_catalog.CATALOGS: catalog = output / (product + '.doccarchive') paths = sorted(catalog.rglob('*')) if not any(p.is_file() for p in paths): @@ -60,7 +60,7 @@ def package_catalogs(output): 'bytes': archive.stat().st_size, 'verified_files': len(records), 'manifest': str(manifest.relative_to(ROOT)), 'manifest_sha256': hashlib.sha256(manifest.read_bytes()).hexdigest(), - 'policy': 'Six native DocC archives; exact internal filenames, sorted entries and normalized tar/gzip metadata.'} + 'policy': 'Eight product DocC archives plus the shared DSL structure reference; exact internal filenames, sorted entries and normalized tar/gzip metadata.'} def main(): @@ -89,7 +89,7 @@ def main(): receipt['readme_examples'] = dict(run(command, output / 'readme-examples.log', 900), executable_fences=example_count) if receipt['readme_examples']['exit_code']: raise ValueError('Compiled README examples failed') - for product in documentation_catalog.PRODUCTS: + for product in documentation_catalog.CATALOGS: graphs = output / 'symbols' / product if graphs.exists(): shutil.rmtree(graphs) diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index fa1c658..c55d5a1 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -13,7 +13,7 @@ import sys import urllib.parse -from documentation_catalog import PRODUCTS +from documentation_catalog import PRODUCTS, CATALOGS from preflight import collect ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -25,14 +25,18 @@ 'JSONStreaming': ('Completion and backpressure', 'completionandbackpressure'), 'JSONPatch': ('Choosing and applying patches', 'applyingpatches'), 'JSONHTML': ('Exact interactive HTML', 'interactiverendering'), - 'JSONBridges': ('Opt-in native conversions', 'numericfidelity')} + 'JSONBridges': ('Opt-in native conversions', 'numericfidelity'), + 'JSONValueDSL': ('Trusted exact value authoring', 'authoredvalues'), + 'JSONSchemaDSL': ('Authored schemas, engine validation', 'authoredschemas')} DESCRIPTIONS = { 'JSON': 'Exact values, strict UTF-8, native Codable, Pointer, presence and finite resource reports.', 'JSONSchema': 'Draft 2020-12, explicit offline registries, immutable plans, annotations and bounded regex.', 'JSONStreaming': 'Pull events and values, strict NDJSON, root-array completion and awaited output chunks.', 'JSONPatch': 'Transactional RFC 6902 JSON Patch and RFC 7396 Merge Patch with exact value semantics.', 'JSONHTML': 'Exact standalone HTML with offline disclosure/search and native-byte copy/download.', - 'JSONBridges': 'Opt-in trusted integer construction and policy-explicit Double and Foundation Decimal projection.'} + 'JSONBridges': 'Opt-in trusted integer construction and policy-explicit Double and Foundation Decimal projection.', + 'JSONValueDSL': 'Typed authored objects and arrays with exact values, omission and finite finalization.', + 'JSONSchemaDSL': 'Typed authored Draft 2020-12 declarations, compiled and evaluated by the existing engine.'} class Links(HTMLParser): @@ -99,7 +103,7 @@ def landing(commit):
Public Swift documentation

Exact JSON. Clear contracts.

-

Keep decimal values exact, Unicode scalar keys distinct and resource use explicit. Explore five Swift libraries that carry those semantics from input bytes to validated, transformed and encoded output.

+

Keep decimal values exact, Unicode scalar keys distinct and resource use explicit. Explore eight Swift libraries that carry those semantics from input bytes to validated, transformed and encoded output.

Swift 6.4+ · Swift language mode 6 · macOS/SDK 27+ · No package dependencies

{''.join(cards)}

Start with the contract

@@ -171,7 +175,7 @@ def main(): if site.exists(): shutil.rmtree(site) site.mkdir() - for product in PRODUCTS: + for product in CATALOGS: command = ['xcrun', 'docc', 'process-archive', 'transform-for-static-hosting', str(catalogs / (product + '.doccarchive')), '--output-path', str(site / product), '--hosting-base-path', BASE_PATH + '/' + product] diff --git a/Scripts/ci.py b/Scripts/ci.py index d1e1d03..ac3fe7a 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -7,7 +7,7 @@ from testing_events import read_events, ROOT_PARAMETER_CASES ROOT = pathlib.Path(__file__).resolve().parents[1] -SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes|sha256InputCapacityAndConcurrentInvocationsRemainIndependent|sharedFormattedHTMLRendererHasIndependentConcurrentInvocations|sharedAnnotatedDocumentRendererHasIndependentConcurrentInvocations|sharedExportPreparationsHaveIndependentConcurrentState|applePDFSimultaneousJobsHaveIndependentStateAndCancellation|e2SharedBridgeInputsHaveInvocationOwnedState|e2SharedCompiledPlanFacadeHasIndependentInvocationState' +SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes|sha256InputCapacityAndConcurrentInvocationsRemainIndependent|sharedFormattedHTMLRendererHasIndependentConcurrentInvocations|sharedAnnotatedDocumentRendererHasIndependentConcurrentInvocations|sharedExportPreparationsHaveIndependentConcurrentState|applePDFSimultaneousJobsHaveIndependentStateAndCancellation|e2SharedBridgeInputsHaveInvocationOwnedState|e2SharedCompiledPlanFacadeHasIndependentInvocationState|dslSharedFinalizationAndPlansAreInvocationOwned' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -25,7 +25,7 @@ def commands(lane, output): # Root behavior/corpora remain required. SwiftData and Apple SDK/lifetime # probes are separate Apple acceptance; they never become Linux passes. return [c for c in commands('native', ROOT / output) - if not any(x in c for x in ['Tools/MobileQualification', '--mode'])] + [ + if not any(x in c for x in ['Tools/MobileQualification', 'Scripts/qualify_release_1_1.py'])] + [ ['python3', '-B', 'Scripts/qualify_linux_reproduction.py', '--output', str(output / 'reproduction')]] if lane == 'linux-safety': # Every behavior case uses Swift Testing. Do not start the generated @@ -53,6 +53,14 @@ def commands(lane, output): ['swift', 'run', '-c', 'release', '--package-path', 'Tools/JSONHTMLBrowser', 'BrowserFixtures', str(output / 'fixtures')], ['python3', '-B', 'Scripts/qualify_html_browser.py', '--fixtures', str(output / 'fixtures'), '--output', str(output / 'browser')], ] + if lane == 'dsl': + return [ + ['swift', 'test', '--filter', 'JSONDSLTests'], + ['swift', 'test', '-c', 'release', '--filter', 'JSONDSLTests'], + ['python3', '-B', 'Scripts/qualify_dual_dsl.py', '--mode', 'compiler', '--output', str(output / 'compiler')], + ['swift', 'run', '--package-path', 'Examples/DualDSL', 'DualDSLConsumer'], + ['swift', 'run', '-c', 'release', '--package-path', 'Examples/DualDSL', 'DualDSLConsumer'], + ] if lane == 'native': return [ ['python3', '-B', 'Scripts/public_check.py'], @@ -65,6 +73,8 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'reader', '--output', str(output / 'reader.json')], ['swift', 'run', '-c', 'release', '--package-path', 'Examples/Consumer', 'Consumer'], ['python3', '-B', 'Scripts/qualify_ergonomics.py'], + ['python3', '-B', 'Scripts/qualify_dual_dsl.py', '--mode', 'imports', '--output', str(output / 'dsl-imports')], + ['swift', 'run', '-c', 'release', '--package-path', 'Examples/DualDSL', 'DualDSLConsumer'], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -113,8 +123,10 @@ def check_result(command, code, log, events=None): if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 2, 3 if sys.platform == 'linux' else 4, 4] if 'thread' in command else sorted([10, 11, 12, 14, 61 if sys.platform == 'linux' else 75, 55, 119]) + expected = [47] if 'JSONDSLTests' in command else [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 2, 3 if sys.platform == 'linux' else 4, 4] if 'thread' in command else sorted([10, 11, 12, 14, 47, 61 if sys.platform == 'linux' else 75, 55, 119]) passed = passed and sorted(counts) == expected + if events is not None and '--filter' not in command and not any(x.startswith('Tools/') for x in command): + passed = passed and events['module_function_counts'] == {'JSONTests': 119, 'JSONBridgesTests': 10, 'JSONSchemaTests': 55, 'JSONRegexRuntimeTests': 12, 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 61 if sys.platform == 'linux' else 75, 'JSONDSLTests': 47} if '--filter' not in command and not any(x.startswith('Tools/') for x in command): passed = passed and (events['parameterized_functions'] == ROOT_PARAMETER_CASES if events is not None else sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log)) == 173) if 'Tools/Qualification' in command: @@ -124,10 +136,13 @@ def check_result(command, code, log, events=None): passed = passed and len(probes) == 49 and dict(probes) == expected_probes if command == ['swift', 'package', 'dump-package'] and passed: d = json.loads(log) - passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming'] 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']} == {name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges']} + passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONSchemaDSL', 'JSONStreaming', 'JSONValueDSL'] 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']} == {name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONValueDSL', 'JSONSchemaDSL']} targets = {t['name']: t for t in d['targets']} - passed = passed and set(targets) == {'JSON', 'JSONBridges', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONBridgesTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONBridges']['dependencies'] == [{'byName': ['JSON', None]}] + passed = passed and set(targets) == {'JSON', 'JSONBridges', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONDSLCore', 'JSONValueDSL', 'JSONSchemaDSL', 'JSONDSLTests', 'JSONTests', 'JSONBridgesTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONBridges']['dependencies'] == [{'byName': ['JSON', None]}] + passed = passed and not targets['JSON']['dependencies'] + for name, dependencies in [('JSONDSLCore', ['JSON']), ('JSONValueDSL', ['JSON', 'JSONDSLCore']), ('JSONSchemaDSL', ['JSON', 'JSONSchema', 'JSONDSLCore'])]: + passed = passed and targets[name]['dependencies'] == [{'byName': [name, None]} for name in dependencies] if command == ['swift', 'package', 'show-dependencies', '--format', 'json'] and passed: passed = json.loads(log)['dependencies'] == [] if command[:2] == ['swift', 'run'] and 'Tools/MobileQualification' in command: @@ -136,6 +151,11 @@ 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_dual_dsl.py']: + mode = command[command.index('--mode') + 1] + passed = passed and ('DUAL_DSL_COMPILER passed negatives=12 configurations=2 mode=' + mode) in log.splitlines() + if command[:2] == ['swift', 'run'] and 'Examples/DualDSL' in command: + passed = passed and 'DUAL_DSL_CONSUMER passed valid=1 invalid=2 operational=1 exact_keys=2 nested_arrays=1' 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: @@ -146,7 +166,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'], required=True) + parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'dsl'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) @@ -212,7 +232,7 @@ def save(): if receipt['status'] == 'passed' and args.lane == 'wasm': try: wasi = json.loads((output / 'probes/wasi.json').read_text()) - if wasi['status'] != 'passed' or wasi['root_counts'] != {'JSONTests':119,'JSONBridgesTests':10,'JSONSchemaTests':55,'JSONRegexRuntimeTests':12,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONHTMLTests':61}: + if wasi['status'] != 'passed' or wasi['root_counts'] != {'JSONTests':119,'JSONBridgesTests':10,'JSONSchemaTests':55,'JSONRegexRuntimeTests':12,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONHTMLTests':61,'JSONDSLTests':47}: raise ValueError('Missing WASI qualification boundary') receipt['materialized_wasi'] = {'path':'probes/wasi.json','sha256':hashlib.sha256((output / 'probes/wasi.json').read_bytes()).hexdigest()} except (OSError,KeyError,ValueError,TypeError) as error: diff --git a/Scripts/documentation_catalog.py b/Scripts/documentation_catalog.py index add1f6e..55680e8 100644 --- a/Scripts/documentation_catalog.py +++ b/Scripts/documentation_catalog.py @@ -7,7 +7,9 @@ import urllib.parse ROOT = pathlib.Path(__file__).resolve().parents[1] -PRODUCTS = ('JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges') +PRODUCTS = ('JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONValueDSL', 'JSONSchemaDSL') +SUPPORT_MODULES = ('JSONDSLCore',) +CATALOGS = PRODUCTS + SUPPORT_MODULES def public_symbols(graph_dir): @@ -16,7 +18,7 @@ def public_symbols(graph_dir): for graph in sorted(graph_dir.glob('*.json')): data = json.loads(graph.read_text()) module = data['module']['name'] - if module not in PRODUCTS: + if module not in CATALOGS: continue for symbol in data['symbols']: location = symbol.get('location') @@ -33,7 +35,7 @@ def public_symbols(graph_dir): raise ValueError('Non-public symbol in public graph') identifier = symbol['identifier']['precise'] symbols[identifier] = dict(symbol, module=module, source=relative.as_posix()) - for module in PRODUCTS: + for module in CATALOGS: if not any(s['module'] == module for s in symbols.values()): raise ValueError('No authored symbols for ' + module) return sorted(symbols.values(), key=lambda s: (s['module'], s['pathComponents'], s['identifier']['precise'])) @@ -56,7 +58,7 @@ def verify_comments(symbols): failures.append(label + ': missing parameter ' + name) return {'authored_public_symbols': len(symbols), 'documented_public_symbols': sum(bool(comment_text(s).strip()) for s in symbols), - 'products': {m: sum(s['module'] == m for s in symbols) for m in PRODUCTS}, + 'products': {m: sum(s['module'] == m for s in symbols) for m in CATALOGS}, 'policy': 'All authored public declarations and named parameters; compiler-synthesized declarations and extension containers excluded.', 'errors': failures} diff --git a/Scripts/qualify_dual_dsl.py b/Scripts/qualify_dual_dsl.py new file mode 100644 index 0000000..be13456 --- /dev/null +++ b/Scripts/qualify_dual_dsl.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Qualify actual-engine DSL domain typing and reproducible compiler scaling experiments.""" +import argparse, datetime, hashlib, json, os, pathlib, platform, re, subprocess, sys, time + +ROOT = pathlib.Path(__file__).resolve().parents[1] +NEGATIVES = { + 'DefinitionInsideProperties.swift': ('Definition', 'JSONSchemaPropertiesContent'), + 'ElementInsideObject.swift': ('Element', 'JSONValueFieldsContent'), + 'FieldInsideSchema.swift': ('Field', 'JSONSchemaClausesContent'), + 'ImplicitFloatingPoint.swift': ('initializer', 'no exact matches'), + 'PropertyInsideDefinitions.swift': ('Property', 'JSONSchemaDefinitionsContent'), + 'SchemaClauseInsideArray.swift': ('SchemaClause', 'JSONValueElementsContent'), +} +POSITIVE = '''import JSONValueDSL +import JSONSchemaDSL +func authored(_ include: Bool, _ names: [String]) throws { + _ = try JSONValueDSL.object { + Field("id", UInt64.max) + if include { Field("flag", true) } else { Field("other", "value") } + Field.array("a") { for name in names { Element(name) }; Element.array { Element(1) } } + } + _ = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { for name in names { Property(name) { Schema.type(.string) } } } + Schema.definitions { Definition("n") { Schema.type(.integer) } } + Schema.anyOf { Rule(false); Rule { Schema.type(.object) } } + } +} +''' +QUALIFIED = '''import JSONValueDSL +import JSONSchemaDSL +enum Field {}; enum Element {}; enum Schema {}; enum Property {}; enum Rule {}; enum Definition {} +func qualifiedDeclarations() throws { + _ = try JSONValueDSL.object { + JSONValueDSL.Field("exact", UInt64.max) + JSONValueDSL.Field.array("items") { JSONValueDSL.Element(1) } + } + _ = try JSONSchemaDSL.document { + JSONSchemaDSL.Schema.type(.object) + JSONSchemaDSL.Schema.properties { + JSONSchemaDSL.Property("exact") { JSONSchemaDSL.Schema.type(.integer) } + } + JSONSchemaDSL.Schema.definitions { + JSONSchemaDSL.Definition("accept") { JSONSchemaDSL.Schema.anyOf { JSONSchemaDSL.Rule(true) } } + } + } +} +''' + + +def fixtures(): + result = {} + for width in (16, 64, 128, 256): + fields = '\n'.join('Field("f%d", %d)' % (n, n) for n in range(width)) + properties = '\n'.join('Property("f%d") { Schema.type(.string) }' % n for n in range(width)) + result['value-%d' % width] = 'import JSONValueDSL\nfunc fixture() throws -> Int { let value = try JSONValueDSL.object {\n' + fields + '\n}; return value.objectValue!.count }\n' + result['schema-%d' % width] = 'import JSONSchemaDSL\nfunc fixture() throws -> Int { let schema = try JSONSchemaDSL.document { Schema.properties {\n' + properties + '\n} }; return schema.raw.objectValue!["properties"]!.objectValue!.count }\n' + inner = 'Element(1)' + for _ in range(24): inner = 'Element.array { ' + inner + ' }' + result['nested-24'] = 'import JSONValueDSL\nfunc fixture() throws { _ = try JSONValueDSL.array { ' + inner + ' } }\n' + result['mixed-choices-loops'] = POSITIVE + result['inference'] = '''import JSONValueDSL +import JSONSchemaDSL +func fixture(_ include: Bool) throws { + let fields = ["a", "b", "c"] + _ = try JSONValueDSL.object { for (i, name) in fields.enumerated() { Field(name, UInt64(i)) }; if include { Field.object("nested") { Field("ok", true) } } } + _ = try JSONSchemaDSL.document { Schema.properties { for name in fields { Property(name) { if include { Schema.type(.string) } else { Schema.type(.null) } } } }; Schema.prefixItems { Rule { Schema.anyOf { Rule(true); Rule(false) } } } } +} +''' + return result + + +def partial_source(source): + start = source.index('public static func buildBlock') + opening = source.index('{', start); depth = 1; end = opening + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}'); end += 1 + replacement = '''public static func buildBlock() -> JSONContentEmpty { .init() } + public static func buildPartialBlock(first: First) -> First { first } + public static func buildPartialBlock(accumulated: Accumulated, next: Next) -> JSONContentTuple { .init(accumulated, next) }''' + domains = [('JSONValueFieldsContent', 'writeValueFields', 'JSONDSLMember'), ('JSONValueElementsContent', 'writeValueElements', 'JSONDSLNode'), ('JSONSchemaClausesContent', 'writeSchemaClauses', 'JSONDSLMember'), ('JSONSchemaPropertiesContent', 'writeSchemaProperties', 'JSONDSLMember'), ('JSONSchemaNodesContent', 'writeSchemaNodes', 'JSONDSLNode'), ('JSONSchemaDependenciesContent', 'writeSchemaDependencies', 'JSONDSLMember'), ('JSONSchemaPatternsContent', 'writeSchemaPatterns', 'JSONDSLMember'), ('JSONSchemaDefinitionsContent', 'writeSchemaDefinitions', 'JSONDSLMember'), ('JSONSchemaDependentSchemasContent', 'writeDependentSchemas', 'JSONDSLMember')] + empty = '\npublic struct JSONContentEmpty: ' + ', '.join(x[0] for x in domains) + ' { public init() {}\n' + empty += '\n'.join('public func '+method+'(to value: inout ['+kind+']) {}' for _,method,kind in domains) + '\n}\n' + return source[:start] + replacement + source[end:] + empty + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--mode', choices=['imports', 'compiler'], default='imports') + parser.add_argument('--output', default='.build/ci/dual-dsl') + args = parser.parse_args(); output = ROOT / args.output; output.mkdir(parents=True, exist_ok=True) + sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest() + record = dict(schema_version=1, status='running', mode=args.mode, source_commit=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip(), commands=[], measurements=[], source_inputs=[], started_utc=datetime.datetime.now(datetime.timezone.utc).isoformat()) + for directory in ['Sources/JSONDSLCore','Sources/JSONValueDSL','Sources/JSONSchemaDSL','Tests/CompileNegative/DualDSL']: + record['source_inputs'] += [dict(path=p.relative_to(ROOT).as_posix(),sha256=sha(p)) for p in sorted((ROOT/directory).rglob('*.swift'))] + consumer = (ROOT/'Examples/DualDSL/Sources/DualDSLConsumer/main.swift').read_text() + assert 'JSONDSLCore' not in consumer and 'JSONWorkMeter' not in consumer + guide_fences = 0 + for name in ['Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md', 'Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md']: + blocks = re.findall(r'```swift\n(.*?)\n```', (ROOT/name).read_text(), re.S) + assert len(blocks) == 1 and blocks[0] in consumer, name + guide_fences += 1 + record['guide_fences'] = guide_fences + def save(): (output/'receipt.json').write_text(json.dumps(record,indent=2)+'\n') + def run(name, command, rejection=(), timed=False, timeout=180): + log=output/(name+'.log'); start=time.monotonic() + measured=['/usr/bin/time','-l']+command if timed and sys.platform=='darwin' else command + with log.open('w') as stream: + try: code=subprocess.run(measured,cwd=ROOT,stdout=stream,stderr=subprocess.STDOUT,timeout=timeout).returncode + except subprocess.TimeoutExpired: code=None + elapsed=time.monotonic()-start; text=log.read_text(); bad_import=any(s in text for s in ["no such module",'cannot load module','unable to load standard library']) + passed=code==0 if not rejection else code==1 and not bad_import and all(s in text for s in rejection) + memory=re.search(r'(\d+)\s+maximum resident set size',text) + row=dict(name=name,command=measured,exit_code=code,expected_rejection=list(rejection),status='passed' if passed else 'failed',seconds=elapsed,maximum_resident_set_bytes=int(memory[1]) if memory else None,log=log.relative_to(ROOT).as_posix(),log_sha256=sha(log)) + record['commands'].append(row);save() + print(name,code,round(elapsed,3),row['status'],flush=True) + if not passed:raise ValueError('Compiler contract failed: '+name) + return row + try: + flags=['swiftc','-swift-version','6','-strict-concurrency=complete','-warnings-as-errors'] + if sys.platform=='darwin': + sdk=subprocess.check_output(['xcrun','--sdk','macosx','--show-sdk-path'],text=True).strip() + flags+=['-sdk',sdk,'-target',platform.machine()+'-apple-macosx27.0'] + paths={} + for config in ('debug','release'): + products=pathlib.Path(subprocess.check_output(['swift','build','-c',config,'--show-bin-path'],cwd=ROOT,text=True).strip()) + include=products/'Modules' if (products/'Modules').is_dir() else products + for module in ('JSON','JSONSchema','JSONDSLCore','JSONValueDSL','JSONSchemaDSL'): + p=include/(module+'.swiftmodule');assert p.is_file() or p.is_dir(),p + paths[config]=(products,include) + source=output/(config+'-public-positive.swift');source.write_text(POSITIVE) + run(config+'-public-positive',flags+['-I',str(include),'-typecheck',str(source)]) + source=output/(config+'-qualified-declarations.swift');source.write_text(QUALIFIED) + run(config+'-qualified-declarations',flags+['-I',str(include),'-typecheck',str(source)]) + for filename,tokens in NEGATIVES.items(): + source=ROOT/'Tests/CompileNegative/DualDSL'/filename + run(config+'-'+source.stem,flags+['-I',str(include),'-typecheck',str(source)],rejection=tokens) + if args.mode=='compiler': + if sys.platform!='darwin':raise ValueError('Native Xcode compiler comparison requires macOS') + products,include=paths['debug']; sources=fixtures() + record['host_observations']=dict(os=platform.platform(),load_average=os.getloadavg(),cpu_count=os.cpu_count(),policy='Concurrent host load is retained; elapsed/compiler RSS observations are not quiet-host, runtime, staging-retention or caller instruction guarantees') + for name,text in sources.items(): (output/(name+'.swift')).write_text(text) + identity=json.loads(subprocess.check_output(['swift','package','show-dependencies','--format','json'],cwd=ROOT,text=True))['identity'] + package=re.sub('[^a-zA-Z0-9_]','_',identity) + alt=output/'partial';alt.mkdir(exist_ok=True) + common=flags+['-package-name',package,'-parse-as-library','-emit-module'] + original=ROOT/'Sources/JSONDSLCore/JSONContent.swift'; changed=alt/'JSONContent.swift';changed.write_text(partial_source(original.read_text())) + core_sources=[str(changed)]+[str(p) for p in sorted((ROOT/'Sources/JSONDSLCore').glob('*.swift')) if p.name!='JSONContent.swift'] + run('partial-core-module',common+['-module-name','JSONDSLCore','-I',str(include)]+core_sources+['-emit-module-path',str(alt/'JSONDSLCore.swiftmodule')]) + for module in ['JSONValueDSL','JSONSchemaDSL']: + run('partial-'+module+'-module',common+['-module-name',module,'-I',str(alt),'-I',str(include)]+[str(p) for p in sorted((ROOT/'Sources'/module).glob('*.swift'))]+['-emit-module-path',str(alt/(module+'.swiftmodule'))]) + for strategy,includes in [('variadic',['-I',str(include)]),('partial',['-I',str(alt),'-I',str(include)])]: + for name in sources: + row=run(strategy+'-'+name,flags+includes+['-typecheck',str(output/(name+'.swift'))],timed=True) + record['measurements'].append(dict(strategy=strategy,fixture=name,source_sha256=sha(output/(name+'.swift')),seconds=row['seconds'],maximum_resident_set_bytes=row['maximum_resident_set_bytes'])) + record['comparison_scope']='Both strategies compile real DSL sources against real JSON/JSONSchema; alternative modules are local experiment outputs, not shipped code or semantic shims.' + assert len([c for c in record['commands'] if c['expected_rejection']])==12 + assert all(sha(ROOT/r['path'])==r['sha256'] for r in record['source_inputs']) + record['status']='passed'; record['negative_fixtures']=6;record['configurations']=2 + except (OSError,ValueError,KeyError,AssertionError,subprocess.SubprocessError) as error: + record['status']='failed';record['error']=str(error) + record['finished_utc']=datetime.datetime.now(datetime.timezone.utc).isoformat();save() + if record['status']=='passed':print('DUAL_DSL_COMPILER passed negatives=12 configurations=2 mode='+args.mode) + return 0 if record['status']=='passed' else 1 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index 392473e..fcd6a4c 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -7,7 +7,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SDK = 'swift-6.4.0-RELEASE_wasm' COUNTS = {'JSONTests': 119, 'JSONBridgesTests': 10, 'JSONSchemaTests': 55, 'JSONRegexRuntimeTests': 12, - 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 61} + 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 61, 'JSONDSLTests': 47} GROUPS = {'core': 6, 'resources': 1, 'schema': 2, 'streaming': 3, 'html': 6} def main(): @@ -54,7 +54,7 @@ def binary(scratch, config, name): run('sdk', [swift, 'sdk', 'list'], lambda c, t: c == 0 and SDK in t.splitlines()) run('wasmkit-version', [wasmkit, '--version']); run('node-version', [node, '--version']) manifest = json.loads(run('root-manifest', [swift, 'package', 'dump-package'])) - if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONBridges', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming']: + if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONBridges', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONSchemaDSL', 'JSONStreaming', 'JSONValueDSL']: raise ValueError('Changed public products/dependencies') def invoke(engine, path, mode='command', arguments=(), repo=True, resources=True): if engine == 'node': diff --git a/Sources/JSONDSLCore/Documentation.docc/JSONDSLCore.md b/Sources/JSONDSLCore/Documentation.docc/JSONDSLCore.md new file mode 100644 index 0000000..56b9a13 --- /dev/null +++ b/Sources/JSONDSLCore/Documentation.docc/JSONDSLCore.md @@ -0,0 +1,38 @@ +# ``JSONDSLCore`` + +Shared typed structure for the two optional DSL products. + +## Overview + +This supporting module has no standalone SwiftPM product. Its public structural types +appear in the generic signatures of JSONValueDSL and JSONSchemaDSL. It imports JSON +only, with no framework result builder, parser, validator or external dependency. + +The builder preserves typed tuples, optional content, choices and loop iterations. +Conditional context conformances decide which declarations are eligible in each domain. +There is no runtime shape inference, String dictionary merge or semantic interpretation. +Protocol implementations and staging are trusted authored code. JSONDSLNode factories +assemble a flat IR; only materialize performs finite iterative finalization. The schema +facade uses a package continuation of the same ledger through engine document admission. + +## Topics + +### Structural composition +- ``JSONContentBuilder`` +- ``JSONContentTuple`` +- ``JSONContentOptional`` +- ``JSONContentChoice`` +- ``JSONContentLoop`` + +### Trusted staging +- ``JSONDSLNode`` +- ``JSONDSLMember`` +- ``JSONValueFieldsContent`` +- ``JSONValueElementsContent`` +- ``JSONSchemaClausesContent`` +- ``JSONSchemaPropertiesContent`` +- ``JSONSchemaDefinitionsContent`` +- ``JSONSchemaPatternsContent`` +- ``JSONSchemaDependentSchemasContent`` +- ``JSONSchemaDependenciesContent`` +- ``JSONSchemaNodesContent`` diff --git a/Sources/JSONDSLCore/JSONContent.swift b/Sources/JSONDSLCore/JSONContent.swift new file mode 100644 index 0000000..c884c96 --- /dev/null +++ b/Sources/JSONDSLCore/JSONContent.swift @@ -0,0 +1,545 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +// A SwiftUI-ContentBuilder-inspired but fully independent typed content structure. +// The builder itself is intentionally domain-agnostic: contexts supply conformance. +// No SwiftUI import, SwiftUI types, implicit JSON coercion or runtime reflection. + +/// Typed sibling storage whose context conformances determine domain eligibility. +public struct JSONContentTuple { + /// Typed siblings in their original authored order. + public let elements: (repeat each Element) + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public init(_ elements: repeat each Element) { self.elements = (repeat each elements) } +} +/// Optional typed content; absence emits no declaration. +public struct JSONContentOptional { + /// Selected typed content, or nil for omission. + public let component: Component? + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - component: The selected typed content, or nil to omit it. + public init(_ component: Component?) { self.component = component } +} +/// Exactly one selected branch without semantic coercion. +public enum JSONContentChoice { + /// Content selected by the first conditional branch. + case first(First) + /// Content selected by the second conditional branch. + case second(Second) +} +/// Ordered typed iterations in trusted authored staging. +public struct JSONContentLoop { + /// Typed loop iterations in order. + public let components: [Component] + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - components: Typed loop iterations in their original order. + public init(_ components: [Component]) { self.components = components } +} + +/// Structural composition shared by the two optional DSL products. +@resultBuilder +public enum JSONContentBuilder { + /// Preserves typed siblings in a variadic tuple; domain eligibility comes from conditional conformances. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public static func buildBlock(_ elements: repeat each Element) -> JSONContentTuple< + repeat each Element + > { + .init(repeat each elements) + } + /// Preserves optional content; nil contributes no declaration. + /// + /// - Parameters: + /// - component: The selected typed content, or nil to omit it. + public static func buildOptional(_ component: Component?) -> JSONContentOptional { + .init(component) + } + /// Preserves the selected branch without a substitute null or Boolean schema. + /// + /// - Parameters: + /// - first: The selected first branch, preserving its domain eligibility. + public static func buildEither(first: First) -> JSONContentChoice { + .first(first) + } + /// Preserves the selected branch without a substitute null or Boolean schema. + /// + /// - Parameters: + /// - second: The selected second branch, preserving its domain eligibility. + public static func buildEither(second: Second) -> JSONContentChoice { + .second(second) + } + /// Preserves loop iterations in order for their containing typed context. + /// + /// - Parameters: + /// - components: Typed loop iterations in their original order. + public static func buildArray(_ components: [Component]) -> JSONContentLoop { + .init(components) + } + /// Forwards available typed content without erasure or semantic interpretation. + /// + /// - Parameters: + /// - component: The selected typed content, or nil to omit it. + public static func buildLimitedAvailability(_ component: Component) -> Component { component } +} + +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONValueFieldsContent { + /// Emits ordered object members into trusted, unmetered staging. + /// + /// - Parameters: + /// - members: The caller-owned ordered buffer; emission performs no admission or evaluation. + func writeValueFields(to members: inout [JSONDSLMember]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONValueElementsContent { + /// Emits ordered array elements into trusted, unmetered staging. + /// + /// - Parameters: + /// - elements: The caller-owned ordered buffer; emission performs no admission or evaluation. + func writeValueElements(to elements: inout [JSONDSLNode]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaClausesContent { + /// Emits explicit schema keyword declarations into trusted, unmetered staging. + /// + /// - Parameters: + /// - members: The caller-owned ordered buffer; emission performs no admission or evaluation. + func writeSchemaClauses(to members: inout [JSONDSLMember]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaPropertiesContent { + /// Emits named property schemas into trusted, unmetered staging. + /// + /// - Parameters: + /// - members: The caller-owned ordered buffer; emission performs no admission or evaluation. + func writeSchemaProperties(to members: inout [JSONDSLMember]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaNodesContent { + /// Emits ordered Boolean or object subschemas into trusted, unmetered staging. + /// + /// - Parameters: + /// - nodes: The caller-owned ordered buffer; emission performs no admission or evaluation. + func writeSchemaNodes(to nodes: inout [JSONDSLNode]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaDependenciesContent { + /// Emits dependent-required declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + func writeSchemaDependencies(to members: inout [JSONDSLMember]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaPatternsContent { + /// Emits pattern-keyed schemas into trusted, unmetered staging. + /// + /// - Parameters: + /// - members: The caller-owned ordered buffer; emission performs no admission or evaluation. + func writeSchemaPatterns(to members: inout [JSONDSLMember]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaDefinitionsContent { + /// Emits named schema definitions into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + func writeSchemaDefinitions(to members: inout [JSONDSLMember]) +} +/// A typed staging emission contract; implementations are trusted authored code, outside finalization accounting. +public protocol JSONSchemaDependentSchemasContent { + /// Emits dependent schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + func writeDependentSchemas(to members: inout [JSONDSLMember]) +} + +// Context-specific conditional conformances mirror ContentBuilder's design principle. +// Deliberately not type-erased; wrong-domain items are compile-time errors. + +extension JSONContentTuple: JSONValueFieldsContent where repeat each Element: JSONValueFieldsContent { + /// Emits ordered object members into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeValueFields(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeValueFields(to: &members) } + } +} +extension JSONContentOptional: JSONValueFieldsContent where Component: JSONValueFieldsContent { + /// Emits ordered object members into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeValueFields(to members: inout [JSONDSLMember]) { + component?.writeValueFields(to: &members) + } +} +extension JSONContentChoice: JSONValueFieldsContent +where First: JSONValueFieldsContent, Second: JSONValueFieldsContent { + /// Emits ordered object members into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeValueFields(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeValueFields(to: &members) + case .second(let item): item.writeValueFields(to: &members) + } + } +} +extension JSONContentLoop: JSONValueFieldsContent where Component: JSONValueFieldsContent { + /// Emits ordered object members into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeValueFields(to members: inout [JSONDSLMember]) { + for component in components { component.writeValueFields(to: &members) } + } +} + +extension JSONContentTuple: JSONValueElementsContent where repeat each Element: JSONValueElementsContent { + /// Emits array elements into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public func writeValueElements(to elements: inout [JSONDSLNode]) { + for element in repeat each self.elements { element.writeValueElements(to: &elements) } + } +} +extension JSONContentOptional: JSONValueElementsContent where Component: JSONValueElementsContent { + /// Emits array elements into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public func writeValueElements(to elements: inout [JSONDSLNode]) { + component?.writeValueElements(to: &elements) + } +} +extension JSONContentChoice: JSONValueElementsContent +where First: JSONValueElementsContent, Second: JSONValueElementsContent { + /// Emits array elements into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public func writeValueElements(to elements: inout [JSONDSLNode]) { + switch self { + case .first(let item): item.writeValueElements(to: &elements) + case .second(let item): item.writeValueElements(to: &elements) + } + } +} +extension JSONContentLoop: JSONValueElementsContent where Component: JSONValueElementsContent { + /// Emits array elements into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public func writeValueElements(to elements: inout [JSONDSLNode]) { + for component in components { component.writeValueElements(to: &elements) } + } +} + +extension JSONContentTuple: JSONSchemaClausesContent where repeat each Element: JSONSchemaClausesContent { + /// Emits schema keyword declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaClauses(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeSchemaClauses(to: &members) } + } +} +extension JSONContentOptional: JSONSchemaClausesContent where Component: JSONSchemaClausesContent { + /// Emits schema keyword declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaClauses(to members: inout [JSONDSLMember]) { + component?.writeSchemaClauses(to: &members) + } +} +extension JSONContentChoice: JSONSchemaClausesContent +where First: JSONSchemaClausesContent, Second: JSONSchemaClausesContent { + /// Emits schema keyword declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaClauses(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeSchemaClauses(to: &members) + case .second(let item): item.writeSchemaClauses(to: &members) + } + } +} +extension JSONContentLoop: JSONSchemaClausesContent where Component: JSONSchemaClausesContent { + /// Emits schema keyword declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaClauses(to members: inout [JSONDSLMember]) { + for component in components { component.writeSchemaClauses(to: &members) } + } +} + +extension JSONContentTuple: JSONSchemaPropertiesContent +where repeat each Element: JSONSchemaPropertiesContent { + /// Emits property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaProperties(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeSchemaProperties(to: &members) } + } +} +extension JSONContentOptional: JSONSchemaPropertiesContent where Component: JSONSchemaPropertiesContent { + /// Emits property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaProperties(to members: inout [JSONDSLMember]) { + component?.writeSchemaProperties(to: &members) + } +} +extension JSONContentChoice: JSONSchemaPropertiesContent +where First: JSONSchemaPropertiesContent, Second: JSONSchemaPropertiesContent { + /// Emits property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaProperties(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeSchemaProperties(to: &members) + case .second(let item): item.writeSchemaProperties(to: &members) + } + } +} +extension JSONContentLoop: JSONSchemaPropertiesContent where Component: JSONSchemaPropertiesContent { + /// Emits property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaProperties(to members: inout [JSONDSLMember]) { + for component in components { component.writeSchemaProperties(to: &members) } + } +} + +extension JSONContentTuple: JSONSchemaNodesContent where repeat each Element: JSONSchemaNodesContent { + /// Emits subschemas in a composition list into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - nodes: The unmetered staged subschema list, preserving order and Boolean schemas. + public func writeSchemaNodes(to nodes: inout [JSONDSLNode]) { + for element in repeat each self.elements { element.writeSchemaNodes(to: &nodes) } + } +} +extension JSONContentOptional: JSONSchemaNodesContent where Component: JSONSchemaNodesContent { + /// Emits subschemas in a composition list into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - nodes: The unmetered staged subschema list, preserving order and Boolean schemas. + public func writeSchemaNodes(to nodes: inout [JSONDSLNode]) { component?.writeSchemaNodes(to: &nodes) } +} +extension JSONContentChoice: JSONSchemaNodesContent +where First: JSONSchemaNodesContent, Second: JSONSchemaNodesContent { + /// Emits subschemas in a composition list into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - nodes: The unmetered staged subschema list, preserving order and Boolean schemas. + public func writeSchemaNodes(to nodes: inout [JSONDSLNode]) { + switch self { + case .first(let item): item.writeSchemaNodes(to: &nodes) + case .second(let item): item.writeSchemaNodes(to: &nodes) + } + } +} +extension JSONContentLoop: JSONSchemaNodesContent where Component: JSONSchemaNodesContent { + /// Emits subschemas in a composition list into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - nodes: The unmetered staged subschema list, preserving order and Boolean schemas. + public func writeSchemaNodes(to nodes: inout [JSONDSLNode]) { + for component in components { component.writeSchemaNodes(to: &nodes) } + } +} + +extension JSONContentTuple: JSONSchemaDependenciesContent +where repeat each Element: JSONSchemaDependenciesContent { + /// Emits dependent-required declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDependencies(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeSchemaDependencies(to: &members) } + } +} +extension JSONContentOptional: JSONSchemaDependenciesContent where Component: JSONSchemaDependenciesContent { + /// Emits dependent-required declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDependencies(to members: inout [JSONDSLMember]) { + component?.writeSchemaDependencies(to: &members) + } +} +extension JSONContentChoice: JSONSchemaDependenciesContent +where First: JSONSchemaDependenciesContent, Second: JSONSchemaDependenciesContent { + /// Emits dependent-required declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDependencies(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeSchemaDependencies(to: &members) + case .second(let item): item.writeSchemaDependencies(to: &members) + } + } +} +extension JSONContentLoop: JSONSchemaDependenciesContent where Component: JSONSchemaDependenciesContent { + /// Emits dependent-required declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDependencies(to members: inout [JSONDSLMember]) { + for component in components { component.writeSchemaDependencies(to: &members) } + } +} + +extension JSONContentTuple: JSONSchemaPatternsContent where repeat each Element: JSONSchemaPatternsContent { + /// Emits patterned property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaPatterns(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeSchemaPatterns(to: &members) } + } +} +extension JSONContentOptional: JSONSchemaPatternsContent where Component: JSONSchemaPatternsContent { + /// Emits patterned property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaPatterns(to members: inout [JSONDSLMember]) { + component?.writeSchemaPatterns(to: &members) + } +} +extension JSONContentChoice: JSONSchemaPatternsContent +where First: JSONSchemaPatternsContent, Second: JSONSchemaPatternsContent { + /// Emits patterned property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaPatterns(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeSchemaPatterns(to: &members) + case .second(let item): item.writeSchemaPatterns(to: &members) + } + } +} +extension JSONContentLoop: JSONSchemaPatternsContent where Component: JSONSchemaPatternsContent { + /// Emits patterned property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaPatterns(to members: inout [JSONDSLMember]) { + for component in components { component.writeSchemaPatterns(to: &members) } + } +} + +extension JSONContentTuple: JSONSchemaDefinitionsContent +where repeat each Element: JSONSchemaDefinitionsContent { + /// Emits named schema definitions into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDefinitions(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeSchemaDefinitions(to: &members) } + } +} +extension JSONContentOptional: JSONSchemaDefinitionsContent where Component: JSONSchemaDefinitionsContent { + /// Emits named schema definitions into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDefinitions(to members: inout [JSONDSLMember]) { + component?.writeSchemaDefinitions(to: &members) + } +} +extension JSONContentChoice: JSONSchemaDefinitionsContent +where First: JSONSchemaDefinitionsContent, Second: JSONSchemaDefinitionsContent { + /// Emits named schema definitions into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDefinitions(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeSchemaDefinitions(to: &members) + case .second(let item): item.writeSchemaDefinitions(to: &members) + } + } +} +extension JSONContentLoop: JSONSchemaDefinitionsContent where Component: JSONSchemaDefinitionsContent { + /// Emits named schema definitions into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDefinitions(to members: inout [JSONDSLMember]) { + for component in components { component.writeSchemaDefinitions(to: &members) } + } +} + +extension JSONContentTuple: JSONSchemaDependentSchemasContent +where repeat each Element: JSONSchemaDependentSchemasContent { + /// Emits dependent schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeDependentSchemas(to members: inout [JSONDSLMember]) { + for element in repeat each self.elements { element.writeDependentSchemas(to: &members) } + } +} +extension JSONContentOptional: JSONSchemaDependentSchemasContent +where Component: JSONSchemaDependentSchemasContent { + /// Emits dependent schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeDependentSchemas(to members: inout [JSONDSLMember]) { + component?.writeDependentSchemas(to: &members) + } +} +extension JSONContentChoice: JSONSchemaDependentSchemasContent +where First: JSONSchemaDependentSchemasContent, Second: JSONSchemaDependentSchemasContent { + /// Emits dependent schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeDependentSchemas(to members: inout [JSONDSLMember]) { + switch self { + case .first(let item): item.writeDependentSchemas(to: &members) + case .second(let item): item.writeDependentSchemas(to: &members) + } + } +} +extension JSONContentLoop: JSONSchemaDependentSchemasContent +where Component: JSONSchemaDependentSchemasContent { + /// Emits dependent schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeDependentSchemas(to members: inout [JSONDSLMember]) { + for component in components { component.writeDependentSchemas(to: &members) } + } +} + +extension JSONContentTuple: Sendable where repeat each Element: Sendable {} +extension JSONContentOptional: Sendable where Component: Sendable {} +extension JSONContentChoice: Sendable where First: Sendable, Second: Sendable {} +extension JSONContentLoop: Sendable where Component: Sendable {} diff --git a/Sources/JSONDSLCore/JSONDSLNode.swift b/Sources/JSONDSLCore/JSONDSLNode.swift new file mode 100644 index 0000000..41f8d7e --- /dev/null +++ b/Sources/JSONDSLCore/JSONDSLNode.swift @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// An ordered member in trusted, authored DSL staging. +/// +/// Staging is not resource admission. The containing DSL finalizes the complete tree. +public struct JSONDSLMember: Sendable { + /// The literal decoded-scalar member name; it is not a Pointer or dotted path. + public let key: String + /// The exact staged payload; admission occurs at finalization. + public let value: JSONDSLNode + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ key: String, _ value: JSONDSLNode) { + self.key = key + self.value = value + } +} + +/// A flat staging representation, without a recursive owned IR lifetime. +/// +/// Factories are trusted authoring operations, outside the finalization budget. +/// They preserve literal containers, exact keys and numbers without interpretation. +public struct JSONDSLNode: Sendable { + private enum Token: Sendable { + case literal(JSON) + case object, array + case key(String) + case endObject, endArray + } + private let tokens: [Token] + private init(_ tokens: [Token]) { self.tokens = tokens } + + /// Stages an exact JSON payload without admitting it or flattening its containers. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func literal(_ value: JSON) -> Self { Self([.literal(value)]) } + /// Combines ordered members into flat, unmetered staging; this factory does not finalize. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public static func object(_ members: [JSONDSLMember]) -> Self { + var tokens: [Token] = [.object] + for member in members { + tokens.append(.key(member.key)) + tokens.append(contentsOf: member.value.tokens) + } + tokens.append(.endObject) + return Self(tokens) + } + /// Combines ordered elements into flat, unmetered staging; literal arrays remain single values. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public static func array(_ elements: [Self]) -> Self { + var tokens: [Token] = [.array] + for element in elements { tokens.append(contentsOf: element.tokens) } + tokens.append(.endArray) + return Self(tokens) + } + + /// Finalizes trusted staging through one finite invocation-owned ledger. + /// + /// This does not account for staging, caller callbacks, or process RSS. + /// - Parameters: + /// - limits: Inclusive finite finalization capacities and work limits. + /// - Throws: Exact duplicate keys, resource/configuration failure or cancellation. + public func materialize(limits: JSONLimits = .balanced) throws -> JSON { + try finalize(limits: limits, domain: .core) { value, _ in value } + } + + private struct Frame { + let object: Bool + var members: [JSONMember] = [] + var values: [JSON] = [] + var index = JSONExactKeyIndex() + var key: JSONKey? + } + + /// The schema facade continues this exact ledger through document admission. + package func finalize( + limits: JSONLimits, domain: JSONOperationDomain, + checkpointObserver: (@Sendable (UInt64) -> Void)? = nil, + _ finish: (JSON, inout JSONWorkMeter) throws -> Result + ) throws -> Result { + var meter = try JSONWorkMeter(limits: limits, domain: domain) + meter.checkpointObserver = checkpointObserver + var frames: [Frame] = [] + var root: JSON? + func requireSlot(_ frames: [Frame], meter: borrowing JSONWorkMeter) throws { + if let parent = frames.last, !parent.object { + try meter.require(.arrayItems, UInt64(parent.values.count) + 1) + } + } + func append(_ value: JSON, frames: inout [Frame], root: inout JSON?, meter: inout JSONWorkMeter) + throws + { + guard !frames.isEmpty else { + guard root == nil else { throw JSONFailure(.configuration, code: .invalidOperation) } + root = value + return + } + let position = frames.count - 1 + if frames[position].object { + guard let key = frames[position].key else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + guard + try frames[position].index.insert( + key, position: frames[position].members.count, meter: &meter) + else { + throw JSONFailure(.syntax, code: .duplicateKey) + } + try meter.retain(JSONLogicalWeights.member) + frames[position].members.append(JSONMember(key, value: value)) + frames[position].key = nil + } else { + try meter.retain(JSONLogicalWeights.arraySlot) + frames[position].values.append(value) + } + } + for token in tokens { + try meter.work() + switch token { + case .literal(let value): + try requireSlot(frames, meter: meter) + try JSONAdmission.value(value, meter: &meter, owned: true, depth: UInt64(frames.count)) + try append(value, frames: &frames, root: &root, meter: &meter) + case .object, .array: + try requireSlot(frames, meter: meter) + try meter.require(.maxDepth, UInt64(frames.count) + 1) + try meter.charge(.nodes) + try meter.retain(JSONLogicalWeights.node) + try meter.retain(JSONLogicalWeights.frame) + if case .object = token { + frames.append(Frame(object: true)) + } else { + frames.append(Frame(object: false)) + } + case .key(let spelling): + guard let last = frames.last, last.object, last.key == nil else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try meter.require(.objectMembers, UInt64(last.members.count) + 1) + try JSONAdmission.text(spelling, key: true, meter: &meter, owned: true) + let key = try JSONKey(spelling, meter: &meter) + guard try last.index.find(key, meter: &meter) == nil else { + throw JSONFailure(.syntax, code: .duplicateKey) + } + frames[frames.count - 1].key = key + case .endObject, .endArray: + guard let frame = frames.popLast(), frame.key == nil else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try meter.release(JSONLogicalWeights.frame) + let value: JSON = + frame.object + ? .object(JSONObject(admitted: frame.members, index: frame.index)) : .array(frame.values) + try append(value, frames: &frames, root: &root, meter: &meter) + } + } + guard frames.isEmpty, let root else { throw JSONFailure(.configuration, code: .invalidOperation) } + try meter.checkpoint() + let result = try finish(root, &meter) + try meter.checkpoint() + return result + } +} diff --git a/Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md b/Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md new file mode 100644 index 0000000..9e5fd2a --- /dev/null +++ b/Sources/JSONSchemaDSL/Documentation.docc/AuthoredSchemas.md @@ -0,0 +1,54 @@ +# Authored schemas, engine validation + +Emit an exact declaration, then explicitly compile and evaluate it. + +The following example is compiled and run by `Examples/DualDSL` after its value example. + +```swift +let schema = try JSONSchemaDSL.document { + Schema.schemaURI("https://json-schema.org/draft/2020-12/schema") + Schema.type(.object) + Schema.properties { + Property("id") { Schema.type(.integer) } + Property("amount") { Schema.reference("#/$defs/Amount") } + } + Schema.required("id", "amount") + Schema.definitions { + Definition("Amount") { + Schema.type(.number) + Schema.minimum(tenth) + Schema.multipleOf(tenth) + } + } +} +let plan = try JSONSchemaCompiler().compile(schema) +let validator = JSONSchemaValidator() +``` + +Import JSON, JSONSchema and JSONSchemaDSL. The source example in `Examples/DualDSL` +also validates a positive instance, two negative instances and a missing-resource +operational failure. A document containing only definitions does not select one; +use an explicit reference such as the one above. IDs and reference text are preserved. +Externally supplied resources use the existing JSONSchemaDocument retrieval identity +and immutable JSONSchemaRegistry; the DSL never fetches a URL or infers a registry. + +`Property` does not imply requiredness, `defaultValue` does not fill missing values, +and `format` does not select assertion policy. Use existing JSONSchemaOptions. +`Rule(false)` is a false subschema; `Rule {}` is an empty object schema; neither is +JSON null. Explicit `rawKeyword` preserves unknown metadata and malformed declarations +for the existing compiler to classify. There is no synonym that silently repairs them. +An empty enum compiles as an empty allowed set and produces ordinary invalidity; +negative lengths and an empty oneOf remain compilation failures under the engine. + +Duplicate exact names fail during finalization; scalar-distinct names remain distinct. +Resource/configuration failures and cancellation throw. An invalid instance returns +`isValid == false`; malformed declarations, missing resources, nonproductive reference +cycles and operational exhaustion throw. Branches do not swallow these failures. +Annotations and dynamic scope remain engine-owned and are compared with handwritten +schema paths in the real-engine tests. + +The authored closure, typed wrappers and staged buffers are unmetered. A flat IR avoids +recursive owned staging-node teardown; finalization uses explicit frames and one finite +ledger through document admission. This is not a whole-DSL resource report, an instruction +quota for caller code, a staging retention cap or process RSS guarantee. Use the existing +checked JSONSchemaBuilder and explicit accumulators for dynamic bounded construction. diff --git a/Sources/JSONSchemaDSL/Documentation.docc/JSONSchemaDSL.md b/Sources/JSONSchemaDSL/Documentation.docc/JSONSchemaDSL.md new file mode 100644 index 0000000..4eccef0 --- /dev/null +++ b/Sources/JSONSchemaDSL/Documentation.docc/JSONSchemaDSL.md @@ -0,0 +1,40 @@ +# ``JSONSchemaDSL`` + +Typed authored Draft 2020-12 declarations, with the existing engine as authority. + +## Overview + +``document(limits:_:)`` emits ordinary JSONSchemaDocument. +``Schema`` supplies explicit keyword declarations. Properties, definitions, +pattern properties, dependent schemas and composition rules have distinct typed +contexts; using a declaration in the wrong context fails Swift compilation. +``boolean(_:limits:)`` emits a Boolean schema without inventing an +object or substituting JSON null. + +Only JSONSchemaCompiler interprets dialects, references and vocabulary requirements. +Only JSONSchemaValidator produces instance verdicts and evaluation annotations. +Compilation is separate from document admission. Properties infer no requiredness, +defaults never mutate instances, format stays annotation by default, and references +resolve only through the existing explicit offline registry. No DSL validator exists. + +Authored callbacks and staging are trusted and unmetered. One finite finalization +ledger continues through JSONSchemaDocument admission; it is not a staging/RSS or +whole-pipeline cumulative-accounting promise. + +## Topics + +### Authoring and evaluation +- +- ``document(limits:_:)`` +- ``boolean(_:limits:)`` +- ``Schema`` +- ``SchemaType`` +- ``SchemaClause`` + +### Typed contexts +- ``Property`` +- ``Definition`` +- ``PatternProperty`` +- ``DependentSchema`` +- ``RequiredDependency`` +- ``Rule`` diff --git a/Sources/JSONSchemaDSL/JSONSchemaDSL.swift b/Sources/JSONSchemaDSL/JSONSchemaDSL.swift new file mode 100644 index 0000000..9bee977 --- /dev/null +++ b/Sources/JSONSchemaDSL/JSONSchemaDSL.swift @@ -0,0 +1,711 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONDSLCore +import JSONSchema + +/// Stages one schema document; only finalization is checked and callbacks/staging are unmetered. +/// +/// - Parameters: +/// - limits: Inclusive finite finalization capacities and work quotas; staging is excluded. +/// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. +public func document( + limits: JSONLimits = .balanced, + @JSONContentBuilder _ content: () -> Content +) throws -> JSONSchemaDocument { + try Task.checkCancellation() + try limits.validate() + var entries: [JSONDSLMember] = [] + content().writeSchemaClauses(to: &entries) + return try JSONDSLNode.object(entries).finalize(limits: limits, domain: .schemaCompilation) { + raw, meter in + try JSONSchemaDocument(raw, meter: &meter) + } +} +/// Admits an ordinary Boolean schema through the existing JSONSchemaDocument authority. +/// +/// - Parameters: +/// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. +/// - limits: Inclusive finite finalization capacities and work quotas; staging is excluded. +public func boolean(_ allowsAll: Bool, limits: JSONLimits = .balanced) throws -> JSONSchemaDocument +{ + try JSONSchemaDocument(.boolean(allowsAll), limits: limits) +} + +/// One staged schema declaration (`keyword: exact value`), not an evaluation rule. +public struct SchemaClause: Sendable, JSONSchemaClausesContent { + /// The staged exact keyword/value declaration. + public let entry: JSONDSLMember + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ name: String, value: JSONDSLNode) { entry = .init(name, value) } + /// Emits schema keyword declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaClauses(to members: inout [JSONDSLMember]) { members.append(entry) } +} + +/// Document/schema keyword types; no type inference from sibling declarations. +public enum SchemaType: String, Sendable { + /// The explicit JSON Schema `object` type name. + case object + /// The explicit JSON Schema `array` type name. + case array + /// The explicit JSON Schema `string` type name. + case string + /// The explicit JSON Schema `number` type name. + case number + /// The explicit JSON Schema `integer` type name. + case integer + /// The explicit JSON Schema `boolean` type name. + case boolean + /// The explicit JSON Schema `null` type name. + case null +} + +/// Fluent namespace that covers standard Draft 2020-12 keywords, but does not +/// infer effective schema shape, override compiler policy or resolve remote refs. +public enum Schema { + private static func one(_ key: String, _ value: JSON) -> SchemaClause { + .init(key, value: .literal(value)) + } + private static func jsonStringArray(_ values: [String]) -> JSONDSLNode { + .array(values.map { .literal(.string($0)) }) + } + private static func jsonArray(_ values: [JSON]) -> JSONDSLNode { .array(values.map(JSONDSLNode.literal)) } + private static func makeObject(_ content: Content) -> JSONDSLNode { + var entries: [JSONDSLMember] = [] + content.writeSchemaClauses(to: &entries) + return .object(entries) + } + private static func schemaKeyword( + _ name: String, _ content: Content + ) -> SchemaClause { .init(name, value: makeObject(content)) } + + // Core and identification. + /// Declares `$schema` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func schemaURI(_ value: String) -> SchemaClause { one("$schema", .string(value)) } + /// Declares `$id` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func identifier(_ value: String) -> SchemaClause { one("$id", .string(value)) } + /// Declares `$anchor` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func anchor(_ value: String) -> SchemaClause { one("$anchor", .string(value)) } + /// Declares `$dynamicAnchor` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func dynamicAnchor(_ value: String) -> SchemaClause { + one("$dynamicAnchor", .string(value)) + } + /// Declares `$ref` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func reference(_ value: String) -> SchemaClause { one("$ref", .string(value)) } + /// Declares `$dynamicRef` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func dynamicReference(_ value: String) -> SchemaClause { + one("$dynamicRef", .string(value)) + } + /// Declares `$comment` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func comment(_ value: String) -> SchemaClause { one("$comment", .string(value)) } + /// Declares `$vocabulary` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - entries: Ordered URI/requirement or named declaration pairs; exact duplicates fail finalization. + public static func vocabulary(_ entries: [(String, Bool)]) -> SchemaClause { + .init("$vocabulary", value: .object(entries.map { .init($0.0, .literal(.boolean($0.1))) })) + } + /// Declares `$defs` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func definitions( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { + var entries: [JSONDSLMember] = [] + content().writeSchemaDefinitions(to: &entries) + return .init("$defs", value: .object(entries)) + } + + // Scalar assertions and typed vocabularies. + /// Declares `type` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func type(_ value: SchemaType) -> SchemaClause { one("type", .string(value.rawValue)) } + /// Declares `type` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - values: Explicit exact payloads in declaration order; nested arrays remain values. + public static func types(_ values: [SchemaType]) -> SchemaClause { + .init("type", value: jsonStringArray(values.map(\.rawValue))) + } + /// Declares `enum` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - values: Explicit exact payloads in declaration order; nested arrays remain values. + public static func enumValues(_ values: [JSON]) -> SchemaClause { + .init("enum", value: jsonArray(values)) + } + /// Declares `const` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func constant(_ value: JSON) -> SchemaClause { one("const", value) } + /// Declares `minimum` using an explicit exact decimal without binary floating-point narrowing. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func minimum(_ value: JSONNumber) -> SchemaClause { one("minimum", .number(value)) } + /// Declares `maximum` using an explicit exact decimal without binary floating-point narrowing. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func maximum(_ value: JSONNumber) -> SchemaClause { one("maximum", .number(value)) } + /// Declares `exclusiveMinimum` using an explicit exact decimal without binary floating-point narrowing. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func exclusiveMinimum(_ value: JSONNumber) -> SchemaClause { + one("exclusiveMinimum", .number(value)) + } + /// Declares `exclusiveMaximum` using an explicit exact decimal without binary floating-point narrowing. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func exclusiveMaximum(_ value: JSONNumber) -> SchemaClause { + one("exclusiveMaximum", .number(value)) + } + /// Declares `multipleOf` using an explicit exact decimal without binary floating-point narrowing. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func multipleOf(_ value: JSONNumber) -> SchemaClause { one("multipleOf", .number(value)) } + /// Declares `minLength` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func minLength(_ value: Int) -> SchemaClause { + one("minLength", .number(JSONNumber(integer: value))) + } + /// Declares `maxLength` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func maxLength(_ value: Int) -> SchemaClause { + one("maxLength", .number(JSONNumber(integer: value))) + } + /// Declares `pattern` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func pattern(_ value: String) -> SchemaClause { one("pattern", .string(value)) } + /// Declares `minItems` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func minItems(_ value: Int) -> SchemaClause { + one("minItems", .number(JSONNumber(integer: value))) + } + /// Declares `maxItems` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func maxItems(_ value: Int) -> SchemaClause { + one("maxItems", .number(JSONNumber(integer: value))) + } + /// Declares `uniqueItems` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func uniqueItems(_ value: Bool) -> SchemaClause { one("uniqueItems", .boolean(value)) } + /// Declares `minContains` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func minContains(_ value: Int) -> SchemaClause { + one("minContains", .number(JSONNumber(integer: value))) + } + /// Declares `maxContains` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func maxContains(_ value: Int) -> SchemaClause { + one("maxContains", .number(JSONNumber(integer: value))) + } + /// Declares `minProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func minProperties(_ value: Int) -> SchemaClause { + one("minProperties", .number(JSONNumber(integer: value))) + } + /// Declares `maxProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func maxProperties(_ value: Int) -> SchemaClause { + one("maxProperties", .number(JSONNumber(integer: value))) + } + /// Declares required names explicitly; property declarations never infer requiredness. + /// + /// - Parameters: + /// - names: Explicit required member names, preserved in declaration order. + public static func required(_ names: String...) -> SchemaClause { + .init("required", value: jsonStringArray(names)) + } + /// Declares format metadata; assertion policy remains with the existing compiler and validator. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + public static func format(_ name: String) -> SchemaClause { one("format", .string(name)) } + + // Metadata/annotation vocabulary; not assertions and never mutate an instance. + /// Declares `title` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func title(_ value: String) -> SchemaClause { one("title", .string(value)) } + /// Declares `description` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func description(_ value: String) -> SchemaClause { one("description", .string(value)) } + /// Declares default metadata without inserting or changing any instance value. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func defaultValue(_ value: JSON) -> SchemaClause { one("default", value) } + /// Declares `examples` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - values: Explicit exact payloads in declaration order; nested arrays remain values. + public static func examples(_ values: [JSON]) -> SchemaClause { + .init("examples", value: jsonArray(values)) + } + /// Declares `deprecated` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func deprecated(_ value: Bool = true) -> SchemaClause { one("deprecated", .boolean(value)) } + /// Declares `readOnly` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func readOnly(_ value: Bool = true) -> SchemaClause { one("readOnly", .boolean(value)) } + /// Emits typed declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func writeOnly(_ value: Bool = true) -> SchemaClause { one("writeOnly", .boolean(value)) } + /// Declares `contentEncoding` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func contentEncoding(_ value: String) -> SchemaClause { + one("contentEncoding", .string(value)) + } + /// Declares `contentMediaType` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public static func contentMediaType(_ value: String) -> SchemaClause { + one("contentMediaType", .string(value)) + } + + // Object applicators. + /// Declares `properties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func properties( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { + var members: [JSONDSLMember] = [] + content().writeSchemaProperties(to: &members) + return .init("properties", value: .object(members)) + } + /// Declares `patternProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func patternProperties( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { + var members: [JSONDSLMember] = [] + content().writeSchemaPatterns(to: &members) + return .init("patternProperties", value: .object(members)) + } + /// Declares `dependentSchemas` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func dependentSchemas( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { + var members: [JSONDSLMember] = [] + content().writeDependentSchemas(to: &members) + return .init("dependentSchemas", value: .object(members)) + } + /// Declares `dependentRequired` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func dependentRequired( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { + var members: [JSONDSLMember] = [] + content().writeSchemaDependencies(to: &members) + return .init("dependentRequired", value: .object(members)) + } + /// Declares `propertyNames` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func propertyNames( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("propertyNames", content()) } + /// Declares `additionalProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public static func additionalProperties(_ allowsAll: Bool) -> SchemaClause { + one("additionalProperties", .boolean(allowsAll)) + } + /// Declares `additionalProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func additionalProperties( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("additionalProperties", content()) } + /// Declares `unevaluatedProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public static func unevaluatedProperties(_ allowsAll: Bool) -> SchemaClause { + one("unevaluatedProperties", .boolean(allowsAll)) + } + /// Declares `unevaluatedProperties` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func unevaluatedProperties( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("unevaluatedProperties", content()) } + + // Array applicators. + /// Declares `items` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public static func items(_ allowsAll: Bool) -> SchemaClause { one("items", .boolean(allowsAll)) } + /// Declares `items` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func items( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("items", content()) } + /// Declares `prefixItems` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func prefixItems( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { + var nodes: [JSONDSLNode] = [] + content().writeSchemaNodes(to: &nodes) + return .init("prefixItems", value: .array(nodes)) + } + /// Declares `contains` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func contains( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("contains", content()) } + /// Declares `unevaluatedItems` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public static func unevaluatedItems(_ allowsAll: Bool) -> SchemaClause { + one("unevaluatedItems", .boolean(allowsAll)) + } + /// Declares `unevaluatedItems` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func unevaluatedItems( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("unevaluatedItems", content()) } + + // Logical/conditional applicators. + private static func alternatives(_ name: String, content: Content) + -> SchemaClause + { + var nodes: [JSONDSLNode] = [] + content.writeSchemaNodes(to: &nodes) + return .init(name, value: .array(nodes)) + } + /// Declares `allOf` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func allOf( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { alternatives("allOf", content: content()) } + /// Declares `anyOf` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func anyOf( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { alternatives("anyOf", content: content()) } + /// Declares `oneOf` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func oneOf( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { alternatives("oneOf", content: content()) } + /// Declares `not` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func not( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("not", content()) } + /// Declares `if` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func ifSchema( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("if", content()) } + /// Declares `then` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func thenSchema( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("then", content()) } + /// Declares `else` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func elseSchema( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("else", content()) } + /// Declares `contentSchema` without repairing, validating or evaluating its schema semantics. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func contentSchema( + @JSONContentBuilder _ content: () -> Content + ) -> SchemaClause { schemaKeyword("contentSchema", content()) } + + // Full-fidelity escape: preservation of extension metadata, unknown optional vocabularies. + // The canonical compiler, not this DSL, determines whether a keyword is admitted. + /// Preserves an exact raw declaration; the existing compiler determines its syntax and meaning. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - exactValue: The exact authored input for this declaration; no semantic fallback is applied. + public static func rawKeyword(_ name: String, _ exactValue: JSON) -> SchemaClause { + one(name, exactValue) + } +} + +/// Declares one subschema in an alternatives list (or a Boolean schema). +public struct Rule: Sendable, JSONSchemaNodesContent { + /// The staged subschema, without evaluation or inferred constraints. + public let node: JSONDSLNode + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public init(@JSONContentBuilder _ content: () -> Content) { + var members: [JSONDSLMember] = [] + content().writeSchemaClauses(to: &members) + node = .object(members) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public init(_ allowsAll: Bool) { node = .literal(.boolean(allowsAll)) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - raw: The exact authored input for this declaration; no semantic fallback is applied. + public init(raw: JSON) { node = .literal(raw) } + /// Emits subschemas in a composition list into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - nodes: The unmetered staged subschema list, preserving order and Boolean schemas. + public func writeSchemaNodes(to nodes: inout [JSONDSLNode]) { nodes.append(node) } +} + +/// A property/definition/pattern property entry. Exact duplicate names are checked later. +public struct Property: Sendable, JSONSchemaPropertiesContent { + /// The ordered exact named subschema declaration. + public let member: JSONDSLMember + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public init( + _ name: String, @JSONContentBuilder _ content: () -> Content + ) { + var members: [JSONDSLMember] = [] + content().writeSchemaClauses(to: &members) + member = .init(name, .object(members)) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public init(_ name: String, allowsAll: Bool) { member = .init(name, .literal(.boolean(allowsAll))) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - rawSchema: The exact authored input for this declaration; no semantic fallback is applied. + public init(_ name: String, rawSchema: JSON) { member = .init(name, .literal(rawSchema)) } + /// Emits property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaProperties(to members: inout [JSONDSLMember]) { members.append(member) } +} + +/// A dependentRequired entry. Does not silently infer presence from properties. +public struct RequiredDependency: Sendable, JSONSchemaDependenciesContent { + /// The literal decoded-scalar member name; it is not a Pointer or dotted path. + public let key: String + /// Explicit required member names; no property inference is performed. + public let names: [String] + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - requires: Explicit required member names; none are inferred from property declarations. + public init(_ key: String, requires: [String]) { + self.key = key + self.names = requires + } + /// Emits dependent-required declarations into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDependencies(to members: inout [JSONDSLMember]) { + members.append(.init(key, .array(names.map { .literal(.string($0)) }))) + } +} + +/// A named `$defs` entry. It cannot be placed in `properties` by accident. +public struct Definition: Sendable, JSONSchemaDefinitionsContent { + /// The ordered exact named subschema declaration. + public let member: JSONDSLMember + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public init( + _ name: String, @JSONContentBuilder _ content: () -> Content + ) { + var entries: [JSONDSLMember] = [] + content().writeSchemaClauses(to: &entries) + member = .init(name, .object(entries)) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - name: The exact declaration name or keyword-specific text, without normalization. + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public init(_ name: String, allowsAll: Bool) { member = .init(name, .literal(.boolean(allowsAll))) } + /// Emits named schema definitions into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaDefinitions(to members: inout [JSONDSLMember]) { members.append(member) } +} + +/// A regular-expression property key. The compiler validates the pattern dialect. +public struct PatternProperty: Sendable, JSONSchemaPatternsContent { + /// The ordered exact named subschema declaration. + public let member: JSONDSLMember + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - pattern: The exact pattern text; regex validity belongs to the schema compiler. + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public init( + _ pattern: String, @JSONContentBuilder _ content: () -> Content + ) { + var entries: [JSONDSLMember] = [] + content().writeSchemaClauses(to: &entries) + member = .init(pattern, .object(entries)) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - pattern: The exact pattern text; regex validity belongs to the schema compiler. + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public init(_ pattern: String, allowsAll: Bool) { member = .init(pattern, .literal(.boolean(allowsAll))) } + /// Emits patterned property schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeSchemaPatterns(to members: inout [JSONDSLMember]) { members.append(member) } +} + +/// A dependentSchemas entry (subschema, not a list of required property names). +public struct DependentSchema: Sendable, JSONSchemaDependentSchemasContent { + /// The ordered exact named subschema declaration. + public let member: JSONDSLMember + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public init( + _ key: String, @JSONContentBuilder _ content: () -> Content + ) { + var entries: [JSONDSLMember] = [] + content().writeSchemaClauses(to: &entries) + member = .init(key, .object(entries)) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - allowsAll: True for an accepting Boolean schema, false for a rejecting Boolean schema. + public init(_ key: String, allowsAll: Bool) { member = .init(key, .literal(.boolean(allowsAll))) } + /// Emits dependent schemas into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeDependentSchemas(to members: inout [JSONDSLMember]) { members.append(member) } +} diff --git a/Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md b/Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md new file mode 100644 index 0000000..33f0752 --- /dev/null +++ b/Sources/JSONValueDSL/Documentation.docc/AuthoredValues.md @@ -0,0 +1,43 @@ +# Trusted exact value authoring + +Keep authored structure separate from bounded finalization. + +The following example is compiled and run by `Examples/DualDSL`. + +```swift +let tenth = try JSONNumber(parsing: "0.1") +let instance = try JSONValueDSL.object { + Field("id", UInt64(9_007_199_254_740_993)) + Field("amount", exactNumber: tenth) + Field("é", 1) + Field("e\u{301}", 2) + Field.array("nested") { + Element.array { + Element(1) + Element(2) + } + } +} +``` + +Import JSON for its exact-number API and JSONValueDSL for syntax. These declarations +retain distinct decoded scalar keys, their insertion order and exact numeric values. +Finalization throws existing JSONFailure for exact duplicates and resource/configuration +failures; cancellation propagates as CancellationError. A partial value is not returned. +Errors must not be replaced with null or defaults. Parse explicit decimal text before +entering the nonthrowing authored closure, so conversion errors propagate normally. + +`if`/`else` selects content; a missing branch contributes no declaration. `for` preserves +iteration order. `Field("note", .null)` is an explicit null member, distinct from absence. +`Elements(contentsOf:)` expands exactly the supplied values; an array within that list +remains an array value. An empty object remains `{}` and an empty array remains `[]`. + +Finalization traverses literal payloads too. All returned values satisfy the supplied +finite profile, with cumulative finalization work and inner cancellation checkpoints. +The existing 49-cap owners and logical weight version are unchanged. The builder +callback and earlier staging allocations are not billed by that finalization ledger. +Caller computation and process RSS are not owned work or logical-retention metrics. +No public whole-DSL report or untrusted staging guarantee is provided in P1.2. + +The shared generic builder types live in JSONDSLCore without a standalone product. +Applications select the JSONValueDSL product; core JSON does not import or re-export it. diff --git a/Sources/JSONValueDSL/Documentation.docc/JSONValueDSL.md b/Sources/JSONValueDSL/Documentation.docc/JSONValueDSL.md new file mode 100644 index 0000000..f164def --- /dev/null +++ b/Sources/JSONValueDSL/Documentation.docc/JSONValueDSL.md @@ -0,0 +1,28 @@ +# ``JSONValueDSL`` + +Typed authored JSON objects and arrays over the existing exact value model. + +## Overview + +Use ``object(limits:_:)`` or ``array(limits:_:)`` +for trusted source-authored declarations. ``Field`` contributes one literal key, +``Element`` contributes one array value, and ``Elements`` explicitly expands a list. +Missing conditional content emits nothing. Nested arrays remain single elements. +Integer input is exact; decimals require an explicit JSONNumber. There is no implicit +Float/Double, Optional or dictionary conversion and no dotted-path interpretation. + +Callbacks, typed wrappers, IR collection and staging are unmetered trusted authoring. +Only finalization is a checked operation. It uses one invocation-owned finite ledger +with iterative frames and the engine's exact admission and key index. Staging is not +an untrusted-input boundary, a live-retention/RSS cap or a whole-pipeline work report. +Use JSON's checked accumulators or strict parser for dynamic untrusted input. + +## Topics + +### Authoring +- +- ``object(limits:_:)`` +- ``array(limits:_:)`` +- ``Field`` +- ``Element`` +- ``Elements`` diff --git a/Sources/JSONValueDSL/JSONValueDSL.swift b/Sources/JSONValueDSL/JSONValueDSL.swift new file mode 100644 index 0000000..ce0fa2f --- /dev/null +++ b/Sources/JSONValueDSL/JSONValueDSL.swift @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONDSLCore + +/// Stages one object; only finalization is checked and callbacks/staging are unmetered. +/// +/// - Parameters: +/// - limits: Inclusive finite finalization capacities and work quotas; staging is excluded. +/// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. +public func object( + limits: JSONLimits = .balanced, + @JSONContentBuilder _ content: () -> Content +) throws -> JSON { + try Task.checkCancellation() + try limits.validate() + var members: [JSONDSLMember] = [] + content().writeValueFields(to: &members) + return try JSONDSLNode.object(members).materialize(limits: limits) +} +/// Stages one array; only finalization is checked and callbacks/staging are unmetered. +/// +/// - Parameters: +/// - limits: Inclusive finite finalization capacities and work quotas; staging is excluded. +/// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. +public func array( + limits: JSONLimits = .balanced, + @JSONContentBuilder _ content: () -> Content +) throws -> JSON { + try Task.checkCancellation() + try limits.validate() + var nodes: [JSONDSLNode] = [] + content().writeValueElements(to: &nodes) + return try JSONDSLNode.array(nodes).materialize(limits: limits) +} + +/// One ordered object member. Keys are literal decoded Unicode scalars; no dotted path coercion. +public struct Field: Sendable, JSONValueFieldsContent { + /// The literal decoded-scalar member name; it is not a Pointer or dotted path. + public let key: String + /// The exact staged payload; admission occurs at finalization. + public let value: JSONDSLNode + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ key: String, _ value: JSON) { + self.key = key + self.value = .literal(value) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ key: String, _ value: String) { self.init(key, .string(value)) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ key: String, _ value: Bool) { self.init(key, .boolean(value)) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ key: String, _ value: Integer) { + self.init(key, .number(JSONNumber(integer: value))) + } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ key: String, exactNumber value: JSONNumber) { self.init(key, .number(value)) } + /// Stages one object; only finalization is checked and callbacks/staging are unmetered. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func object( + _ key: String, @JSONContentBuilder _ content: () -> Content + ) -> Self { + var members: [JSONDSLMember] = [] + content().writeValueFields(to: &members) + return Self(key: key, value: .object(members)) + } + /// Stages one array; only finalization is checked and callbacks/staging are unmetered. + /// + /// - Parameters: + /// - key: The literal decoded-scalar key, without path parsing or normalization. + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func array( + _ key: String, @JSONContentBuilder _ content: () -> Content + ) -> Self { + var nodes: [JSONDSLNode] = [] + content().writeValueElements(to: &nodes) + return Self(key: key, value: .array(nodes)) + } + private init(key: String, value: JSONDSLNode) { + self.key = key + self.value = value + } + /// Emits ordered object members into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - members: The unmetered ordered staging buffer; this is not a checked accumulator. + public func writeValueFields(to members: inout [JSONDSLMember]) { members.append(.init(key, value)) } +} + +/// One array item. A nested JSON array remains one element; never silently flattens. +public struct Element: Sendable, JSONValueElementsContent { + /// The exact staged payload; admission occurs at finalization. + public let value: JSONDSLNode + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ value: JSON) { self.value = .literal(value) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ value: String) { self.init(.string(value)) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ value: Bool) { self.init(.boolean(value)) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - value: The exact declared payload; it is not coerced or interpreted by the DSL. + public init(_ value: Integer) { self.init(.number(JSONNumber(integer: value))) } + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - exactNumber: The exact authored input for this declaration; no semantic fallback is applied. + public init(exactNumber: JSONNumber) { self.init(.number(exactNumber)) } + /// Stages one object; only finalization is checked and callbacks/staging are unmetered. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func object(@JSONContentBuilder _ content: () -> Content) + -> Self + { + var members: [JSONDSLMember] = [] + content().writeValueFields(to: &members) + return Self(value: .object(members)) + } + /// Stages one array; only finalization is checked and callbacks/staging are unmetered. + /// + /// - Parameters: + /// - content: Trusted synchronous authored declarations; callback execution and staging are unmetered. + public static func array(@JSONContentBuilder _ content: () -> Content) + -> Self + { + var nodes: [JSONDSLNode] = [] + content().writeValueElements(to: &nodes) + return Self(value: .array(nodes)) + } + private init(value: JSONDSLNode) { self.value = value } + /// Emits array elements into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public func writeValueElements(to elements: inout [JSONDSLNode]) { elements.append(value) } +} + +/// Explicit sequence expansion (rather than implicit flattening of array values). +public struct Elements: Sendable, JSONValueElementsContent { + /// Caller-supplied values to expand explicitly, preserving array values as single elements. + public let values: [JSON] + /// Stores exact authored content without admission, coercion or schema evaluation. + /// + /// - Parameters: + /// - values: Explicit exact payloads in declaration order; nested arrays remain values. + public init(contentsOf values: [JSON]) { self.values = values } + /// Emits array elements into trusted staging; this does not perform admission or evaluation. + /// + /// - Parameters: + /// - elements: Typed siblings or array elements in their authored order. + public func writeValueElements(to elements: inout [JSONDSLNode]) { + elements.append(contentsOf: values.map(JSONDSLNode.literal)) + } +} diff --git a/Tests/CompileNegative/DualDSL/DefinitionInsideProperties.swift b/Tests/CompileNegative/DualDSL/DefinitionInsideProperties.swift new file mode 100644 index 0000000..c7d9fb7 --- /dev/null +++ b/Tests/CompileNegative/DualDSL/DefinitionInsideProperties.swift @@ -0,0 +1,2 @@ +import JSONSchemaDSL +let _ = try JSONSchemaDSL.document { Schema.properties { Definition("id") { Schema.type(.string) } } } diff --git a/Tests/CompileNegative/DualDSL/ElementInsideObject.swift b/Tests/CompileNegative/DualDSL/ElementInsideObject.swift new file mode 100644 index 0000000..9d08907 --- /dev/null +++ b/Tests/CompileNegative/DualDSL/ElementInsideObject.swift @@ -0,0 +1,3 @@ +import JSONSchemaDSL +import JSONValueDSL +let _ = try JSONValueDSL.object { Element("not-an-object-field") } diff --git a/Tests/CompileNegative/DualDSL/FieldInsideSchema.swift b/Tests/CompileNegative/DualDSL/FieldInsideSchema.swift new file mode 100644 index 0000000..c53a4a7 --- /dev/null +++ b/Tests/CompileNegative/DualDSL/FieldInsideSchema.swift @@ -0,0 +1,3 @@ +import JSONSchemaDSL +import JSONValueDSL +let _ = try JSONSchemaDSL.document { Field("invalid", "value") } diff --git a/Tests/CompileNegative/DualDSL/ImplicitFloatingPoint.swift b/Tests/CompileNegative/DualDSL/ImplicitFloatingPoint.swift new file mode 100644 index 0000000..ed67149 --- /dev/null +++ b/Tests/CompileNegative/DualDSL/ImplicitFloatingPoint.swift @@ -0,0 +1,2 @@ +import JSONValueDSL +let _ = try JSONValueDSL.object { Field("cannot-assume-fidelity", 0.1) } diff --git a/Tests/CompileNegative/DualDSL/PropertyInsideDefinitions.swift b/Tests/CompileNegative/DualDSL/PropertyInsideDefinitions.swift new file mode 100644 index 0000000..8b2e45b --- /dev/null +++ b/Tests/CompileNegative/DualDSL/PropertyInsideDefinitions.swift @@ -0,0 +1,2 @@ +import JSONSchemaDSL +let _ = try JSONSchemaDSL.document { Schema.definitions { Property("id") { Schema.type(.string) } } } diff --git a/Tests/CompileNegative/DualDSL/SchemaClauseInsideArray.swift b/Tests/CompileNegative/DualDSL/SchemaClauseInsideArray.swift new file mode 100644 index 0000000..eddb431 --- /dev/null +++ b/Tests/CompileNegative/DualDSL/SchemaClauseInsideArray.swift @@ -0,0 +1,3 @@ +import JSONSchemaDSL +import JSONValueDSL +let _ = try JSONValueDSL.array { Schema.type(.string) } diff --git a/Tests/JSONDSLTests/DSLPrototypeTests.swift b/Tests/JSONDSLTests/DSLPrototypeTests.swift new file mode 100644 index 0000000..14343f0 --- /dev/null +++ b/Tests/JSONDSLTests/DSLPrototypeTests.swift @@ -0,0 +1,298 @@ +import JSON +import JSONDSLCore +import JSONSchema +import JSONSchemaDSL +import JSONValueDSL +// SPDX-License-Identifier: Apache-2.0 +import Testing + +@Suite struct DSLPrototypeTests { + @Test func prototypeValueObjectNestedArrayConditionalAndLoop() throws { + let addOptional = true + let doc = try JSONValueDSL.object { + Field("id", UInt64(9_007_199_254_740_993)) + Field("title", "A customer") + Field("verified", true) + if addOptional { Field("note", .null) } + Field.array("tags") { + Element("one") + Element("two") + Element.array { + Element(10) + Element(20) + } + for i in 1...3 { Element(i) } + } + Field.object("meta") { + Field("kind", "demo") + Field("active", false) + } + } + let o = try #require(doc.objectValue) + #expect((o.members.map { $0.key.string }) == (["id", "title", "verified", "note", "tags", "meta"])) + #expect((o["note"]) == (.null)) + #expect((o["tags"]?.arrayValue?.count) == (6)) + #expect((o["tags"]?.arrayValue?[2].arrayValue?.count) == (2)) + #expect((o["meta"]?.objectValue?["kind"]) == (.string("demo"))) + } + @Test func prototypeEmptyContainersAndNoFlattening() throws { + let value = try JSONValueDSL.object { + Field.object("empty") {} + Field.array("nested") { + Element.array { Element(1) } + } + } + #expect((value.objectValue?["empty"]?.objectValue?.members.count) == (0)) + let a = try #require(value.objectValue?["nested"]?.arrayValue) + #expect((a.count) == (1)) + #expect((a[0].arrayValue?.count) == (1)) + } + @Test func prototypeConditionalAbsenceIsNotNull() throws { + let show = false + let doc = try JSONValueDSL.object { + Field("a", 1) + if show { Field("b", .null) } + } + #expect((doc.objectValue?["b"]) == nil) + #expect((doc.objectValue?.members.count) == (1)) + } + @Test func prototypeChoiceAndExplicitFlatten() throws { + let simple = false + let doc = try JSONValueDSL.array { + Element("initial") + if simple { Element(1) } else { Element(2) } + Elements(contentsOf: [3, 4]) + } + let a = try #require(doc.arrayValue) + #expect((a.count) == (4)) + #expect((a[1]) == (.number(JSONNumber(integer: 2)))) + } + @Test func prototypeDuplicateValueKeysMustThrow() throws { + #expect(throws: JSONFailure.self) { + try JSONValueDSL.object { + Field("name", "first") + Field("name", "second") + } + } + } + @Test func prototypeUnicodeScalarDistinctKeysRemainDistinct() throws { + let a = "\u{e9}" + let b = "e\u{301}" + #expect((a) == (b)) // Swift canonical String equality: demonstration of trap. + let obj = try JSONValueDSL.object { + Field(a, 1) + Field(b, 2) + } + #expect((obj.objectValue?.members.count) == (2)) + #expect(!(JSONKey(a) == JSONKey(b))) + } + @Test func prototypeSchemaRichDraft202012Shape() throws { + let schema = try JSONSchemaDSL.document { + Schema.schemaURI("https://json-schema.org/draft/2020-12/schema") + Schema.identifier("https://example.test/customer") + Schema.title("Customer") + Schema.description("Demo shape") + Schema.type(.object) + Schema.minProperties(1) + Schema.maxProperties(12) + Schema.properties { + Property("id") { Schema.reference("#/$defs/ID") } + Property("name") { + Schema.type(.string) + Schema.minLength(2) + Schema.maxLength(100) + Schema.pattern("^[^0-9]+$") + } + Property("state") { + Schema.enumValues(["new", "active", "closed"]) + Schema.defaultValue("new") + } + Property("orders") { + Schema.type(.array) + Schema.minItems(0) + Schema.uniqueItems(true) + Schema.items { + Schema.type(.integer) + Schema.minimum(JSONNumber(integer: 0)) + } + } + Property("sentinel", allowsAll: false) + } + Schema.required("id", "name") + Schema.patternProperties { PatternProperty("^x-") { Schema.type(.string) } } + Schema.dependentRequired { RequiredDependency("card", requires: ["billingAddress"]) } + Schema.dependentSchemas { DependentSchema("country") { Schema.type(.object) } } + Schema.propertyNames { Schema.type(.string) } + Schema.additionalProperties(false) + Schema.unevaluatedProperties(false) + Schema.definitions { + Definition("ID") { + Schema.type(.integer) + Schema.minimum(JSONNumber(integer: 0)) + } + } + Schema.rawKeyword("x-internal-note", "preserved") + } + let obj = try #require(schema.raw.objectValue) + #expect((obj["type"]) == (.string("object"))) + #expect((obj["properties"]?.objectValue?.members.count) == (5)) + #expect((obj["$defs"]?.objectValue?.members.count) == (1)) + #expect((obj["x-internal-note"]) == (.string("preserved"))) + #expect((obj["dependentRequired"]?.objectValue?.members.count) == (1)) + let plan = try JSONSchemaCompiler().compile(schema) + #expect( + try JSONSchemaValidator().validate(JSONParser().parse(#"{"id":1,"name":"Ada"}"#), against: plan) + .isValid) + #expect( + try !JSONSchemaValidator().validate(JSONParser().parse(#"{"id":-1,"name":"Ada"}"#), against: plan) + .isValid) + } + @Test func prototypeArrayCompositionConditionalAndBooleanSchema() throws { + let include = true + let schema = try JSONSchemaDSL.document { + Schema.type(.array) + Schema.prefixItems { + Rule { Schema.type(.string) } + Rule(false) + if include { Rule { Schema.type(.number) } } + } + Schema.contains { Schema.type(.string) } + Schema.minContains(1) + Schema.maxContains(3) + Schema.unevaluatedItems(false) + Schema.anyOf { + Rule { Schema.minItems(1) } + Rule(true) + } + Schema.oneOf { + Rule(false) + Rule(true) + } + Schema.allOf { + Rule(true) + Rule { Schema.maxItems(10) } + } + Schema.not { Schema.type(.null) } + Schema.ifSchema { Schema.minItems(2) } + Schema.thenSchema { Schema.minItems(3) } + Schema.elseSchema { Schema.minItems(1) } + } + let obj = try #require(schema.raw.objectValue) + #expect((obj["prefixItems"]?.arrayValue?.count) == (3)) + #expect((obj["oneOf"]?.arrayValue?.count) == (2)) + #expect((obj["allOf"]?.arrayValue?.count) == (2)) + let plan = try JSONSchemaCompiler().compile(schema) + #expect(try JSONSchemaValidator().validate(.array(["ok"]), against: plan).isValid) + #expect(try !JSONSchemaValidator().validate(.array(["ok", 1]), against: plan).isValid) + } + @Test func prototypeSchemaVocabularyAndAnnotations() throws { + let c = try JSONSchemaDSL.document { + Schema.vocabulary([("https://example.test/v", true)]) + Schema.anchor("hello") + Schema.dynamicAnchor("node") + Schema.dynamicReference("#node") + Schema.comment("not an assertion") + Schema.format("email") + Schema.contentEncoding("base64") + Schema.contentMediaType("text/plain") + Schema.contentSchema { Schema.type(.string) } + Schema.examples(["Ada", "Grace"]) + Schema.deprecated(true) + Schema.readOnly(true) + Schema.writeOnly(false) + } + #expect((c.raw.objectValue?["$vocabulary"]) != nil) + #expect((c.raw.objectValue?["readOnly"]) == (.boolean(true))) + let plan = try JSONSchemaCompiler().compile(c) + do { + _ = try JSONSchemaValidator().validate("person@example.test", against: plan) + Issue.record("Self dynamic reference is nonproductive, not invalidity") + } catch let error as JSONSchemaEvaluationFailure { + #expect(error.category == .nonproductiveReference) + } + } + @Test func prototypeSchemaDuplicateKeywordsAndPropertiesFail() throws { + #expect(throws: JSONFailure.self) { + try JSONSchemaDSL.document { + Schema.type(.object) + Schema.type(.array) + } + } + #expect(throws: JSONFailure.self) { + try JSONSchemaDSL.document { + Schema.properties { + Property("id") { Schema.type(.string) } + Property("id") { Schema.type(.integer) } + } + } + } + } + @Test func prototypeSchemaConditionalsAndLoopsDoNotEmitFalseSchemas() throws { + let shouldInclude = false + let schema = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + for name in ["alpha", "beta"] { Property(name) { Schema.type(.string) } } + if shouldInclude { Property("absent", allowsAll: false) } + } + } + let props = try #require(schema.raw.objectValue?["properties"]?.objectValue) + #expect((props.members.map { $0.key.string }) == (["alpha", "beta"])) + #expect((props["absent"]) == nil) + } + @Test func prototypeSchemaBooleanRoot() throws { + let accepting = try JSONSchemaDSL.boolean(true) + let rejecting = try JSONSchemaDSL.boolean(false) + #expect((accepting.raw) == (.boolean(true))) + #expect((rejecting.raw) == (.boolean(false))) + } + @Test func prototypeExactDecimalInputShape() throws { + let exact = try JSONNumber(parsing: "0.1") + let value = try JSONValueDSL.object { Field("decimal", exactNumber: exact) } + #expect((value.objectValue?["decimal"]) == (.number(exact))) + let zero = try JSONNumber(parsing: "0.0") + let schema = try JSONSchemaDSL.document { + Schema.type(.number) + Schema.multipleOf(exact) + Schema.minimum(zero) + } + #expect((schema.raw.objectValue?["multipleOf"]) == (.number(exact))) + } + @Test func prototypePrototypeCapacityAndDepthPreflight() throws { + var limit = JSONLimits.balanced + limit[.maxDepth] = 12 + limit[.arrayItems] = 2 + limit[.objectMembers] = 2 + #expect(throws: JSONFailure.self) { + try JSONValueDSL.array(limits: limit) { + Element(1) + Element(2) + Element(3) + } + } + #expect(throws: JSONFailure.self) { + try JSONValueDSL.object(limits: limit) { + Field("a", 1) + Field("b", 2) + Field("c", 3) + } + } + var shallow = JSONLimits.balanced + shallow[.maxDepth] = 1 + #expect(throws: JSONFailure.self) { + try JSONValueDSL.object(limits: shallow) { + Field.object("a") { Field.object("b") { Field("c", 1) } } + } + } + } + @Test func prototypeTypeBuilderNoConflationOfKeywordAndProperty() throws { + let doc = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("empty") {} + } + } + #expect( + (doc.raw.objectValue?["properties"]?.objectValue?["empty"]?.objectValue?.members.count) == (0)) + } +} diff --git a/Tests/JSONDSLTests/IntegrationTests.swift b/Tests/JSONDSLTests/IntegrationTests.swift new file mode 100644 index 0000000..336eec8 --- /dev/null +++ b/Tests/JSONDSLTests/IntegrationTests.swift @@ -0,0 +1,36 @@ +import JSON +import JSONSchema +import JSONSchemaDSL +import JSONValueDSL +// SPDX-License-Identifier: Apache-2.0 +import Testing + +@Test func dslExactEngineIntegration() throws { + let tenth = try JSONNumber(parsing: "0.1") + let value = try JSONValueDSL.object { + Field("amount", exactNumber: tenth) + Field("id", UInt64(9_007_199_254_740_993)) + Field.array("nested") { + Element.array { + Element(1) + Element(2) + } + } + } + let manual = try JSONParser().parse(#"{"amount":0.1,"id":9007199254740993,"nested":[[1,2]]}"#) + #expect(value == manual) + let schema = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("amount") { + Schema.type(.number) + Schema.minimum(tenth) + } + } + Schema.required("amount") + } + let plan = try JSONSchemaCompiler().compile(schema) + #expect(try JSONSchemaValidator().validate(value, against: plan).isValid) + #expect( + try !JSONSchemaValidator().validate(JSONParser().parse(#"{"amount":0.09}"#), against: plan).isValid) +} diff --git a/Tests/JSONDSLTests/ResourceLifecycleTests.swift b/Tests/JSONDSLTests/ResourceLifecycleTests.swift new file mode 100644 index 0000000..aa5e1bc --- /dev/null +++ b/Tests/JSONDSLTests/ResourceLifecycleTests.swift @@ -0,0 +1,220 @@ +import JSON +import JSONDSLCore +import JSONSchema +import JSONSchemaDSL +import JSONValueDSL +import Synchronization +// SPDX-License-Identifier: Apache-2.0 +import Testing + +@Test func dslV18ParentNodeCapacityIncludesAllNestedChildren() throws { + var limits = JSONLimits.balanced + limits[.nodes] = 4 + let accepted = try JSONValueDSL.object(limits: limits) { + Field.array("a") { + Element(1) + Element(2) + } + } + #expect(accepted == (try JSONParser().parse(#"{"a":[1,2]}"#))) + limits[.nodes] = 3 + do { + _ = try JSONValueDSL.object(limits: limits) { + Field.array("a") { + Element(1) + Element(2) + } + } + Issue.record("Parent quota must include nested children") + } catch let error as JSONFailure { + #expect(error.category == .resource && error.code == .limitExceeded) + #expect(error.resource?.metric == .nodes && error.resource?.attemptedUsage == 4) + } +} + +@Test func dslFinalizationSharesWorkAndRetentionAcrossSiblings() throws { + let node = JSONDSLNode.object([ + .init("one", .array([.literal(1), .literal(2)])), + .init("two", .object([.init("text", .literal("é"))])), + ]) + let stats = try node.finalize(limits: .balanced, domain: .core) { _, meter in meter.statistics() } + #expect(stats[.nodes] == 6 && stats[.workUnits] > 10 && stats.peakLogicalRetainedBytes > 0) + var limits = JSONLimits.balanced + limits[.workUnits] = stats[.workUnits] + _ = try node.materialize(limits: limits) + limits[.workUnits] -= 1 + do { + _ = try node.materialize(limits: limits) + Issue.record("Work must not reset at siblings") + } catch let error as JSONFailure { + #expect(error.resource?.metric == .workUnits && error.code == .limitExceeded) + } + limits = .balanced + limits[.retainedBytes] = stats.peakLogicalRetainedBytes + _ = try node.materialize(limits: limits) + limits[.retainedBytes] -= 1 + do { + _ = try node.materialize(limits: limits) + Issue.record("Overlapping finalization ownership must be charged") + } catch let error as JSONFailure { + #expect(error.resource?.metric == .retainedBytes && error.code == .limitExceeded) + } +} + +@Test func dslSchemaDocumentAdmissionContinuesTheSameLedger() throws { + let node = JSONDSLNode.object([.init("title", .literal("x"))]) + let stats = try node.finalize(limits: .balanced, domain: .schemaCompilation) { raw, meter in + _ = try JSONSchemaDocument(raw, meter: &meter) + return meter.statistics() + } + #expect(stats.domain == .schemaCompilation && stats[.nodes] == 4 && stats[.schemaNodes] == 4) + var limits = JSONLimits.balanced + limits[.nodes] = 4 + _ = try JSONSchemaDSL.document(limits: limits) { Schema.title("x") } + limits[.nodes] = 3 + do { + _ = try JSONSchemaDSL.document(limits: limits) { Schema.title("x") } + Issue.record("Document admission must not start a fresh meter") + } catch let error as JSONFailure { + #expect(error.resource?.domain == .schemaCompilation && error.resource?.metric == .nodes) + #expect(error.resource?.attemptedUsage == 4) + } +} + +@Test func dslLiteralPayloadAdmissionCannotBypassCoreCaps() throws { + let payloads: [(JSON, JSONResourceMetric, UInt64)] = [ + (.array([1, 2]), .arrayItems, 1), + (.object(try JSONObject(members: [.init("long", value: 1)])), .keyBytes, 3), + (.string("é"), .stringBytes, 1), + (.number(try JSONNumber(parsing: "1234")), .numberDigits, 3), + (.number(try JSONNumber(parsing: "1e9")), .absoluteExponent, 8), + ] + for (payload, metric, cap) in payloads { + var limits = JSONLimits.balanced + limits[metric] = cap + do { + _ = try JSONValueDSL.array(limits: limits) { Element(payload) } + Issue.record("Literal payload must be admitted") + } catch let error as JSONFailure { + #expect(error.code == .limitExceeded && error.resource?.metric == metric) + } + } +} + +@Test func dslFinalizationFailureIsTerminalEvenWhenPackageContinuationCatchesIt() throws { + var observed = false + do { + _ = try JSONDSLNode.literal(.null).finalize(limits: .balanced, domain: .core) { value, meter in + do { try meter.work(UInt64.max) } catch let error as JSONFailure { + observed = true + #expect(error.code == .counterOverflow) + } + return value + } + Issue.record("Caught finalization failure must prevent publication") + } catch let error as JSONFailure { #expect(observed && error.code == .counterOverflow) } +} + +@Test func dslFlatIRRejectsFiveThousandLevelsWithoutRecursiveLoweringOrIRTeardown() throws { + var node = JSONDSLNode.literal(.null) + for _ in 0..<5_000 { node = .array([node]) } + var limits = JSONLimits.balanced + limits[.maxDepth] = 64 + do { + _ = try node.materialize(limits: limits) + Issue.record("Deep staging must fail the finalization ceiling") + } catch let error as JSONFailure { + #expect(error.resource?.metric == .maxDepth && error.resource?.attemptedUsage == 65) + } + var admitted = JSONDSLNode.literal(.null) + for _ in 0..<128 { admitted = .array([admitted]) } + let exact = try admitted.materialize() + #expect( + try JSONWriter().writeString(exact) == String(repeating: "[", count: 128) + "null" + + String(repeating: "]", count: 128)) +} + +@Test func dslV19CancellationAtEntryDoesNotRunAuthoredCallback() async throws { + let started = Mutex(false) + let gate = AsyncStream.makeStream() + let task = Task { + for await _ in gate.stream { break } + return try JSONValueDSL.object { + Field( + "visited", + started.withLock { state -> Bool in + state = true + return true + }) + } + } + task.cancel() + gate.continuation.yield(()) + gate.continuation.finish() + await #expect(throws: CancellationError.self) { try await task.value } + #expect(started.withLock { !$0 }) +} + +@Test func dslV19CancellationInsideOwnedFinalizationNeverReturnsAValue() async throws { + let cancelled = Mutex(false) + let handle = Mutex?>(nil) + let gate = AsyncStream.makeStream() + let node = JSONDSLNode.array((0..<4_096).map { .literal(.number(JSONNumber(integer: $0))) }) + let task = Task { + for await _ in gate.stream { break } + var limits = JSONLimits.balanced + limits[.cancellationQuantum] = 1 + return try node.finalize( + limits: limits, domain: .core, + checkpointObserver: { work in + if work >= 64 { + cancelled.withLock { $0 = true } + handle.withLock { $0?.cancel() } + } + } + ) { value, _ in value } + } + handle.withLock { $0 = task } + gate.continuation.yield(()) + gate.continuation.finish() + await #expect(throws: CancellationError.self) { try await task.value } + #expect(cancelled.withLock { $0 }) + handle.withLock { $0 = nil } +} + +@Test func dslSharedFinalizationAndPlansAreInvocationOwned() async throws { + let declaration = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("id") { + Schema.type(.integer) + Schema.minimum(JSONNumber(integer: 1)) + } + } + Schema.required("id") + } + let plan = try JSONSchemaCompiler().compile(declaration) + let field = Field("id", UInt64(9_007_199_254_740_993)) + try await withThrowingTaskGroup(of: Bool.self) { group in + for index in 0..<32 { + group.addTask { + var limits = JSONLimits.balanced + if index.isMultiple(of: 2) { + limits[.nodes] = 1 + do { + _ = try JSONValueDSL.object(limits: limits) { field } + return false + } catch let error as JSONFailure { return error.resource?.metric == .nodes } + } + let value = try JSONValueDSL.object { field } + return try JSONSchemaValidator().validate(value, against: plan).isValid + } + } + var completed = 0 + for try await outcome in group { + #expect(outcome) + completed += 1 + } + #expect(completed == 32) + } +} diff --git a/Tests/JSONDSLTests/SemanticTests.swift b/Tests/JSONDSLTests/SemanticTests.swift new file mode 100644 index 0000000..f8f7091 --- /dev/null +++ b/Tests/JSONDSLTests/SemanticTests.swift @@ -0,0 +1,479 @@ +import JSON +import JSONSchema +import JSONSchemaDSL +import JSONValueDSL +// SPDX-License-Identifier: Apache-2.0 +import Testing + +private func parity( + _ declaration: JSONSchemaDocument, _ text: String, + valid: [String], invalid: [String], registry: JSONSchemaRegistry = .empty +) throws { + let handwritten = try JSONSchemaDocument(JSONParser().parse(text)) + #expect(try JSONComparison.equals(declaration.raw, handwritten.raw)) + let compiler = JSONSchemaCompiler() + let authored = try compiler.compile(declaration, registry: registry) + let manual = try compiler.compile(handwritten, registry: registry) + let validator = JSONSchemaValidator(options: .init(collectAnnotations: true)) + for (instances, expected) in [(valid, true), (invalid, false)] { + for text in instances { + let instance = try JSONParser().parse(text) + let a = try validator.validate(instance, against: authored) + let b = try validator.validate(instance, against: manual) + #expect(a.isValid == expected) + #expect(a.isValid == b.isValid) + #expect(a.annotations.map(\.value) == b.annotations.map(\.value)) + #expect(a.annotations.map(\.keywordLocation) == b.annotations.map(\.keywordLocation)) + } + } +} + +private func duplicate(_ body: () throws -> Void) throws { + do { + try body() + Issue.record("Exact duplicate declaration must throw") + } catch let error as JSONFailure { + #expect(error.category == .syntax && error.code == .duplicateKey) + #expect(error.resource == nil) + } +} + +@Test func dslV01ExactIntegersIncludeBoth128BitBoundaries() throws { + let value = try JSONValueDSL.object { + Field("large", UInt64(9_007_199_254_740_993)) + Field("signed", Int128.min) + Field("unsigned", UInt128.max) + } + let expected = try JSONParser().parse( + "{\"large\":9007199254740993,\"signed\":\(Int128.min),\"unsigned\":\(UInt128.max)}") + #expect(value == expected) + #expect(try #require(value.objectValue?["signed"]?.numberValue).integer(as: Int128.self) == Int128.min) + #expect( + try #require(value.objectValue?["unsigned"]?.numberValue).integer(as: UInt128.self) == UInt128.max) +} + +@Test func dslV02ExactDecimalLexemeAndMultipleOfNeverUseEpsilon() throws { + let tenth = try JSONNumber(parsing: "1.00e-1") + let value = try JSONValueDSL.object { Field("n", exactNumber: tenth) } + #expect(try #require(value.objectValue?["n"]?.numberValue).numericText(style: .preserved) == "1.00e-1") + let schema = try JSONSchemaDSL.document { + Schema.type(.number) + Schema.minimum(tenth) + Schema.multipleOf(tenth) + Schema.maximum(JSONNumber(integer: 2)) + } + try parity( + schema, #"{"type":"number","minimum":1.00e-1,"multipleOf":1.00e-1,"maximum":2}"#, + valid: ["0.1", "0.3", "2"], invalid: ["0.0999999999999999999999", "0.3000000000000000000001", "2.1"]) +} + +@Test func dslV04EveryExactDuplicateTableRejects() throws { + try duplicate { + _ = try JSONValueDSL.object { + Field("same", 1) + Field("same", 2) + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.title("a") + Schema.title("b") + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.properties { + Property("id") {} + Property("id") {} + } + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.definitions { + Definition("id") {} + Definition("id") {} + } + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.patternProperties { + PatternProperty("x") {} + PatternProperty("x") {} + } + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.dependentSchemas { + DependentSchema("x") {} + DependentSchema("x") {} + } + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.dependentRequired { + RequiredDependency("x", requires: ["a"]) + RequiredDependency("x", requires: ["b"]) + } + } + } + try duplicate { + _ = try JSONSchemaDSL.document { + Schema.vocabulary([("https://e.test/v", false), ("https://e.test/v", false)]) + } + } +} + +@Test func dslV05ScalarKeysLiteralPathsOrderAndEquality() throws { + let value = try JSONValueDSL.object { + Field("é", 1) + Field("e\u{301}", 2) + Field("a/b~.c", 3) + Field("", .null) + } + let object = try #require(value.objectValue) + #expect( + object.members.map { Array($0.key.string.unicodeScalars) } + == ["é", "e\u{301}", "a/b~.c", ""].map { Array($0.unicodeScalars) }) + #expect(object["é"] == 1 && object["e\u{301}"] == 2) + #expect(try JSONPointer(parsing: "/a~1b~0.c").resolve(in: value) == 3) + let reversed = try JSONValueDSL.object { + Field("", .null) + Field("a/b~.c", 3) + Field("e\u{301}", 2) + Field("é", 1) + } + #expect(value == reversed) + #expect(try JSONWriter().writeString(value) != JSONWriter().writeString(reversed)) + let schema = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("é") { Schema.constant(1) } + Property("e\u{301}") { Schema.constant(2) } + } + Schema.required("é", "e\u{301}") + } + try parity( + schema, + #"{"type":"object","properties":{"é":{"const":1},"e\u0301":{"const":2}},"required":["é","e\u0301"]}"#, + valid: [#"{"é":1,"e\u0301":2}"#], invalid: [#"{"é":1}"#, #"{"é":2,"e\u0301":1}"#]) +} + +@Test func dslV06ConditionalOmissionHasNoSubstituteValue() throws { + let include = false + let value = try JSONValueDSL.object { + if include { Field("missing", .null) } + Field("null", .null) + } + #expect(try value.presence(of: "missing") == .absent) + #expect(try value.presence(of: "null") == .null) + let schema = try JSONSchemaDSL.document { + if include { Schema.not {} } + Schema.properties { if include { Property("missing", allowsAll: false) } } + } + try parity(schema, #"{"properties":{}}"#, valid: ["null", "{}", #"{"missing":1}"#], invalid: []) +} + +@Test func dslV07OnlyExplicitElementsExpand() throws { + let one = try JSONValueDSL.array { + Element.array { + Element(1) + Element(2) + } + Element(.array([3, 4])) + } + let expanded = try JSONValueDSL.array { Elements(contentsOf: [.array([1, 2]), 3, 4]) } + #expect(one == (try JSONParser().parse("[[1,2],[3,4]]"))) + #expect(expanded == (try JSONParser().parse("[[1,2],3,4]"))) +} + +@Test func dslV08And09FalseTrueEmptyAndNullRemainDistinct() throws { + let rejecting = try JSONSchemaDSL.boolean(false) + let accepting = try JSONSchemaDSL.boolean(true) + let empty = try JSONSchemaDSL.document {} + #expect(rejecting.raw == .boolean(false) && accepting.raw == .boolean(true)) + #expect(empty.raw.objectValue?.isEmpty == true && empty.raw != rejecting.raw && empty.raw != .null) + try parity(rejecting, "false", valid: [], invalid: ["null", "{}", "false", "0"]) + try parity(accepting, "true", valid: ["null", "{}", "false", "0"], invalid: []) + try parity(empty, "{}", valid: ["null", "{}", "false", "0"], invalid: []) + #expect(throws: JSONSchemaCompilationFailure.self) { try JSONSchemaDocument(.null) } +} + +@Test func dslV10RequirednessIsOnlyExplicit() throws { + let optional = try JSONSchemaDSL.document { + Schema.properties { Property("n") { Schema.type(.integer) } } + } + let required = try JSONSchemaDSL.document { + Schema.properties { Property("n") { Schema.type(.integer) } } + Schema.required("n") + } + try parity( + optional, #"{"properties":{"n":{"type":"integer"}}}"#, valid: ["{}", #"{"n":1}"#], + invalid: [#"{"n":"1"}"#]) + try parity( + required, #"{"properties":{"n":{"type":"integer"}},"required":["n"]}"#, valid: [#"{"n":1}"#], + invalid: ["{}", #"{"n":null}"#]) +} + +@Test func dslV11DefaultAndUnknownMetadataNeverMutateInstances() throws { + let schema = try JSONSchemaDSL.document { + Schema.properties { + Property("n") { + Schema.type(.integer) + Schema.defaultValue(7) + } + } + Schema.rawKeyword("x-note", "A declaration, not an instruction") + } + let instance = try JSONValueDSL.object {} + let result = try JSONSchemaValidator().validate(instance, against: JSONSchemaCompiler().compile(schema)) + #expect(result.isValid && instance.objectValue?.isEmpty == true) + let property = try schema.node(at: JSONPointer(parsing: "/properties/n")) + #expect(try property.declaration(of: "default") == .value(7)) + #expect( + try schema.node().declaration(of: "x-note") == .value(.string("A declaration, not an instruction"))) +} + +@Test func dslV12FormatPolicyStaysWithValidator() throws { + let schema = try JSONSchemaDSL.document { + Schema.type(.string) + Schema.format("email") + } + let plan = try JSONSchemaCompiler().compile(schema) + let annotation = try JSONSchemaValidator(options: .init(collectAnnotations: true)).validate( + "bad", against: plan) + #expect(annotation.isValid) + #expect( + annotation.annotations.contains { $0.keyword == JSONKey("format") && $0.value == .string("email") }) + #expect( + try !JSONSchemaValidator(options: .init(formatAssertion: true)).validate("bad", against: plan).isValid + ) + #expect( + try JSONSchemaValidator(options: .init(formatAssertion: true)).validate( + "person@example.test", against: plan + ).isValid) +} + +@Test func dslV13AlternativesAndConditionalsPreserveDeclarations() throws { + let schema = try JSONSchemaDSL.document { + Schema.allOf { + Rule { Schema.type(.integer) } + Rule { Schema.minimum(JSONNumber(integer: 0)) } + } + Schema.anyOf { + Rule { Schema.multipleOf(JSONNumber(integer: 2)) } + Rule { Schema.constant(3) } + } + Schema.oneOf { + Rule(false) + Rule(true) + } + Schema.ifSchema { Schema.minimum(JSONNumber(integer: 10)) } + Schema.thenSchema { Schema.maximum(JSONNumber(integer: 12)) } + Schema.elseSchema { Schema.maximum(JSONNumber(integer: 6)) } + Schema.not { Schema.constant(4) } + } + try parity( + schema, + #"{"allOf":[{"type":"integer"},{"minimum":0}],"anyOf":[{"multipleOf":2},{"const":3}],"oneOf":[false,true],"if":{"minimum":10},"then":{"maximum":12},"else":{"maximum":6},"not":{"const":4}}"#, + valid: ["0", "2", "3", "6", "10", "12"], invalid: ["-2", "1", "4", "8", "14", "0.1"]) +} + +@Test func dslV14PointerAnchorAndNestedIdentifierUseCompiler() throws { + let schema = try JSONSchemaDSL.document { + Schema.identifier("https://e.test/root") + Schema.definitions { + Definition("a/b~") { + Schema.type(.integer) + Schema.anchor("small") + } + Definition("child") { + Schema.identifier("child") + Schema.type(.integer) + } + } + Schema.allOf { + Rule { Schema.reference("#/$defs/a~1b~0") } + Rule { Schema.reference("#sm%61ll") } + Rule { Schema.reference("child") } + } + } + try parity( + schema, + ##"{"$id":"https://e.test/root","$defs":{"a/b~":{"type":"integer","$anchor":"small"},"child":{"$id":"child","type":"integer"}},"allOf":[{"$ref":"#/$defs/a~1b~0"},{"$ref":"#sm%61ll"},{"$ref":"child"}]}"##, + valid: ["1"], invalid: ["\"1\"", "1.1"]) +} + +@Test func dslV14DynamicScopeAndOfflineRegistryAreUnmodified() throws { + let base = try JSONSchemaDSL.document { + Schema.identifier("https://e.test/tree") + Schema.dynamicAnchor("node") + Schema.type(.object) + Schema.properties { Property("child") { Schema.dynamicReference("#node") } } + } + let resource = try JSONSchemaDocument(base.raw, retrievalURI: JSONSchemaURI("https://e.test/tree")) + let registry = try JSONSchemaRegistry(documents: [resource]) + let schema = try JSONSchemaDSL.document { + Schema.identifier("https://e.test/strict") + Schema.dynamicAnchor("node") + Schema.reference("https://e.test/tree") + Schema.unevaluatedProperties(false) + } + try parity( + schema, + ##"{"$id":"https://e.test/strict","$dynamicAnchor":"node","$ref":"https://e.test/tree","unevaluatedProperties":false}"##, + valid: ["{}", #"{"child":{}}"#], invalid: [#"{"extra":1}"#, #"{"child":{"extra":1}}"#], + registry: registry) + do { + _ = try JSONSchemaCompiler().compile(schema) + Issue.record("Offline resource must be explicitly supplied") + } catch let error as JSONSchemaCompilationFailure { #expect(error.category == .missingResource) } +} + +@Test func dslV15RequiredUnknownVocabularyRemainsAnOperationalFailure() throws { + let meta = try JSONSchemaDSL.document { + Schema.identifier("https://e.test/dialect") + Schema.schemaURI("https://json-schema.org/draft/2020-12/schema") + Schema.vocabulary([("https://e.test/unknown-vocabulary", true)]) + } + let registry = try JSONSchemaRegistry(documents: [ + JSONSchemaDocument(meta.raw, retrievalURI: JSONSchemaURI("https://e.test/dialect")) + ]) + let schema = try JSONSchemaDSL.document { + Schema.schemaURI("https://e.test/dialect") + Schema.rawKeyword("custom", .null) + } + do { + _ = try JSONSchemaCompiler().compile(schema, registry: registry) + Issue.record("Required unknown vocabulary must throw") + } catch let error as JSONSchemaCompilationFailure { #expect(error.category == .vocabulary) } +} + +@Test func dslV16MalformedBoundsAndEmptyAlternativesAreNotRepaired() throws { + let malformed = [ + try JSONSchemaDSL.document { Schema.minLength(-1) }, + try JSONSchemaDSL.document { Schema.multipleOf(JSONNumber(integer: 0)) }, + try JSONSchemaDSL.document { Schema.oneOf {} }, + try JSONSchemaDSL.document { Schema.required("same", "same") }, + ] + for declaration in malformed { + do { + _ = try JSONSchemaCompiler().compile(declaration) + Issue.record("Malformed declaration must fail compilation") + } catch let error as JSONSchemaCompilationFailure { + #expect(error.category == .schemaSyntax || error.category == .metaSchema) + } + } + let huge = try JSONNumber(parsing: "1e100") + let schema = try JSONSchemaDSL.document { + Schema.type(.integer) + Schema.minimum(huge) + } + try parity( + schema, #"{"type":"integer","minimum":1e100}"#, valid: ["1e100"], + invalid: ["99999999999999999999999999999999999999999999"]) +} + +@Test func dslV17ContainsAndUnevaluatedAnnotationsRemainEngineOwned() throws { + let schema = try JSONSchemaDSL.document { + Schema.contains { Schema.type(.integer) } + Schema.minContains(1) + Schema.maxContains(2) + Schema.unevaluatedItems(false) + } + try parity( + schema, #"{"contains":{"type":"integer"},"minContains":1,"maxContains":2,"unevaluatedItems":false}"#, + valid: ["[1]", "[1,2]"], invalid: ["[]", "[1,2,3]", #"[1,"x"]"#]) + let plan = try JSONSchemaCompiler().compile(schema) + let report = try JSONSchemaValidator(options: .init(collectAnnotations: true)).validate( + .array([1, 2]), against: plan) + #expect(report.annotations.contains { $0.keyword == JSONKey("contains") && $0.value == .array([0, 1]) }) +} + +@Test func dslV17SuccessfulAnyOfBranchesUnionEvaluatedProperties() throws { + let schema = try JSONSchemaDSL.document { + Schema.anyOf { + Rule { + Schema.properties { Property("a") { Schema.type(.integer) } } + Schema.required("a") + } + Rule { + Schema.properties { Property("b") { Schema.type(.string) } } + Schema.required("b") + } + } + Schema.unevaluatedProperties(false) + } + try parity( + schema, + #"{"anyOf":[{"properties":{"a":{"type":"integer"}},"required":["a"]},{"properties":{"b":{"type":"string"}},"required":["b"]}],"unevaluatedProperties":false}"#, + valid: [#"{"a":1}"#, #"{"b":"yes"}"#, #"{"a":1,"b":"yes"}"#], + invalid: ["{}", #"{"a":1,"b":2}"#, #"{"a":1,"extra":1}"#]) +} + +@Test func dslV20AdditionalKeywordShapeAndScalarLengthParity() throws { + let schema = try JSONSchemaDSL.document { + Schema.types([.string, .null]) + Schema.minLength(2) + Schema.maxLength(3) + Schema.examples(["e\u{301}", .null]) + Schema.readOnly() + Schema.writeOnly(false) + Schema.deprecated(false) + Schema.contentEncoding("base64") + Schema.contentMediaType("text/plain") + Schema.contentSchema { Schema.type(.string) } + } + try parity( + schema, + #"{"type":["string","null"],"minLength":2,"maxLength":3,"examples":["e\u0301",null],"readOnly":true,"writeOnly":false,"deprecated":false,"contentEncoding":"base64","contentMediaType":"text/plain","contentSchema":{"type":"string"}}"#, + valid: ["null", #""e\u0301""#, #""abc""#], invalid: [#""é""#, #""abcd""#, "1"]) +} + +// Draft 2020-12 validation section 6.1.2 recommends nonempty enum arrays but +// does not require them. Preserve the real compiler's empty-set behavior. +@Test func dslEmptyEnumCompilesAndRejectsEveryInstance() throws { + let schema = try JSONSchemaDSL.document { Schema.enumValues([]) } + try parity(schema, #"{"enum":[]}"#, valid: [], invalid: ["null", "true", "1", "[]", "{}", #""text""#]) +} + +@Test func dslNumericObjectAndDependencyFactoriesMatchWrittenVocabulary() throws { + let schema = try JSONSchemaDSL.document { + Schema.type(.object); Schema.minProperties(1); Schema.maxProperties(3) + Schema.propertyNames { Schema.pattern("^[a-z]") } + Schema.properties { + Property("n") { + Schema.type(.integer); Schema.minimum(JSONNumber(integer: 0)); Schema.maximum(JSONNumber(integer: 10)) + Schema.exclusiveMinimum(JSONNumber(integer: 0)); Schema.exclusiveMaximum(JSONNumber(integer: 10)) + Schema.multipleOf(JSONNumber(integer: 3)) + } + Property("flag") { Schema.type(.boolean) } + } + Schema.patternProperties { PatternProperty("^x-") { Schema.type(.string) } } + Schema.dependentRequired { RequiredDependency("n", requires: ["flag"]) } + Schema.dependentSchemas { DependentSchema("flag") { Schema.required("n") } } + Schema.additionalProperties(false) + } + try parity(schema, #"{"type":"object","minProperties":1,"maxProperties":3,"propertyNames":{"pattern":"^[a-z]"},"properties":{"n":{"type":"integer","minimum":0,"maximum":10,"exclusiveMinimum":0,"exclusiveMaximum":10,"multipleOf":3},"flag":{"type":"boolean"}},"patternProperties":{"^x-":{"type":"string"}},"dependentRequired":{"n":["flag"]},"dependentSchemas":{"flag":{"required":["n"]}},"additionalProperties":false}"#, + valid: [#"{"n":3,"flag":false}"#, #"{"n":9,"flag":true,"x-one":"yes"}"#], + invalid: ["{}", #"{"n":3}"#, #"{"flag":true}"#, #"{"n":0,"flag":true}"#, #"{"n":10,"flag":true}"#, #"{"n":3,"flag":true,"x-a":1}"#, #"{"n":3,"flag":true,"other":1}"#, #"{"n":3,"flag":true,"x-a":"a","x-b":"b"}"#]) +} + +@Test func dslSchemaValuedApplicatorsAndRawBooleanEscapePreserveMeaning() throws { + let schema = try JSONSchemaDSL.document { + Schema.type(.array); Schema.minItems(1); Schema.maxItems(3); Schema.uniqueItems(true) + Schema.prefixItems { Rule { Schema.type(.string) } } + Schema.items { Schema.type(.integer) } + Schema.unevaluatedItems { Schema.type(.integer) } + } + try parity(schema, #"{"type":"array","minItems":1,"maxItems":3,"uniqueItems":true,"prefixItems":[{"type":"string"}],"items":{"type":"integer"},"unevaluatedItems":{"type":"integer"}}"#, + valid: [#"["first"]"#, #"["first",1,2]"#], invalid: ["[]", #"["first",1,1]"#, #"["first","second"]"#, #"["first",1,2,3]"#]) + let object = try JSONSchemaDSL.document { Schema.type(.object); Schema.additionalProperties { Schema.type(.integer) }; Schema.unevaluatedProperties { Schema.type(.integer) } } + try parity(object, #"{"type":"object","additionalProperties":{"type":"integer"},"unevaluatedProperties":{"type":"integer"}}"#, + valid: ["{}", #"{"anything":1}"#], invalid: [#"{"anything":"1"}"#]) + let noNames = try JSONSchemaDSL.document { Schema.rawKeyword("propertyNames", .boolean(false)) } + try parity(noNames, #"{"propertyNames":false}"#, valid: ["{}", "null"], invalid: [#"{"name":1}"#]) +} diff --git a/Tests/JSONDSLTests/StressTypeCheckTests.swift b/Tests/JSONDSLTests/StressTypeCheckTests.swift new file mode 100644 index 0000000..a2fa18b --- /dev/null +++ b/Tests/JSONDSLTests/StressTypeCheckTests.swift @@ -0,0 +1,259 @@ +import JSON +import JSONSchema +import JSONSchemaDSL +import JSONValueDSL +// SPDX-License-Identifier: Apache-2.0 +import Testing + +@Suite struct StressTypeCheckTests { + /// Compile-time fixture: 96 heterogeneously placed declarations without + /// fixed-arity buildBlock overloads. No speed claim without proper profiling. + @Test func prototypeWideSchemaAndValueBlocks() throws { + let schema = try JSONSchemaDSL.document { + Schema.type(.object) + Schema.properties { + Property("field001") { Schema.type(.string) } + Property("field002") { Schema.type(.string) } + Property("field003") { Schema.type(.string) } + Property("field004") { + Schema.type(.string) + Schema.minLength(4) + } + Property("field005") { Schema.type(.string) } + Property("field006") { Schema.type(.string) } + Property("field007") { Schema.type(.string) } + Property("field008") { + Schema.type(.string) + Schema.minLength(1) + } + Property("field009") { Schema.type(.string) } + Property("field010") { Schema.type(.string) } + Property("field011") { Schema.type(.string) } + Property("field012") { + Schema.type(.string) + Schema.minLength(5) + } + Property("field013") { Schema.type(.string) } + Property("field014") { Schema.type(.string) } + Property("field015") { Schema.type(.string) } + Property("field016") { + Schema.type(.string) + Schema.minLength(2) + } + Property("field017") { Schema.type(.string) } + Property("field018") { Schema.type(.string) } + Property("field019") { Schema.type(.string) } + Property("field020") { + Schema.type(.string) + Schema.minLength(6) + } + Property("field021") { Schema.type(.string) } + Property("field022") { Schema.type(.string) } + Property("field023") { Schema.type(.string) } + Property("field024") { + Schema.type(.string) + Schema.minLength(3) + } + Property("field025") { Schema.type(.string) } + Property("field026") { Schema.type(.string) } + Property("field027") { Schema.type(.string) } + Property("field028") { + Schema.type(.string) + Schema.minLength(0) + } + Property("field029") { Schema.type(.string) } + Property("field030") { Schema.type(.string) } + Property("field031") { Schema.type(.string) } + Property("field032") { + Schema.type(.string) + Schema.minLength(4) + } + Property("field033") { Schema.type(.string) } + Property("field034") { Schema.type(.string) } + Property("field035") { Schema.type(.string) } + Property("field036") { + Schema.type(.string) + Schema.minLength(1) + } + Property("field037") { Schema.type(.string) } + Property("field038") { Schema.type(.string) } + Property("field039") { Schema.type(.string) } + Property("field040") { + Schema.type(.string) + Schema.minLength(5) + } + Property("field041") { Schema.type(.string) } + Property("field042") { Schema.type(.string) } + Property("field043") { Schema.type(.string) } + Property("field044") { + Schema.type(.string) + Schema.minLength(2) + } + Property("field045") { Schema.type(.string) } + Property("field046") { Schema.type(.string) } + Property("field047") { Schema.type(.string) } + Property("field048") { + Schema.type(.string) + Schema.minLength(6) + } + Property("field049") { Schema.type(.string) } + Property("field050") { Schema.type(.string) } + Property("field051") { Schema.type(.string) } + Property("field052") { + Schema.type(.string) + Schema.minLength(3) + } + Property("field053") { Schema.type(.string) } + Property("field054") { Schema.type(.string) } + Property("field055") { Schema.type(.string) } + Property("field056") { + Schema.type(.string) + Schema.minLength(0) + } + Property("field057") { Schema.type(.string) } + Property("field058") { Schema.type(.string) } + Property("field059") { Schema.type(.string) } + Property("field060") { + Schema.type(.string) + Schema.minLength(4) + } + Property("field061") { Schema.type(.string) } + Property("field062") { Schema.type(.string) } + Property("field063") { Schema.type(.string) } + Property("field064") { + Schema.type(.string) + Schema.minLength(1) + } + Property("field065") { Schema.type(.string) } + Property("field066") { Schema.type(.string) } + Property("field067") { Schema.type(.string) } + Property("field068") { + Schema.type(.string) + Schema.minLength(5) + } + Property("field069") { Schema.type(.string) } + Property("field070") { Schema.type(.string) } + Property("field071") { Schema.type(.string) } + Property("field072") { + Schema.type(.string) + Schema.minLength(2) + } + Property("field073") { Schema.type(.string) } + Property("field074") { Schema.type(.string) } + Property("field075") { Schema.type(.string) } + Property("field076") { + Schema.type(.string) + Schema.minLength(6) + } + Property("field077") { Schema.type(.string) } + Property("field078") { Schema.type(.string) } + Property("field079") { Schema.type(.string) } + Property("field080") { + Schema.type(.string) + Schema.minLength(3) + } + Property("field081") { Schema.type(.string) } + Property("field082") { Schema.type(.string) } + Property("field083") { Schema.type(.string) } + Property("field084") { + Schema.type(.string) + Schema.minLength(0) + } + Property("field085") { Schema.type(.string) } + Property("field086") { Schema.type(.string) } + Property("field087") { Schema.type(.string) } + Property("field088") { + Schema.type(.string) + Schema.minLength(4) + } + Property("field089") { Schema.type(.string) } + Property("field090") { Schema.type(.string) } + Property("field091") { Schema.type(.string) } + Property("field092") { + Schema.type(.string) + Schema.minLength(1) + } + Property("field093") { Schema.type(.string) } + Property("field094") { Schema.type(.string) } + Property("field095") { Schema.type(.string) } + Property("field096") { + Schema.type(.string) + Schema.minLength(5) + } + } + } + #expect((schema.raw.objectValue?["properties"]?.objectValue?.members.count) == (96)) + let doc = try JSONValueDSL.object { + Field("field001", "value1") + Field("field002", "value2") + Field("field003", "value3") + Field("field004", "value4") + Field("field005", "value5") + Field("field006", "value6") + Field("field007", "value7") + Field("field008", "value8") + Field("field009", "value9") + Field("field010", "value10") + Field("field011", "value11") + Field("field012", "value12") + Field("field013", "value13") + Field("field014", "value14") + Field("field015", "value15") + Field("field016", "value16") + Field("field017", "value17") + Field("field018", "value18") + Field("field019", "value19") + Field("field020", "value20") + Field("field021", "value21") + Field("field022", "value22") + Field("field023", "value23") + Field("field024", "value24") + Field("field025", "value25") + Field("field026", "value26") + Field("field027", "value27") + Field("field028", "value28") + Field("field029", "value29") + Field("field030", "value30") + Field("field031", "value31") + Field("field032", "value32") + Field("field033", "value33") + Field("field034", "value34") + Field("field035", "value35") + Field("field036", "value36") + Field("field037", "value37") + Field("field038", "value38") + Field("field039", "value39") + Field("field040", "value40") + Field("field041", "value41") + Field("field042", "value42") + Field("field043", "value43") + Field("field044", "value44") + Field("field045", "value45") + Field("field046", "value46") + Field("field047", "value47") + Field("field048", "value48") + Field("field049", "value49") + Field("field050", "value50") + Field("field051", "value51") + Field("field052", "value52") + Field("field053", "value53") + Field("field054", "value54") + Field("field055", "value55") + Field("field056", "value56") + Field("field057", "value57") + Field("field058", "value58") + Field("field059", "value59") + Field("field060", "value60") + Field("field061", "value61") + Field("field062", "value62") + Field("field063", "value63") + Field("field064", "value64") + } + #expect((doc.objectValue?.members.count) == (64)) + let plan = try JSONSchemaCompiler().compile(schema) + #expect(try JSONSchemaValidator().validate(doc, against: plan).isValid) + #expect( + try !JSONSchemaValidator().validate(JSONParser().parse(#"{"field001":1}"#), against: plan).isValid + ) + } +} From 8fc83cad69df9f28af133c173391ae677804e304 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:53:01 +0200 Subject: [PATCH 02/25] docs: qualify DSL declarations and supporting catalog routes Retain measured variadic composition, record real native and WASI outcomes and the disk blocker, preserve all product-guide checks, and complete prototype test provenance. --- .../Architecture/DualDSL/Decisions.md | 23 +++++++++++---- Documentation/Architecture/DualDSL/Plan.md | 10 +++---- .../Architecture/DualDSL/Progress.json | 29 ++++++++++++++----- .../Architecture/ExtractionManifest.json | 24 +++++++++++++++ Documentation/CI/README.md | 8 ++--- Documentation/DocumentationTooling.md | 7 +++-- Scripts/build_documentation_site.py | 5 +++- 7 files changed, 80 insertions(+), 26 deletions(-) diff --git a/Documentation/Architecture/DualDSL/Decisions.md b/Documentation/Architecture/DualDSL/Decisions.md index c99cd0c..5f3eb74 100644 --- a/Documentation/Architecture/DualDSL/Decisions.md +++ b/Documentation/Architecture/DualDSL/Decisions.md @@ -57,12 +57,23 @@ new Boolean-schema overload aliases are deferred: existing Rule(Boolean), table initializers and rawKeyword preserve the complete declaration without another surface. The identical prototype rawSchemaKeyword synonym is removed in favor of rawKeyword. -## D5 — Compiler strategy (pending measurement) - -The initial integration retains variadic buildBlock and conditional context conformances. -G4 records actual Swift 6.4 timings, diagnostics and memory observations before selecting -it over a pairwise buildPartialBlock alternative. Measurements under concurrent host load -are observations, not a quiet-host or broad compiler-throughput guarantee. +## D5 — Retain measured variadic composition + +Native Swift 6.4 compiled both strategies against the real engine. The final +comparison passed 41 commands, including 22 scaling probes: value/schema blocks +of 16/64/128/256 declarations, nested depth 24, choices/loops and mixed inference. +All six domain-negative fixtures rejected in both configurations, and ordinary +and module-qualified consumers compiled with complete strict concurrency. + +The final 256-property schema typecheck took 3.765 s with variadic composition +versus 5.304 s with pairwise partial blocks; compiler maximum resident sizes were +122,929,152 and 137,789,440 bytes respectively. An earlier integration run measured +1.097 s versus 5.513 s. All raw fixture hashes and measurements are retained; neither +run was quiet (final load averages approximately 600/587/533). Some small branching +fixtures favored partial composition. These observations justify retaining the +working variadic strategy through the tested scale, not a general speed guarantee. +Compiler RSS does not bound DSL staging, finalization retention or process RSS. +The alternative modules remain experiment outputs and are not shipped code. ## D6 — Empty enum is distinct from malformed schema diff --git a/Documentation/Architecture/DualDSL/Plan.md b/Documentation/Architecture/DualDSL/Plan.md index 0b2eb2a..4788e74 100644 --- a/Documentation/Architecture/DualDSL/Plan.md +++ b/Documentation/Architecture/DualDSL/Plan.md @@ -9,9 +9,8 @@ has no standalone product. Root dependencies, platforms and all 49 profiles rema The maintainer-supplied packet SHA-256 is `a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588`. -Its 32-file manifest verified. Only its three DSL source roots and six negative -fixtures are adapted. Neither signature shim is an engine input. Its sixteen -positive cases are ported from XCTest to Swift Testing and real JSONLimits. +Its 32-file manifest verified. Its three DSL source roots and six negative fixtures are adapted. Neither +signature shim is an engine input. Its sixteen positive cases are ported from XCTest to Swift Testing and real JSONLimits. Original and adaptation contributions retain Apache-2.0. ## Gates and reviewable increments @@ -30,8 +29,9 @@ Original and adaptation contributions retain Apache-2.0. 6. **G6 final qualification:** exact-count native/safety/docs and real Linux lanes; public consumer, strict concurrency, source/diff/receipts and remaining limitations. -Each coherent increment is committed locally. No push, PR, merge, tag or publication -is authorized by this integration request. Existing release and sibling worktrees +Each coherent increment is committed on the isolated feature branch. The maintainer +authorized a branch push and existing ARM64/x86_64 Linux CI after native checks. +PR creation, merging, tagging and release publication remain separate. Existing release and sibling worktrees are preserved. A passing shim or typecheck is never a semantic qualification claim. Private input and raw receipts live in ignored `Artifacts/DualDSLP1.2`; source identity, diff --git a/Documentation/Architecture/DualDSL/Progress.json b/Documentation/Architecture/DualDSL/Progress.json index 899b6d9..222b83c 100644 --- a/Documentation/Architecture/DualDSL/Progress.json +++ b/Documentation/Architecture/DualDSL/Progress.json @@ -6,13 +6,26 @@ "branch": "codex/dual-dsl-p1.2", "release_ready": false, "gates": { - "P1.2-G0": "in_progress", - "P1.2-G1": "in_progress", - "P1.2-G2": "in_progress", - "P1.2-G3": "in_progress", - "P1.2-G4": "pending", - "P1.2-G5": "pending", - "P1.2-G6": "pending" + "P1.2-G0": "passed", + "P1.2-G1": "passed", + "P1.2-G2": "passed", + "P1.2-G3": "passed", + "P1.2-G4": "passed", + "P1.2-G5": "in_progress", + "P1.2-G6": "in_progress" }, - "scope": "Trusted authored staging; finite iterative shared-ledger finalization; no staging/RSS/caller-work claim" + "scope": "Trusted authored staging; finite iterative shared-ledger finalization; no staging/RSS/caller-work claim", + "observed": { + "baseline_native_commands": 17, + "dsl_tests_per_configuration": 47, + "native_root_functions": 343, + "baseline_parameter_invocations": 173, + "compiler_commands": 41, + "negative_rejections": 12, + "native_safety": "passed after retained transient PDF failure and unchanged full rerun", + "native_lane": "13 commands passed; downstream mobile tool build blocked by ENOSPC; full lane not passed", + "docs": "1005/1005 comments;9 catalogs passed; route fix privately verified, final lane pending", + "wasi": "real public release consumer passed Node and WasmKit; consumer scope only", + "linux": "authorized branch push/CI pending native completion or explicit changed-condition authorization" + } } diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index f61232c..853ae69 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -236,6 +236,30 @@ "Examples/DualDSL" ], "evidence": "Scripts/ci.py: actual native, safety, docs and supported platform receipts; no shim qualification claim" + }, + { + "repository": "Maintainer-supplied SwiftJSON-DualDSL-Prototypes-P1 packet", + "commit": null, + "source_archive_sha256": "a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588", + "original_path": "Tests/DSLPrototypesTests/DSLPrototypeTests.swift", + "original_sha256": "57ea84d6729740abb326819a316cd72cd756aec2e4c7990a5b9bb2f61000f2b8", + "destination": "Tests/JSONDSLTests/DSLPrototypeTests.swift", + "disposition": "Ported all16 XCTest prototype cases to Swift Testing and actual JSONLimits; full schemas also compile and evaluate actual positive/negative or explicitly operational outcomes. No shim is used.", + "attribution": "Apache-2.0 prototype SPDX retained; original integration cases extend actual-engine qualification.", + "validation": "Scripts/ci.py --lane dsl; native and safety exact discovery", + "evidence": "Actual-engine Debug/Release/ASan receipts and real public consumer" + }, + { + "repository": "Maintainer-supplied SwiftJSON-DualDSL-Prototypes-P1 packet", + "commit": null, + "source_archive_sha256": "a69432b69f59d3d587502c2453fed3d00259845b9335a551b962965be10f9588", + "original_path": "Tests/DSLPrototypesTests/StressTypeCheckTests.swift", + "original_sha256": "56a803836cb115e84c35abe65c15a3800e082e11593096a4143b73f76f3ff98c", + "destination": "Tests/JSONDSLTests/StressTypeCheckTests.swift", + "disposition": "Ported all16 XCTest prototype cases to Swift Testing and actual JSONLimits; full schemas also compile and evaluate actual positive/negative or explicitly operational outcomes. No shim is used.", + "attribution": "Apache-2.0 prototype SPDX retained; original integration cases extend actual-engine qualification.", + "validation": "Scripts/ci.py --lane dsl; native and safety exact discovery", + "evidence": "Actual-engine Debug/Release/ASan receipts and real public consumer" } ], "original_implementation_notes": [ diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index b533eef..dda7b95 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -63,7 +63,7 @@ distribution, architecture or libc claims. `python3 Scripts/ci.py --lane wasm` requires the matching Swift.org 6.4.0 compiler, SDK and WasmKit plus Node. It records cross-build and runtime outcomes separately. -Node executes the complete 256-function/173-parameter portable root suites, +Node executes the current 329-function/173-parameter portable root suites, independent corpus policies and all 49 cap outcomes. Both hosts execute the frozen public consumer; WasmKit retains its separately recorded 14-function consumer scope. The [WASI contract](../Architecture/WebAssembly.md) defines host/safety scopes. @@ -77,9 +77,9 @@ Apple SDK declaration probes. It builds all five products in iPhoneOS and simulator consumers. A bounded offscreen WebPage region experiment remains outside the production API. The lane establishes native API/file transport and SDK builds, not share sheets or receiving-editor UI. Explicit paginated PDF and simulator/device execution have separate materialized receipts. -Current root discovery is 270 functions on Mac (75 HTML) and 256 on Linux/WASI -(61 HTML). The focused export lane requires 38 functions on Mac. Ten Mac shared -functions, including independent PDF cancellation, run under Thread Sanitizer. All 49 cap checks +Current root discovery is 343 functions on Mac (75 HTML, 47 DSL) and 329 on Linux/WASI +(61 HTML, 47 DSL). The focused export lane requires 38 functions on Mac. Thirteen Mac shared +functions, including independent PDF cancellation and DSL finalization, run under Thread Sanitizer. All 49 cap checks and the 173 baseline parameterized invocations remain required. ## Structured Swift Testing receipts diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index ffe2a07..d7d5c04 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -1,8 +1,8 @@ # Building and maintaining the DocC catalogs Swift source comments are the API documentation authority. Native compiler symbol -graphs supply the declarations and links for eight public-product Swift DocC catalogs plus a shared DSL structure reference. Each catalog -combines the product reference with curated Topics and a focused usage guide. +graphs supply the declarations and links for eight public-product Swift DocC catalogs plus a shared DSL structure reference. Each product catalog combines its reference with curated Topics and a focused +usage guide; the supporting DSL catalog documents shared structural declarations. See the [Swift DocC documentation](https://www.swift.org/documentation/docc/). ## Build locally @@ -30,6 +30,9 @@ symbols, missing comments, stale examples or a failed catalog fail the lane. | `.build/handoff/docc/JSONHTML.doccarchive` | Exact formatted HTML API and rendering guide | | `.build/handoff/docc/JSONPatch.doccarchive` | Patch API and choosing/applying patches guide | | `.build/handoff/docc/JSONBridges.doccarchive` | Opt-in native numeric conversion and fidelity guide | +| `.build/handoff/docc/JSONValueDSL.doccarchive` | Trusted value authoring API and guide | +| `.build/handoff/docc/JSONSchemaDSL.doccarchive` | Trusted schema authoring API and guide | +| `.build/handoff/docc/JSONDSLCore.doccarchive` | Shared structural reference; no standalone product | | `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | | `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | | `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index c55d5a1..1485755 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -188,7 +188,10 @@ def main(): receipt['transforms'].append(entry) if result.returncode: raise ValueError('Native static-hosting transformation failed: ' + product) - for route in [product.lower(), 'documentation/' + GUIDES[product][1]]: + routes = [product.lower()] + if product in PRODUCTS: + routes.append('documentation/' + GUIDES[product][1]) + for route in routes: if not (site / product / 'documentation' / route / 'index.html').is_file(): raise ValueError('Missing product root or guide: ' + route) # DocC transforms its emitted routes but retains the original archive From 345d4b11cd8e4f00fe53063f9f1d1e13030aa3ea Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:00:13 +0200 Subject: [PATCH 03/25] docs: record actual DSL native qualification and recovery Record all production command outcomes, complete sanitizer rerun, corpus and cap counts, strict DocC and actual completion evidence. Keep Linux pending and authored staging limitations explicit. --- .../Architecture/DualDSL/Progress.json | 12 +-- .../Qualification/DualDSL/NativeResults.md | 82 +++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 Documentation/Qualification/DualDSL/NativeResults.md diff --git a/Documentation/Architecture/DualDSL/Progress.json b/Documentation/Architecture/DualDSL/Progress.json index 222b83c..162d737 100644 --- a/Documentation/Architecture/DualDSL/Progress.json +++ b/Documentation/Architecture/DualDSL/Progress.json @@ -11,7 +11,7 @@ "P1.2-G2": "passed", "P1.2-G3": "passed", "P1.2-G4": "passed", - "P1.2-G5": "in_progress", + "P1.2-G5": "passed", "P1.2-G6": "in_progress" }, "scope": "Trusted authored staging; finite iterative shared-ledger finalization; no staging/RSS/caller-work claim", @@ -23,9 +23,11 @@ "compiler_commands": 41, "negative_rejections": 12, "native_safety": "passed after retained transient PDF failure and unchanged full rerun", - "native_lane": "13 commands passed; downstream mobile tool build blocked by ENOSPC; full lane not passed", - "docs": "1005/1005 comments;9 catalogs passed; route fix privately verified, final lane pending", + "native_lane": "All19 production commands passed across retained13-command run plus6-command source-equivalent continuation after ENOSPC; no uninterrupted full-lane exit0 claimed", + "docs": "passed:1005/1005 comments,9 catalogs,1530 routes,7636 links", "wasi": "real public release consumer passed Node and WasmKit; consumer scope only", - "linux": "authorized branch push/CI pending native completion or explicit changed-condition authorization" - } + "linux": "maintainer-authorized feature-branch push and ARM64/x86_64 CI pending execution", + "completion": "passed:3 actual SourceKit-LSP contexts; interactive Xcode UI not claimed" + }, + "native_results": "Documentation/Qualification/DualDSL/NativeResults.md" } diff --git a/Documentation/Qualification/DualDSL/NativeResults.md b/Documentation/Qualification/DualDSL/NativeResults.md new file mode 100644 index 0000000..131f24e --- /dev/null +++ b/Documentation/Qualification/DualDSL/NativeResults.md @@ -0,0 +1,82 @@ +# Dual DSL P1.2 native qualification + +This unreleased integration uses the actual engine, not the packet's signature shims. +Base: `e8c3e4bd9ae40d797f07a527ea98879acee3c057`; implementation: +`917dab492883178b0ea9b57bce3a526b597e6373`; documentation correction: +`8fc83cad69df9f28af133c173391ae677804e304`. Library, manifest and test sources +are identical between the latter two commits. The intervening changes are records, +provenance and documentation tooling. Root dependencies remain empty; existing six +products gain no dependency on the two optional DSL products. + +## Observed native results + +macOS 27.0 arm64; Xcode 27.2 (27B5019j), SDK 27.2; Apple Swift 6.4 +(swiftlang-6.4.0.34.1), Swift language mode 6. Commands select the matching Xcode +and SDK explicitly; no system toolchain setting or baseline is changed. + +- Untouched baseline: all 17 production native commands passed, 296 functions + per configuration and 173 parameter invocations. +- Integration: all 47 DSL Swift Testing functions passed Debug and Release. + Full root Debug, Release and Address Sanitizer: 343 functions in eight exact + bundles, retaining all 173 parameter invocations. Shared-state Thread Sanitizer: + 13 functions. Strict memory safety compilation with warnings as errors passed. +- Production native commands 0–12 passed. ENOSPC then interrupted the downstream + mobile-tool build and receipt writer. The exact remaining six commands passed + in a source-equivalent continuation with serial builds and cleanup of completed + task-owned tool caches. This is 19 individually passed commands across retained + receipts, not a single uninterrupted native-lane exit-zero claim. +- Mobile tool: three behavior tests, eight public fixtures, awaited cancellation + and three separately exited disk persistence phases with eight rows. This is + native SDK/tool execution, not a new physical-device qualification claim. +- Rooted consumer: 20 explicitly selected cases, eight positive/twelve negative + and two operational checks. Independent syntax: 318 observed, 287 evaluated, + 31 policy. Schema: 2337 observed, 2300 evaluated, 37 policy. Authored cases: 82; + budget cases: nine; deterministic malformed replay: 4396. All 49 cap probes + passed. Policy cases are not evaluated successes. +- Six compiler-negative fixtures rejected for intended domain typing in Debug + and Release. The final compiler experiment passed 41 commands and two guide + fences, including 22 measured strategy/fixture combinations. Qualified-name + collision consumers passed. See [the compiler decision](../../Architecture/DualDSL/Decisions.md). +- Separate public DSL consumer passed Debug/Release: one valid, two ordinary + invalid and one missing-resource operational outcome, exact scalar keys and + nested arrays. SourceKit-LSP returned the required value/schema factories, + qualified types and Schema keywords in three real completion contexts. No + interactive Xcode UI session is claimed. +- Documentation lane passed: 1005/1005 authored public declarations with named + parameters, nine strict DocC catalogs, compiled README examples, 1530 static + routes and 7636 verified local links. The helper catalog has no public product; + all eight product guides remain mandatory. +- Matching Swift.org 6.4 WASI SDK: the real public release consumer built and + passed in Node and WasmKit. This is consumer scope, not a new full WASI or + host-sanitizer claim. Linux execution remains a separately recorded gate. + +The first full ASan run recorded an unchanged PDF cancellation test receiving +`JSONPDFFailure.timedOut` rather than cancellation on a heavily loaded host. The +same case passed independently without edits, followed by a complete passing +safety rerun. Failed receipts remain part of the private return evidence. The +initial SDK mismatch, missing-comment diagnostics, site-route assumption and +concurrent SwiftPM manifest-output contamination are retained with their fixes; +no failure is relabeled as a pass or policy exclusion. + +## Public additions and semantic authority + +| Addition | Authority and regression coverage | +| --- | --- | +| `JSONValueDSL.object/array`, Field, Element, Elements | JSON/JSONNumber, exact JSONObject admission/index; V01–V07, duplicate tables, literal caps and public consumer | +| `JSONSchemaDSL.document/boolean`, Schema keyword factories and typed tables | JSONSchemaDocument, existing compiler/validator and explicit registry; V08–V17/V20, full prototype examples, annotation/verdict/manual-raw parity | +| JSONDSLCore typed composition and flat node/member staging | Domain-constrained native Swift composition; six negative fixtures, choices/loops/nesting, 16–256 declaration probes | +| Iterative finalization and package schema continuation | Existing invocation-owned engine meter and admission; V18/V19, inclusive work/retention/node caps, terminal error, depth and cancellation tests | +| Immutable declarations and conditional Sendable | Native compiler, 32 independent task invocations, shared immutable plan and exact shared-state TSan discovery | + +Trusted callbacks, wrapper construction, emission and IR staging remain unmetered. +There is no whole-DSL cumulative report, staging cancellation/retention guarantee, +RSS bound or caller-instruction quota. Finalization uses one finite ledger through +schema document admission. Compiler/evaluator invocations remain separate explicit +engine operations. The next architecture question is whether an additional bounded +dynamic-authoring API merits metering from the first staged append; it is not +silently added to this authored API. + +Runtime throughput is not benchmarked by the compiler experiment. Its loaded-host +compiler elapsed time/RSS observations justify the retained strategy only through +the tested scale. Projection, text-sequence framing, extra numeric coercions, online +resolution and new schema semantics remain outside this wave. From 6f6bd90467aae0354dc5dcfdc7e125a5f6e8d2de Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:13:26 +0200 Subject: [PATCH 04/25] ci: verify the exact Linux container checkout identity Trust only GITHUB_WORKSPACE in the ephemeral container and assert its HEAD matches GITHUB_SHA before qualification. Preserve metadata failures and all native, corpus, cap, privacy and compiler checks. --- .github/workflows/linux.yml | 6 ++++++ Documentation/Architecture/DualDSL/Progress.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 5324728..729b003 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -40,6 +40,12 @@ jobs: with: persist-credentials: false fetch-depth: 0 + - name: Verify the checked-out container workspace + run: | + # The image user differs from the checkout owner. Trust this exact + # ephemeral workspace so Git identity and full-history checks run. + git config --global --add safe.directory "$GITHUB_WORKSPACE" + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" - run: python3 -B Scripts/ci.py --lane '${{ matrix.lane }}' - name: Preserve Linux receipts if: always() diff --git a/Documentation/Architecture/DualDSL/Progress.json b/Documentation/Architecture/DualDSL/Progress.json index 162d737..0978217 100644 --- a/Documentation/Architecture/DualDSL/Progress.json +++ b/Documentation/Architecture/DualDSL/Progress.json @@ -26,7 +26,7 @@ "native_lane": "All19 production commands passed across retained13-command run plus6-command source-equivalent continuation after ENOSPC; no uninterrupted full-lane exit0 claimed", "docs": "passed:1005/1005 comments,9 catalogs,1530 routes,7636 links", "wasi": "real public release consumer passed Node and WasmKit; consumer scope only", - "linux": "maintainer-authorized feature-branch push and ARM64/x86_64 CI pending execution", + "linux": "run38679209482:both safety lanes passed; both full lanes passed9 commands then failed Git ownership metadata before DSL compiler imports. Narrow exact-checkout trust/SHA verification added to ephemeral workflow; rerun pending.", "completion": "passed:3 actual SourceKit-LSP contexts; interactive Xcode UI not claimed" }, "native_results": "Documentation/Qualification/DualDSL/NativeResults.md" From ed08ed4b397cf84e05084dc325b4b7232da3358b Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 06:33:25 +0200 Subject: [PATCH 05/25] docs: close DSL qualification with verified portable evidence Record both Linux architectures full and safety outcomes, byte-identical clean reproduction, actual WASI DSL tests and Apple SDK builds. Preserve failed attempts and explicit staging/platform scopes; keep merging and release publication separate. --- .../Architecture/DualDSL/Progress.json | 21 ++++++-- .../Qualification/DualDSL/LinuxResults.md | 51 +++++++++++++++++++ .../Qualification/DualDSL/NativeResults.md | 8 ++- 3 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 Documentation/Qualification/DualDSL/LinuxResults.md diff --git a/Documentation/Architecture/DualDSL/Progress.json b/Documentation/Architecture/DualDSL/Progress.json index 0978217..11322c4 100644 --- a/Documentation/Architecture/DualDSL/Progress.json +++ b/Documentation/Architecture/DualDSL/Progress.json @@ -12,7 +12,7 @@ "P1.2-G3": "passed", "P1.2-G4": "passed", "P1.2-G5": "passed", - "P1.2-G6": "in_progress" + "P1.2-G6": "passed" }, "scope": "Trusted authored staging; finite iterative shared-ledger finalization; no staging/RSS/caller-work claim", "observed": { @@ -25,9 +25,20 @@ "native_safety": "passed after retained transient PDF failure and unchanged full rerun", "native_lane": "All19 production commands passed across retained13-command run plus6-command source-equivalent continuation after ENOSPC; no uninterrupted full-lane exit0 claimed", "docs": "passed:1005/1005 comments,9 catalogs,1530 routes,7636 links", - "wasi": "real public release consumer passed Node and WasmKit; consumer scope only", - "linux": "run38679209482:both safety lanes passed; both full lanes passed9 commands then failed Git ownership metadata before DSL compiler imports. Narrow exact-checkout trust/SHA verification added to ephemeral workflow; rerun pending.", - "completion": "passed:3 actual SourceKit-LSP contexts; interactive Xcode UI not claimed" + "wasi": "passed:47 new DSL tests Debug/Release in Node; portable root bundles compiled; public release consumer Node/WasmKit. No new execution of other root corpora or host sanitizers claimed", + "linux": "passed:run38680343090 at6f6bd90467aae0354dc5dcfdc7e125a5f6e8d2de; ARM64/x86_64 full16 and safety4 commands each;329 root functions/173 parameters,12 portable TSan; actual receipts and identical archive bytes verified", + "completion": "passed:3 actual SourceKit-LSP contexts; interactive Xcode UI not claimed", + "apple_sdk": "passed:iPhoneOS/iPhoneSimulator27.2 public DSL consumer builds at existing minimum27; SDK-only scope" }, - "native_results": "Documentation/Qualification/DualDSL/NativeResults.md" + "native_results": "Documentation/Qualification/DualDSL/NativeResults.md", + "qualification_status": "passed", + "release_readiness_reason": "Qualified feature candidate; maintainer API/merge and release authorization remain separate, not a failed technical gate", + "qualified_ci_source": "6f6bd90467aae0354dc5dcfdc7e125a5f6e8d2de", + "linux_results": "Documentation/Qualification/DualDSL/LinuxResults.md", + "remaining_limitations": [ + "Trusted authored staging/callbacks excluded from finalization work and retention accounting", + "No whole-DSL report, caller instruction quota or hard process RSS/throughput guarantee", + "Interactive Xcode UI and new physical-device execution not claimed", + "All other WASI corpus/sanitizer execution remains its separately qualified existing scope" + ] } diff --git a/Documentation/Qualification/DualDSL/LinuxResults.md b/Documentation/Qualification/DualDSL/LinuxResults.md new file mode 100644 index 0000000..8385b96 --- /dev/null +++ b/Documentation/Qualification/DualDSL/LinuxResults.md @@ -0,0 +1,51 @@ +# Dual DSL P1.2 Linux qualification + +[Recovery run 38680343090](https://github.com/ThorFuchs/Swift-JSON/actions/runs/38680343090) +passed all four jobs at `6f6bd90467aae0354dc5dcfdc7e125a5f6e8d2de`. +Both ARM64 and x86_64 used Ubuntu 24.04, Swift 6.4.0 and the unchanged official +multi-architecture image pin +`swift@sha256:31d14d727f4451f25e29bac9c42563a098c39ac66415e8bad970f48038e38aca`. +The actual environment and every command/log digest are in the downloaded receipts. + +Each architecture passed 16 full qualification commands and four safety commands. +Debug, Release and ASan each required eight exact bundles, 329 functions and all +173 parameter invocations. Shared-state TSan required 12 portable functions; +strict-memory-safety compilation passed. Six intended compiler negatives rejected +in each of two configurations, with public positive and qualified-name collision +consumers. The separate public DSL consumer passed real valid/invalid/operational +outcomes. All 49 cap probes and unchanged independent corpus policies passed: +318 syntax cases (287 evaluated/31 policy), 2337 schema cases (2300 evaluated/37 +policy), 82 authored, nine budget and 4396 malformed replay cases. Policy cases +are not evaluated successes, and operational errors are not false verdicts. + +Both jobs independently rebuilt identical candidate bytes, verified the manifest, +extracted without Git metadata, built all products, ran six integrity/resource +functions and executed the public consumer with compiled README DSL examples. +Archive SHA-256 is +`3f00c91f205bbafb9c722e6bb5d5c0f4ba9996c359f046a16d12bac91699e547`, +1,352,635 bytes, 931 entries including its 930-file source manifest. This matches +the local immutable source archive byte-for-byte. Every job's source-input map +matches that archive, with unchanged-source assertions and actual Git SHA recorded. + +[First run 38679209482](https://github.com/ThorFuchs/Swift-JSON/actions/runs/38679209482) +is retained as failed. Its two safety jobs passed; its full lanes passed nine +commands then stopped before the DSL compiler probes when Git rejected container +checkout ownership. The recovery trusts only the exact ephemeral GITHUB_WORKSPACE +and asserts HEAD equals GITHUB_SHA before qualification. No wildcard trust, host +configuration or test relaxation was added. Git identity and full privacy/history +checks now execute, rather than being skipped for an unreadable Git checkout. + +The native results remain separately qualified, including the transparent ENOSPC +continuation and the retained PDF timeout followed by an unchanged complete passing +safety rerun. No Apple/device claim is inferred from Linux. Additional iPhoneOS and +iPhoneSimulator 27.2 SDK builds (minimum 27) passed the public DSL consumer. Matching +Swift 6.4 WASI builds compiled portable root bundles, and all 47 new DSL tests passed +Debug and Release in Node; the public release consumer also passed WasmKit. These +additional scopes do not imply a new full WASI corpus/sanitizer or physical-device run. + +Only reporting/qualification records may follow this tested source identity in the +freeze commit. Library, test, manifest, consumer and qualification-harness source +are unchanged since the implementation commit. The workflow change is confined +to exact container checkout identity verification; protected checks and release +publication remain separate. Trusted unmetered authoring and finite finalization +retain their explicit resource boundary. diff --git a/Documentation/Qualification/DualDSL/NativeResults.md b/Documentation/Qualification/DualDSL/NativeResults.md index 131f24e..2c2f379 100644 --- a/Documentation/Qualification/DualDSL/NativeResults.md +++ b/Documentation/Qualification/DualDSL/NativeResults.md @@ -48,7 +48,7 @@ and SDK explicitly; no system toolchain setting or baseline is changed. all eight product guides remain mandatory. - Matching Swift.org 6.4 WASI SDK: the real public release consumer built and passed in Node and WasmKit. This is consumer scope, not a new full WASI or - host-sanitizer claim. Linux execution remains a separately recorded gate. + host-sanitizer claim. Linux execution is recorded in [the separate results](LinuxResults.md). The first full ASan run recorded an unchanged PDF cancellation test receiving `JSONPDFFailure.timedOut` rather than cancellation on a heavily loaded host. The @@ -80,3 +80,9 @@ Runtime throughput is not benchmarked by the compiler experiment. Its loaded-hos compiler elapsed time/RSS observations justify the retained strategy only through the tested scale. Projection, text-sequence framing, extra numeric coercions, online resolution and new schema semantics remain outside this wave. + +The final portable follow-up also executed all 47 new DSL functions in both +configurations in Node, after compiling the portable root bundles. It preserves +the limited WASI scope above for other suites and host sanitizers. Real public DSL +consumer cross-builds passed iPhoneOS/iPhoneSimulator 27.2 SDKs at minimum iOS 27; +these are build observations, separate from the native persistence tool and devices. From a2cf3c494eda7ac919c9544797776d8d427dfb57 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:28:02 +0200 Subject: [PATCH 06/25] docs: align DSL release and catalogue inventory --- README.md | 14 ++++++++------ RELEASING.md | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a370dee..9288be4 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ Choose the products your application needs: SwiftPM dependency graph is empty. Qualification and benchmark tools are separate packages. -The Linux development increment adds portable schema integrity for all four products -without runtime dependencies. Its initial qualification scope is Swift 6.4+, +Linux support includes portable schema integrity without runtime dependencies. +Its qualification scope is Swift 6.4+, 64-bit Ubuntu 24.04/glibc; see the [Linux contract](Documentation/Architecture/Linux.md) for execution gates and limits. Apple platform requirements remain unchanged. @@ -497,11 +497,13 @@ Browse the [public documentation site](https://thorfuchs.github.io/Swift-JSON/): [JSONPatch](https://thorfuchs.github.io/Swift-JSON/JSONPatch/documentation/jsonpatch/). The site tracks `main` and publishes only after all three native CI lanes pass. -The [documentation index](Documentation/README.md) links the behavior contracts. The five DocC catalogs -provide public API references, focused guides, parameter descriptions and failure -contracts, compiled from the native Swift public symbol graphs. +The [documentation index](Documentation/README.md) links the behavior contracts. +Eight product DocC catalogues provide public API references, focused guides, +parameter descriptions and failure contracts, compiled from native Swift public +symbol graphs. A ninth catalogue documents shared DSL composition types; its +helper module has no standalone product. -Build all five catalogs on a supported native host: +Build all nine catalogues on a supported native host: ```sh python3 Scripts/ci.py --lane docs diff --git a/RELEASING.md b/RELEASING.md index 6cb011b..a0a8b7d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -6,7 +6,8 @@ or releases. Published version tags are protected against updates and deletion. 1. Require passed native, safety and documentation lanes for the exact candidate commit on macOS/SDK 27+ and Swift 6.4+ with language mode 6. -2. Review six public products (including JSONHTML and opt-in JSONBridges), the empty root dependency graph, compiled external +2. Review eight public products (including JSONHTML, opt-in JSONBridges and the + two opt-in DSLs), the empty root dependency graph, compiled external consumers, populated pinned suites, policy exclusions, all 49 finite caps, schema/regex behavior and cancellation boundaries. 3. Review licenses, provenance, public file inventory and documentation links. From 64264218dc0073a5738ab557bfa25b3bb9f6fa7a Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:30:55 +0200 Subject: [PATCH 07/25] docs: clarify supporting DSL catalogue route --- Documentation/DocumentationTooling.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index d7d5c04..cc4695f 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -67,7 +67,9 @@ The manifest and receipt preserve file-level and archive-level digests. The docs lane uses native `docc process-archive transform-for-static-hosting` to prepare each qualified archive under `/Swift-JSON/`. A shared landing -page links eight public-product references and the supporting DSL reference and guides. The build verifies product/guide routes, +page links all eight product references and guides. The supporting DSL reference is +also deployed at `/Swift-JSON/JSONDSLCore/documentation/jsondslcore/`, without a +product card. The build verifies product/guide and supporting-reference routes, local HTML links and assets, rejects private-path/credential markers and symlinks, and records a file manifest. Only the generated site is packaged for Pages; source graphs, command logs and private maintainer records are not website content. From 971377a6267794e80edfdc0ca9bd902bd7bd92bc Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:28:23 +0200 Subject: [PATCH 08/25] Prototype scoped public JSONTransform over the shared query kernel --- .../Architecture/Release1.7/JSONTransform.md | 47 ++++++ .../Qualification/TestDiscovery.json | 18 ++- Package.swift | 5 +- Scripts/ci.py | 10 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- Sources/JSONCLIEngine/CLIErrorExit.swift | 16 ++ Sources/JSONCLIEngine/CLIQueryCommand.swift | 5 +- .../JSONFilterRuntime/CLICompatibility.swift | 9 ++ .../JSONFilterRuntime/DecimalMagnitude.swift | 52 +++--- Sources/JSONFilterRuntime/ExactDecimal.swift | 24 +-- .../FilterBindingPattern.swift | 4 +- .../FilterCollectionBuiltins.swift | 8 +- .../JSONFilterRuntime/FilterCompiler.swift | 10 +- .../JSONFilterRuntime/FilterContainment.swift | 8 +- Sources/JSONFilterRuntime/FilterFormats.swift | 12 +- Sources/JSONFilterRuntime/FilterLexer.swift | 6 +- .../JSONFilterRuntime/FilterLocation.swift | 12 +- .../JSONFilterRuntime/FilterOperators.swift | 18 +-- Sources/JSONFilterRuntime/FilterPaths.swift | 18 +-- .../FilterPrimitiveBuiltins.swift | 2 +- .../FilterStringBuiltins.swift | 18 +-- Sources/JSONFilterRuntime/FilterVM.swift | 76 ++++----- Sources/JSONFilterRuntime/FilterValues.swift | 24 +-- Sources/JSONFilterRuntime/RangeState.swift | 8 +- ...xt.swift => TransformRuntimeContext.swift} | 61 +++---- ...eLease.swift => TransformValueLease.swift} | 16 +- .../JSONTransformExecution.swift | 140 ++++++++++++++++ .../JSONTransform/JSONTransformFailure.swift | 69 ++++++++ .../JSONTransform/JSONTransformLimits.swift | 67 ++++++++ .../JSONTransform/JSONTransformProgram.swift | 79 +++++++++ .../TransformSpikeTests.swift | 152 ++++++++++++++++++ Tools/TransformQualification/Package.swift | 9 ++ Tools/TransformQualification/README.md | 11 ++ .../Sources/TransformConsumer/Main.swift | 56 +++++++ 34 files changed, 865 insertions(+), 207 deletions(-) create mode 100644 Documentation/Architecture/Release1.7/JSONTransform.md create mode 100644 Sources/JSONCLIEngine/CLIErrorExit.swift create mode 100644 Sources/JSONFilterRuntime/CLICompatibility.swift rename Sources/JSONFilterRuntime/{CLIJobContext.swift => TransformRuntimeContext.swift} (73%) rename Sources/JSONFilterRuntime/{CLIValueLease.swift => TransformValueLease.swift} (76%) create mode 100644 Sources/JSONTransform/JSONTransformExecution.swift create mode 100644 Sources/JSONTransform/JSONTransformFailure.swift create mode 100644 Sources/JSONTransform/JSONTransformLimits.swift create mode 100644 Sources/JSONTransform/JSONTransformProgram.swift create mode 100644 Tests/JSONTransformTests/TransformSpikeTests.swift create mode 100644 Tools/TransformQualification/Package.swift create mode 100644 Tools/TransformQualification/README.md create mode 100644 Tools/TransformQualification/Sources/TransformConsumer/Main.swift diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md new file mode 100644 index 0000000..2109793 --- /dev/null +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -0,0 +1,47 @@ +# JSONTransform extraction checkpoint + +This feature-branch API is provisional. Stable 1.6.1 and development main remain +separate authorities. This source adds JSONTransform to the nine development +libraries; it does not remove JSONInspection or import another candidate. + +The chosen spike uses one private JSONFilterRuntime kernel. JSONTransform owns an +opaque Sendable `JSONTransformProgram` and scoped noncopyable +`JSONTransformExecution`. An execution pulls `JSONTransformOutput?`: nil means EOF, +while a present handle may contain JSON null. Handles share source leases and are +not Sendable. JSON extracted from a handle is caller-owned; the scope's logical +accounting is not RSS and does not meter arbitrary callbacks. + +`compile(_:variables:limits:)` has its own finite compilation ledger. The plan +conservatively reserves its compiler-retained bytes in each standalone execution. +`withExecution(input:bindings:limits:source:_:)` owns one cumulative native meter +across admission, every pull, provider value and result. It returns a native +operation report. `used(_:)`, `statistics` and `remainingWorkUnits` expose counters +without exporting a meter. `cancel()` latches an operational failure. Catching a +failed pull cannot make scope completion successful. Provider EOF is distinct from +JSON null; callback work and upstream allocations remain caller-owned. + +For CLI composition the same program offers package-only `compileAccounted` and +`makeAccountedExecution` adapters borrowing the existing noncopyable operation +context. They invoke the same compiler and VM. There is no fresh per-record meter +and no second evaluator. The host still owns files, framing, source draining, +output, process statuses and the sixteen original CLI metric names. All 49 core +metrics remain unchanged. Package-only CLI aliases preserve existing host tests +and ownership probes during migration; these are not public API. + +Option B (one common private operation scope) is selected for the spike over a +second transform sub-ledger or per-next meters. Borrowing only the raw native +meter would also need to carry live frame/binding/collection counters and the +refund queue; splitting them risks two health/ownership authorities. The current +common scope preserves those invariants. Host metadata still stored in that +private context needs further ownership review before final API freeze. + +Swift 6.4 rejected a throwing initializer that conditionally initialized a +noncopyable owner across a catch. A factory now fully initializes the owner before +fallible preparation. No unsafe annotations or compiler flags were introduced. +A standalone external consumer in Tools/TransformQualification and eleven Swift +Testing functions exercise the actual engine. The private execution receipts bind +their exact source revisions; a working spike is not full release qualification. + +JTX-2 through JTX-7 remain separate qualification gates. No merge, tag, publication, +Linux/WASI runtime support, complete conformance, controlled performance or final +API acceptance follows from this checkpoint. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 6f5fc8a..1c0679f 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,8 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 76, + "JSONTransformTests": 11 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -35,7 +36,8 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 4, + "JSONTransformTests": 1 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -62,7 +64,8 @@ "JSONCLIEngine", "JSONCLI", "JSONNavigation", - "JSONInspection" + "JSONInspection", + "JSONTransform" ], "portable_root_functions": { "JSONTests": 190, @@ -76,7 +79,8 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 76, + "JSONTransformTests": 11 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,7 +93,8 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 76, + "JSONTransformTests": 11 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -103,7 +108,8 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 4, + "JSONTransformTests": 1 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/Package.swift b/Package.swift index dcdd32c..d0339ed 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,7 @@ let package = Package( .library(name: "JSONBridges", targets: ["JSONBridges"]), .library(name: "JSONNavigation", targets: ["JSONNavigation"]), .library(name: "JSONInspection", targets: ["JSONInspection"]), + .library(name: "JSONTransform", targets: ["JSONTransform"]), .executable(name: "swift-json", targets: ["JSONCLI"]) ], targets: [ @@ -35,7 +36,9 @@ let package = Package( .target(name: "JSONPatch", dependencies: ["JSON"]), .target(name: "JSONHTML", dependencies: ["JSON"], exclude: ["Assets"]), .target(name: "JSONFilterRuntime", dependencies: ["JSON"]), - .target(name: "JSONCLIEngine", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch", "JSONHTML", "JSONFilterRuntime"]), + .target(name: "JSONTransform", dependencies: ["JSON", "JSONFilterRuntime"]), + .testTarget(name: "JSONTransformTests", dependencies: ["JSON", "JSONTransform"]), + .target(name: "JSONCLIEngine", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch", "JSONHTML", "JSONFilterRuntime", "JSONTransform"]), .executableTarget(name: "JSONCLI", dependencies: ["JSONCLIEngine"]), .testTarget(name: "JSONCLITests", dependencies: ["JSONCLIEngine", "JSONFilterRuntime", "JSON", "JSONPatch", "JSONSchema", "JSONHTML"]), .testTarget(name: "JSONHTMLTests", dependencies: ["JSONHTML", "JSON"], resources: [.copy("Fixtures")]), diff --git a/Scripts/ci.py b/Scripts/ci.py index 096ea07..a0ce7f4 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -16,6 +16,7 @@ SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' +SHARED_TESTS += '|jtxPlanIsSendableAndExecutionsHaveIndependentHealth' SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' @@ -134,7 +135,7 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_html_browser.py', '--fixtures', str(output / 'fixtures'), '--output', str(output / 'browser')], ] if lane == 'cli': - return [['swift', 'build', '-c', 'release', '--product', 'swift-json'], ['swift', 'test', '--filter', 'cli|sharedFilterPlan'], ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')], + return [['swift', 'build', '-c', 'release', '--product', 'swift-json'], ['swift', 'test', '--test-product', 'JSONCLITests', '--filter', 'cli|sharedFilterPlan'], ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')], ['python3', '-B', 'Scripts/qualify_cli_integration.py', '--binary', '.build/release/swift-json', '--output', str(output / 'integration-subprocess.json')], ['python3', '-B', 'Scripts/qualify_cli_limits.py', '--binary', '.build/release/swift-json', '--output', str(output / 'cli-limits.json')], ['swift', 'build', '-c', 'release', '--package-path', 'Tools/CLIQualification', '--product', 'CLIExactCompare'], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C1', '--output', str(output / 'foundation-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C2', '--output', str(output / 'vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C3', '--output', str(output / 'arithmetic-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C4', '--output', str(output / 'advanced-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C5', '--output', str(output / 'integration-vectors.json')], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/CLIQualification', 'ProviderConsumer'], ['python3', '-B', 'Scripts/qualify_schema_providers.py', '--binary', '.build/release/swift-json', '--output', str(output / 'providers.json')]] @@ -273,12 +274,13 @@ def check_result(command, code, log, events=None): passed = data['observed'] == data['passed'] == expected and data['failed'] == data['policy'] == data['blocked'] == 0 if command == ['swift', 'package', 'dump-package'] and passed: d = json.loads(log) - passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONInspection', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', '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', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation', 'JSONInspection']}, **{'swift-json': ['JSONCLI']}) + passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONInspection', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONTransform', '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', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation', 'JSONInspection', 'JSONTransform']}, **{'swift-json': ['JSONCLI']}) targets = {t['name']: t for t in d['targets']} - passed = passed and set(targets) == {'JSON', 'JSONNavigation', 'JSONNavigationTests', 'JSONInspection', 'JSONInspectionTests', 'JSONIntegrationTests', 'JSONBridges', 'JSONBridgesTests', 'JSONSchema', 'JSONSchemaExamples', 'JSONSchemaExamplesTests', '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', 'JSONInspection', 'JSONInspectionTests', 'JSONIntegrationTests', 'JSONBridges', 'JSONBridgesTests', 'JSONSchema', 'JSONSchemaExamples', 'JSONSchemaExamplesTests', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests', 'JSONTransform', 'JSONTransformTests'} 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 targets['JSONNavigation']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONNavigationTests']['dependencies'] == [{'byName': ['JSONNavigation', None]}, {'byName': ['JSON', None]}] passed = passed and targets['JSONSchemaExamples']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONSchema', None]}] and targets['JSON']['dependencies'] == [] and targets['JSONSchema']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONRegexRuntime', None]}] + passed = passed and targets['JSONTransform']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONFilterRuntime', None]}] and targets['JSONTransformTests']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONTransform', None]}] passed = passed and targets['JSONInspection']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONNavigation', None]}] and targets['JSONInspectionTests']['dependencies'] == [{'byName': ['JSONInspection', None]}, {'byName': ['JSONNavigation', None]}, {'byName': ['JSON', None]}] if command[:3] == ['python3', '-B', 'Scripts/qualify_schema_example_matrix.py']: passed = passed and 'SCHEMA_EXAMPLES_MATRIX passed schemas=64 certified=512 invalid=64 processes=2' in log.splitlines() diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 0a2b127..a244ffd 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:b7cfb84e34289d4bb2342e2897c637cdf4fd19f2455072696d27f396bf8ef735" + package static let revision = "sha256:5360a44f2590f398047bafb8b0732b531ea7c65d3396b035ec4c290e7207e1e3" package static let kind = "source-sha256" } diff --git a/Sources/JSONCLIEngine/CLIErrorExit.swift b/Sources/JSONCLIEngine/CLIErrorExit.swift new file mode 100644 index 0000000..de9a184 --- /dev/null +++ b/Sources/JSONCLIEngine/CLIErrorExit.swift @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSONFilterRuntime + +// Process exit policy belongs to the CLI host, never the transformation library. +extension CLIError { + package var exit: Int32 { + switch category { + case .usage: 2 + case .compile: 3 + case .language, .input: 5 + case .resource: 6 + case .io: 7 + case .cancelled: 130 + } + } +} diff --git a/Sources/JSONCLIEngine/CLIQueryCommand.swift b/Sources/JSONCLIEngine/CLIQueryCommand.swift index d2460f5..9ad8ba2 100644 --- a/Sources/JSONCLIEngine/CLIQueryCommand.swift +++ b/Sources/JSONCLIEngine/CLIQueryCommand.swift @@ -2,6 +2,7 @@ import Foundation import JSON import JSONFilterRuntime +import JSONTransform package enum CLIQueryCommand { package static func run(_ arguments: CLIArguments, context: inout CLIJobContext) throws -> Int32 { @@ -33,7 +34,7 @@ package enum CLIQueryCommand { try? context.reclaimValueLeases() } bindings = try makeBindings(arguments, context: &context) - let program = try FilterCompiler.compile(source, variables: Set(bindings.keys), context: &context) + let program = try JSONTransformProgram.compileAccounted(source, variables: Set(bindings.keys), context: &context) try context.domain(.core) let output = try CLIOutput(destination: arguments.options["output"], overwrite: arguments.overwrite, inputs: inputFiles + auxiliary, context: &context) let framing: CLIInputFraming @@ -47,7 +48,7 @@ package enum CLIQueryCommand { var finalTruth: Bool? func evaluate(_ input: FilterYield, cursor: inout CLIInputCursor, context: inout CLIJobContext) throws { try context.domain(.core) - var vm = try FilterVM(program: program, input: input, bindings: bindings, context: &context) + var vm = try program.makeAccountedExecution(input: input, bindings: bindings, context: &context) defer { try? vm.close(context: &context) } while true { try context.domain(.core) diff --git a/Sources/JSONFilterRuntime/CLICompatibility.swift b/Sources/JSONFilterRuntime/CLICompatibility.swift new file mode 100644 index 0000000..b1da888 --- /dev/null +++ b/Sources/JSONFilterRuntime/CLICompatibility.swift @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Package-only migration aliases preserve host/ownership-probe source spelling. +// They are not exported by the public transformation API. +package typealias CLIJobContext = TransformRuntimeContext +package typealias CLILimits = TransformRuntimeLimits +package typealias CLIResourceMetric = TransformRuntimeMetric +package typealias CLIError = TransformRuntimeFailure +package typealias CLIValueLease = TransformValueLease +package typealias CLILeaseRefunds = TransformLeaseRefunds diff --git a/Sources/JSONFilterRuntime/DecimalMagnitude.swift b/Sources/JSONFilterRuntime/DecimalMagnitude.swift index cb1ba7e..9d8efca 100644 --- a/Sources/JSONFilterRuntime/DecimalMagnitude.swift +++ b/Sources/JSONFilterRuntime/DecimalMagnitude.swift @@ -6,37 +6,37 @@ import JSON package struct DecimalMagnitude { private(set) var storage: [UInt8] private var start: Int = 0 - private let lease: CLIValueLease? + private let lease: TransformValueLease? package init(borrowing digits: [UInt8]) { storage = digits; lease = nil } - private init(storage: [UInt8], lease: CLIValueLease) { self.storage = storage; self.lease = lease } + private init(storage: [UInt8], lease: TransformValueLease) { self.storage = storage; self.lease = lease } package var count: Int { storage.count - start } package var isZero: Bool { count == 1 && storage[start] == 0 } package subscript(_ index: Int) -> UInt8 { storage[start + index] } package static func zero() -> Self { .init(borrowing: [0]) } - package static func allocate(_ count: Int, context: inout CLIJobContext) throws -> Self { + package static func allocate(_ count: Int, context: inout TransformRuntimeContext) throws -> Self { try context.checkpoint() - guard count > 0, count < Int.max else { throw CLIError(.resource, "counterOverflow", metric: "numberDigits") } + guard count > 0, count < Int.max else { throw TransformRuntimeFailure(.resource, "counterOverflow", metric: "numberDigits") } let maximum = context.meter.limits[.numberDigits] let (twice, overflow) = maximum.multipliedReportingOverflow(by: 2) let (scratch, extraOverflow) = twice.addingReportingOverflow(2) - guard !overflow, !extraOverflow, UInt64(count) <= scratch else { try context.meter.require(.numberDigits, UInt64(count)); throw CLIError(.resource, "limitExceeded", metric: "numberDigits") } + guard !overflow, !extraOverflow, UInt64(count) <= scratch else { try context.meter.require(.numberDigits, UInt64(count)); throw TransformRuntimeFailure(.resource, "limitExceeded", metric: "numberDigits") } try context.meter.retain(UInt64(count)) - let lease: CLIValueLease + let lease: TransformValueLease do { lease = try .init(ownedRetained: UInt64(count), context: &context) } catch { try context.meter.release(UInt64(count)); throw error } return .init(storage: [UInt8](repeating: 0, count: count), lease: lease) } - package static func step(_ context: inout CLIJobContext) throws { try context.meter.work(subsystem: .numericWorkUnits) } - private mutating func trim(context: inout CLIJobContext) throws { + package static func step(_ context: inout TransformRuntimeContext) throws { try context.meter.work(subsystem: .numericWorkUnits) } + private mutating func trim(context: inout TransformRuntimeContext) throws { while start + 1 < storage.count && storage[start] == 0 { try Self.step(&context); start += 1 } } - package static func compare(_ a: Self, _ b: Self, context: inout CLIJobContext) throws -> Int { + package static func compare(_ a: Self, _ b: Self, context: inout TransformRuntimeContext) throws -> Int { try step(&context) if a.count != b.count { return a.count < b.count ? -1 : 1 } for i in 0.. Self { + package static func add(_ a: Self, _ b: Self, context: inout TransformRuntimeContext) throws -> Self { let count = max(a.count, b.count) var output = try allocate(count + 1, context: &context), carry: UInt16 = 0 for offset in 0.. Self { - guard try compare(a, b, context: &context) >= 0 else { throw CLIError(.resource, "arithmeticInvariant") } + package static func subtract(_ a: Self, _ b: Self, context: inout TransformRuntimeContext) throws -> Self { + guard try compare(a, b, context: &context) >= 0 else { throw TransformRuntimeFailure(.resource, "arithmeticInvariant") } var output = try allocate(a.count, context: &context), borrow: Int16 = 0 for offset in 0.. Self { + package static func multiply(_ a: Self, _ b: Self, context: inout TransformRuntimeContext) throws -> Self { if a.isZero || b.isZero { try step(&context); return .zero() } let (count, overflow) = a.count.addingReportingOverflow(b.count) - guard !overflow else { throw CLIError(.resource, "counterOverflow", metric: "numberDigits") } + guard !overflow else { throw TransformRuntimeFailure(.resource, "counterOverflow", metric: "numberDigits") } var output = try allocate(count, context: &context) for i in (0.. Self { - guard zeros >= 0 else { throw CLIError(.resource, "arithmeticInvariant") } + package static func shifted(_ value: Self, zeros: Int64, context: inout TransformRuntimeContext) throws -> Self { + guard zeros >= 0 else { throw TransformRuntimeFailure(.resource, "arithmeticInvariant") } if value.isZero { try step(&context); return .zero() } let (count, overflow) = UInt64(value.count).addingReportingOverflow(UInt64(zeros)) - guard !overflow, count < UInt64(Int.max) else { throw CLIError(.resource, "counterOverflow", metric: "numberDigits") } + guard !overflow, count < UInt64(Int.max) else { throw TransformRuntimeFailure(.resource, "counterOverflow", metric: "numberDigits") } var output = try allocate(Int(count), context: &context) for i in 0.. (quotient: Self, remainder: Self) { + package static func divmod(_ a: Self, _ b: Self, context: inout TransformRuntimeContext) throws -> (quotient: Self, remainder: Self) { guard !b.isZero else { throw FilterLanguageFailure("divisionByZero") } if try compare(a, b, context: &context) < 0 { return (.zero(), a) } var quotient = try allocate(a.count, context: &context) var remainder = try allocate(b.count + 1, context: &context), used = 1 - func remainderCompare(context: inout CLIJobContext) throws -> Int { + func remainderCompare(context: inout TransformRuntimeContext) throws -> Int { try step(&context) if used != b.count { return used < b.count ? -1 : 1 } for i in 0.. 1 && remainder.storage[0] == 0 { for j in 1.. Self { + package static func gcd(_ a: Self, _ b: Self, context: inout TransformRuntimeContext) throws -> Self { var left = a, right = b while !right.isZero { try context.checkpoint(); try step(&context) @@ -140,18 +140,18 @@ package struct DecimalMagnitude { } return left } - package func coefficient(context: inout CLIJobContext) throws -> Self { + package func coefficient(context: inout TransformRuntimeContext) throws -> Self { var output = try Self.allocate(count, context: &context) for i in 0.. Bool { + package mutating func removingTrailingZero(context: inout TransformRuntimeContext) throws -> Bool { try Self.step(&context) guard count > 1, storage.last == 0 else { return false } storage.removeLast(); return true } - package static func prefix(_ digits: [UInt8], count: Int, context: inout CLIJobContext) throws -> Self { - guard count > 0, count <= digits.count else { throw CLIError(.resource, "arithmeticInvariant") } + package static func prefix(_ digits: [UInt8], count: Int, context: inout TransformRuntimeContext) throws -> Self { + guard count > 0, count <= digits.count else { throw TransformRuntimeFailure(.resource, "arithmeticInvariant") } var result = try allocate(count, context: &context) for index in 0.. Never { + private static func failure(_ context: inout TransformRuntimeContext, metric: JSONResourceMetric = .absoluteExponent) throws -> Never { let error = JSONFailure(.resource, code: .counterOverflow, resource: .init(domain: context.meter.domain, metric: metric, limit: context.meter.limits[metric], attemptedUsage: nil, overflow: true)) context.latch(error); throw error } - private static func addExponent(_ a: Int64, _ b: Int64, context: inout CLIJobContext) throws -> Int64 { + private static func addExponent(_ a: Int64, _ b: Int64, context: inout TransformRuntimeContext) throws -> Int64 { let (sum, overflow) = a.addingReportingOverflow(b); if overflow { try failure(&context) }; return sum } - private static func subtractExponent(_ a: Int64, _ b: Int64, context: inout CLIJobContext) throws -> Int64 { + private static func subtractExponent(_ a: Int64, _ b: Int64, context: inout TransformRuntimeContext) throws -> Int64 { let (difference, overflow) = a.subtractingReportingOverflow(b); if overflow { try failure(&context) }; return difference } - private static func admit(_ number: JSONNumber, context: inout CLIJobContext) throws { + private static func admit(_ number: JSONNumber, context: inout TransformRuntimeContext) throws { try context.meter.resetValueBounds(); try JSONAdmission.value(.number(number), meter: &context.meter) } - private static func admitToken(_ number: JSONNumber, context: inout CLIJobContext) throws { + private static func admitToken(_ number: JSONNumber, context: inout TransformRuntimeContext) throws { let c = number.components var bytes = UInt64(c.digits.count) + (c.negative ? 1 : 0) if c.exponent != 0 { try context.work(20); bytes += 1 + UInt64(String(c.exponent).utf8.count) } try context.meter.require(.numberTokenBytes, bytes) } - private static func normalized(_ magnitude: DecimalMagnitude, negative: Bool, exponent: Int64, context: inout CLIJobContext) throws -> Result { + private static func normalized(_ magnitude: DecimalMagnitude, negative: Bool, exponent: Int64, context: inout TransformRuntimeContext) throws -> Result { if magnitude.isZero { try DecimalMagnitude.step(&context); return Result(JSONNumber(integer: 0)) } var value = try magnitude.coefficient(context: &context), exponent = exponent while try value.removingTrailingZero(context: &context) { exponent = try addExponent(exponent, 1, context: &context) } try context.meter.require(.numberDigits, UInt64(value.count)); try context.meter.require(.absoluteExponent, exponent.magnitude) return Result(.init(components: .init(negative: negative, digits: value.coefficientDigits, exponent: exponent)), storage: value) } - package static func binary(_ op: String, _ lhs: JSONNumber, _ rhs: JSONNumber, context: inout CLIJobContext) throws -> FilterYield { + package static func binary(_ op: String, _ lhs: JSONNumber, _ rhs: JSONNumber, context: inout TransformRuntimeContext) throws -> FilterYield { try context.checkpoint(); try admit(lhs, context: &context); try admit(rhs, context: &context) let number: Result switch op { @@ -65,7 +65,7 @@ package enum ExactDecimal { return try FilterValues.produced(.number(number.number), context: &context) } } - private static func adding(_ lhs: JSONNumber, _ rhs: JSONNumber, subtract: Bool, context: inout CLIJobContext) throws -> Result { + private static func adding(_ lhs: JSONNumber, _ rhs: JSONNumber, subtract: Bool, context: inout TransformRuntimeContext) throws -> Result { let a = lhs.components, originalB = rhs.components let b = DecimalComponents(negative: !originalB.isZero && (originalB.negative != subtract), digits: originalB.digits, exponent: originalB.exponent) if a.isZero { return .init(components: b) } @@ -79,11 +79,11 @@ package enum ExactDecimal { else { value = try .subtract(right, left, context: &context); negative = b.negative } return try normalized(value, negative: negative, exponent: exponent, context: &context) } - private static func discarded(context: inout CLIJobContext) throws { + private static func discarded(context: inout TransformRuntimeContext) throws { let (next, overflow) = context.roundedDivisions.addingReportingOverflow(1) if overflow { try failure(&context, metric: .workUnits) }; context.roundedDivisions = next } - private static func quantized(_ input: Result, scale: Int, context: inout CLIJobContext) throws -> Result { + private static func quantized(_ input: Result, scale: Int, context: inout TransformRuntimeContext) throws -> Result { return try withExtendedLifetime(input) { let c = input.number.components, target = -Int64(scale) if c.isZero || c.exponent >= target { return input } @@ -106,7 +106,7 @@ package enum ExactDecimal { return try normalized(quotient, negative: c.negative, exponent: target, context: &context) } } - private static func dividing(_ lhs: JSONNumber, _ rhs: JSONNumber, context: inout CLIJobContext) throws -> Result { + private static func dividing(_ lhs: JSONNumber, _ rhs: JSONNumber, context: inout TransformRuntimeContext) throws -> Result { guard !rhs.isZero else { throw FilterLanguageFailure("divisionByZero") } if lhs.isZero { return Result(JSONNumber(integer: 0)) } let a = lhs.components, b = rhs.components @@ -155,7 +155,7 @@ package enum ExactDecimal { } return try normalized(quotient, negative: negative, exponent: -Int64(scale), context: &context) } - package static func unary(_ name: String, _ number: JSONNumber, context: inout CLIJobContext) throws -> FilterYield { + package static func unary(_ name: String, _ number: JSONNumber, context: inout TransformRuntimeContext) throws -> FilterYield { try context.checkpoint(); try admit(number, context: &context) let c = number.components, output: Result if name == "positive" { output = .init(components: c) } diff --git a/Sources/JSONFilterRuntime/FilterBindingPattern.swift b/Sources/JSONFilterRuntime/FilterBindingPattern.swift index 6fc5d9e..02edb7f 100644 --- a/Sources/JSONFilterRuntime/FilterBindingPattern.swift +++ b/Sources/JSONFilterRuntime/FilterBindingPattern.swift @@ -6,11 +6,11 @@ package struct FilterBindingPattern:Sendable { package struct Shape:Sendable{let object:Bool;let path:[JSON]} package let bindings:[Binding] package let shapes:[Shape] - package static func compile(_ range:Range,tokens:[FilterToken],matches:[Int:Int],context:inout CLIJobContext)throws->Self{ + package static func compile(_ range:Range,tokens:[FilterToken],matches:[Int:Int],context:inout TransformRuntimeContext)throws->Self{ struct Work{let range:Range;let path:[JSON]} var work:[Work]=[],bindings:[Binding]=[],shapes:[Shape]=[] try context.meter.retain(40);work.append(.init(range:range,path:[])) - func top(_ range:Range,context:inout CLIJobContext)throws->[Int]{ + func top(_ range:Range,context:inout TransformRuntimeContext)throws->[Int]{ var result:[Int]=[],i=range.lowerBound while ii{i=close+1}else{i += 1}} return result diff --git a/Sources/JSONFilterRuntime/FilterCollectionBuiltins.swift b/Sources/JSONFilterRuntime/FilterCollectionBuiltins.swift index 67a7192..ae791a9 100644 --- a/Sources/JSONFilterRuntime/FilterCollectionBuiltins.swift +++ b/Sources/JSONFilterRuntime/FilterCollectionBuiltins.swift @@ -4,7 +4,7 @@ import JSON package enum FilterCollectionBuiltins { package static let arities:[String:Set]=["sort":[0],"sort_by":[1],"group_by":[1],"unique":[0],"unique_by":[1],"min":[0],"max":[0],"min_by":[1],"max_by":[1],"reverse":[0],"flatten":[0,1],"first":[0,1],"last":[0,1],"nth":[1,2],"any":[0,1,2],"all":[0,1,2]] package struct Keyed {package let value:JSON;package let keys:[JSON]} - package static func ordered(_ source:[Keyed],context:inout CLIJobContext)throws->[Keyed] { + package static func ordered(_ source:[Keyed],context:inout TransformRuntimeContext)throws->[Keyed] { let bytes=UInt64(source.count)*32 try context.meter.retain(bytes*2) var completed=false @@ -26,12 +26,12 @@ package enum FilterCollectionBuiltins { } try context.meter.release(bytes);completed=true;return values } - package static func keyed(_ name:String,values:[Keyed],context:inout CLIJobContext)throws->FilterYield { + package static func keyed(_ name:String,values:[Keyed],context:inout TransformRuntimeContext)throws->FilterYield { try context.checkpoint();let entry=context.meter.used(.retainedBytes) var owned:UInt64=0,slots:UInt64=0,transferred=false defer{try? context.meter.release(context.meter.used(.retainedBytes)-entry-owned);if !transferred{try? context.release(.collectionValues,slots)}} let ordered=try ordered(values,context:&context) - func append(_ value:JSON,to array:inout [JSON],context:inout CLIJobContext)throws{ + func append(_ value:JSON,to array:inout [JSON],context:inout TransformRuntimeContext)throws{ try context.charge(.collectionValues);slots += 1;try context.meter.require(.arrayItems,UInt64(array.count)+1);try context.meter.retain(16);try context.work();array.append(value) } let result:JSON @@ -59,7 +59,7 @@ package enum FilterCollectionBuiltins { } let output=try FilterValues.produced(result,collections:slots,context:&context);owned=output.lease?.retained ?? 0;transferred=true;return output } - package static func flattened(_ source:JSON,depth:Int?,context:inout CLIJobContext)throws->FilterYield { + package static func flattened(_ source:JSON,depth:Int?,context:inout TransformRuntimeContext)throws->FilterYield { guard case .array(let array)=source else{throw FilterLanguageFailure("expectedArray")} try context.checkpoint();let entry=context.meter.used(.retainedBytes) var owned:UInt64=0,slots:UInt64=0,transferred=false diff --git a/Sources/JSONFilterRuntime/FilterCompiler.swift b/Sources/JSONFilterRuntime/FilterCompiler.swift index d583a63..e138a8e 100644 --- a/Sources/JSONFilterRuntime/FilterCompiler.swift +++ b/Sources/JSONFilterRuntime/FilterCompiler.swift @@ -20,7 +20,7 @@ package enum FilterCompiler { "error":[0,1], "input":[0], "inputs":[0], "add":[0], "abs":[0], "floor":[0], "ceil":[0], "trunc":[0], "round":[0], "range":[1,2,3], "while":[2], "until":[2], "recurse":[0,1,2], "walk":[1], "path":[1], "paths":[0,1], "getpath":[1], "setpath":[2], "delpaths":[1], "del":[1] ].merging(FilterStringBuiltins.arities) { old, _ in old }.merging(FilterCollectionBuiltins.arities) { old, _ in old } - package static func compile(_ source: String, variables: Set = ["ARGS"], context: inout CLIJobContext) throws -> FilterProgram { + package static func compile(_ source: String, variables: Set = ["ARGS"], context: inout TransformRuntimeContext) throws -> FilterProgram { try context.domain(.core) let tokens = try FilterLexer.lex(source, context: &context) guard !tokens.isEmpty else { throw FilterCompileFailure("emptyFilter", at: 0) } @@ -39,14 +39,14 @@ package enum FilterCompiler { } guard nesting.isEmpty else { throw FilterCompileFailure("unclosedDelimiter", at: tokens[nesting.last!.0].range.lowerBound) } var nodes: [FilterProgram.Node] = [], ranges: [Range] = [], work: [Work] = [], functions:[FilterProgram.Function]=[], patterns:[FilterBindingPattern]=[] - func slot(_ span: Range, depth: UInt64, variables: Set, functions:[String:Int]=[:],parameters:Set=[],declarations:Set=[], context: inout CLIJobContext) throws -> Int { + func slot(_ span: Range, depth: UInt64, variables: Set, functions:[String:Int]=[:],parameters:Set=[],declarations:Set=[], context: inout TransformRuntimeContext) throws -> Int { guard !span.isEmpty else { throw FilterCompileFailure("missingExpression", at: source.utf8.count) } try context.require(.queryDepth, depth); try context.charge(.astNodes); try context.meter.retain(128) let id = nodes.count; nodes.append(.pending); ranges.append(tokens[span.lowerBound].range.lowerBound.., context: inout CLIJobContext) throws -> [Int] { + func top(_ span: Range, context: inout TransformRuntimeContext) throws -> [Int] { var result: [Int] = [], i = span.lowerBound while i < span.upperBound { try context.work(); try context.meter.retain(8); taskScratch += 8; result.append(i) @@ -65,7 +65,7 @@ package enum FilterCompiler { guard !span.isEmpty else { throw FilterCompileFailure("missingExpression", at: ranges[task.destination].lowerBound) } } let lo = span.lowerBound, hi = span.upperBound, first = tokens[lo].text - func child(_ range: Range, context: inout CLIJobContext) throws -> Int { try slot(range,depth:task.depth+1,variables:task.variables,functions:task.functions,parameters:task.parameters,declarations:task.declarations,context:&context) } + func child(_ range: Range, context: inout TransformRuntimeContext) throws -> Int { try slot(range,depth:task.depth+1,variables:task.variables,functions:task.functions,parameters:task.parameters,declarations:task.declarations,context:&context) } let positions = try top(span, context:&context) if first=="def" { guard let semi=matches[lo],semi+1[Int]{ + func interpolationArguments(_ start:Int,context:inout TransformRuntimeContext)throws->[Int]{ guard start+1Bool { + package static func contains(_ left:JSON,_ right:JSON,context:inout TransformRuntimeContext)throws->Bool { func kind(_ value:JSON)->Int{switch value{case .null:0;case .boolean:1;case .number:2;case .string:3;case .array:4;case .object:5}} guard kind(left)==kind(right) else{throw FilterLanguageFailure("containsTypeMismatch")} try context.checkpoint();let entry=context.meter.used(.retainedBytes) defer{try? context.meter.release(context.meter.used(.retainedBytes)-entry)} var stack:[Frame]=[] - func push(_ left:JSON,_ right:JSON,context:inout CLIJobContext)throws { + func push(_ left:JSON,_ right:JSON,context:inout TransformRuntimeContext)throws { try context.require(.runtimeFrames,context.used(.runtimeFrames)+UInt64(stack.count)+1) try context.meter.retain(64);stack.append(.init(left:left,right:right)) } @@ -52,10 +52,10 @@ package enum FilterContainment { default: completed=kind(frame.left)==kind(frame.right) ? try FilterValues.equals(frame.left,frame.right,context:&context):false } - guard let result=completed else{throw CLIError(.resource,"containmentInvariant")} + guard let result=completed else{throw TransformRuntimeFailure(.resource,"containmentInvariant")} if stack.isEmpty{return result} stack[stack.count-1].incoming=result } - throw CLIError(.resource,"containmentInvariant") + throw TransformRuntimeFailure(.resource,"containmentInvariant") } } diff --git a/Sources/JSONFilterRuntime/FilterFormats.swift b/Sources/JSONFilterRuntime/FilterFormats.swift index 9bd18dc..c0fbd70 100644 --- a/Sources/JSONFilterRuntime/FilterFormats.swift +++ b/Sources/JSONFilterRuntime/FilterFormats.swift @@ -5,23 +5,23 @@ import JSON /// Bounded text encoders. @sh produces inert text and never invokes a process. package enum FilterFormats { package static let names: Set = ["text","json","html","uri","csv","tsv","sh","base64","base64d"] - package static func apply(_ name:String,value:JSON,context:inout CLIJobContext)throws->FilterYield { + package static func apply(_ name:String,value:JSON,context:inout TransformRuntimeContext)throws->FilterYield { try context.checkpoint() let entry=context.meter.used(.retainedBytes) var owned:UInt64=0,output="",bytes:UInt64=0 defer{try? context.meter.release(context.meter.used(.retainedBytes)-entry-owned)} - func append(_ text:String,context:inout CLIJobContext)throws { + func append(_ text:String,context:inout TransformRuntimeContext)throws { for scalar in text.unicodeScalars { let size=UInt64(scalar.utf8.count) try context.work(size);try context.meter.require(.stringBytes,bytes+size);try context.meter.retain(size) output.unicodeScalars.append(scalar);bytes += size } } - func text(_ value:JSON,context:inout CLIJobContext)throws->String {try FilterStringBuiltins.text(value,context:&context)} - func scalar(_ value:JSON,context:inout CLIJobContext)throws->String { + func text(_ value:JSON,context:inout TransformRuntimeContext)throws->String {try FilterStringBuiltins.text(value,context:&context)} + func scalar(_ value:JSON,context:inout TransformRuntimeContext)throws->String { switch value{case .array,.object:throw FilterLanguageFailure("formatExpectedScalar");case .null:return "";default:return try text(value,context:&context)} } - func quoteShell(_ source:String,context:inout CLIJobContext)throws { + func quoteShell(_ source:String,context:inout TransformRuntimeContext)throws { try append("'",context:&context) for scalar in source.unicodeScalars {try append(scalar.value==39 ? "'\\''" : String(scalar),context:&context)} try append("'",context:&context) @@ -76,7 +76,7 @@ package enum FilterFormats { guard input.count%4==0 else{throw FilterLanguageFailure("invalidBase64")} func digit(_ byte:UInt8)throws->UInt8{switch byte{case 65...90:return byte-65;case 97...122:return byte-97+26;case 48...57:return byte-48+52;case 43:return 62;case 47:return 63;default:throw FilterLanguageFailure("invalidBase64")}} var decoded:[UInt8]=[],index=0 - func byte(_ value:UInt8,context:inout CLIJobContext)throws{try context.work();try context.meter.require(.stringBytes,UInt64(decoded.count)+1);try context.meter.retain(1);decoded.append(value)} + func byte(_ value:UInt8,context:inout TransformRuntimeContext)throws{try context.work();try context.meter.require(.stringBytes,UInt64(decoded.count)+1);try context.meter.retain(1);decoded.append(value)} while index>4),context:&context) if input[index+2]==61{guard index+4==input.count,input[index+3]==61,b&15==0 else{throw FilterLanguageFailure("invalidBase64")}} diff --git a/Sources/JSONFilterRuntime/FilterLexer.swift b/Sources/JSONFilterRuntime/FilterLexer.swift index 13b351d..b3e10bc 100644 --- a/Sources/JSONFilterRuntime/FilterLexer.swift +++ b/Sources/JSONFilterRuntime/FilterLexer.swift @@ -2,14 +2,14 @@ import JSON package enum FilterLexer { - package static func lex(_ source: String, context: inout CLIJobContext) throws -> [FilterToken] { + package static func lex(_ source: String, context: inout TransformRuntimeContext) throws -> [FilterToken] { let count = UInt64(source.utf8.count) try context.charge(.querySourceBytes, count); try context.meter.retain(count); try context.work(count) let bytes = Array(source.utf8) enum Job { case scan(Range), token(FilterToken.Kind,Range) } var tokens: [FilterToken] = [], i = 0, end = bytes.count var jobs:[Job]=[] - func append(_ kind: FilterToken.Kind, _ start: Int, _ end: Int, context: inout CLIJobContext) throws { + func append(_ kind: FilterToken.Kind, _ start: Int, _ end: Int, context: inout TransformRuntimeContext) throws { try context.charge(.queryTokens); try context.meter.retain(48) tokens.append(.init(kind: kind, range: start..Self{ + private init(root:FilterLocationRoot,segments:[JSON],lease:TransformValueLease?){self.root=root;self.segments=segments;self.lease=lease} + package func appending(_ segment:JSON,context:inout TransformRuntimeContext)throws->Self{ switch segment{case .string:break;case .number(let n):guard n.isInteger else{throw FilterLanguageFailure("invalidPathSegment")};default:throw FilterLanguageFailure("invalidPathSegment")} let count=UInt64(segments.count)+1,bytes=count*16 try context.meter.require(.arrayItems,count);try context.meter.retain(bytes) diff --git a/Sources/JSONFilterRuntime/FilterOperators.swift b/Sources/JSONFilterRuntime/FilterOperators.swift index b8b6403..3d96e63 100644 --- a/Sources/JSONFilterRuntime/FilterOperators.swift +++ b/Sources/JSONFilterRuntime/FilterOperators.swift @@ -4,7 +4,7 @@ import JSON /// Type-directed exact overloads. Construction is invocation-owned and re-admitted; /// strings and object keys are never normalized through Swift canonical equality. package enum FilterOperators { - package static func apply(_ op: String, _ a: JSON, _ b: JSON, context: inout CLIJobContext) throws -> FilterYield { + package static func apply(_ op: String, _ a: JSON, _ b: JSON, context: inout TransformRuntimeContext) throws -> FilterYield { if case .number(let lhs) = a, case .number(let rhs) = b { return try ExactDecimal.binary(op, lhs, rhs, context: &context) } @@ -15,7 +15,7 @@ package enum FilterOperators { try? context.meter.release(context.meter.used(.retainedBytes) - entry - owned) if !transferred { try? context.release(.collectionValues, collections) } } - func collect(_ count: UInt64, context: inout CLIJobContext) throws { + func collect(_ count: UInt64, context: inout TransformRuntimeContext) throws { try context.charge(.collectionValues, count); collections += count try context.meter.require(.arrayItems, collections) try context.meter.retain(count * 16); try context.work(count) @@ -52,11 +52,11 @@ package enum FilterOperators { // Check the host index before narrowing a count; no hidden iteration limit. let repeats: Int do { repeats = try count.integer(as: Int.self, meter: &context.meter) } - catch let failure as JSONFailure where failure.category == .conversion { throw CLIError(.resource, "counterOverflow", metric: "stringBytes") } + catch let failure as JSONFailure where failure.category == .conversion { throw TransformRuntimeFailure(.resource, "counterOverflow", metric: "stringBytes") } var bytes: UInt64 = 0 for _ in text.utf8 { try context.work(); bytes += 1 } let (total, overflow) = bytes.multipliedReportingOverflow(by: UInt64(repeats)) - guard !overflow else { throw CLIError(.resource, "counterOverflow", metric: "stringBytes") } + guard !overflow else { throw TransformRuntimeFailure(.resource, "counterOverflow", metric: "stringBytes") } try context.meter.require(.stringBytes, total); try context.meter.retain(total) var output = "" for _ in 0.. JSON { - func frame(_ left: JSONObject, _ right: JSONObject, parent: Int?, context: inout CLIJobContext) throws -> MergeFrame { + private static func merge(_ left: JSONObject, _ right: JSONObject, recursive: Bool, context: inout TransformRuntimeContext) throws -> JSON { + func frame(_ left: JSONObject, _ right: JSONObject, parent: Int?, context: inout TransformRuntimeContext) throws -> MergeFrame { try context.meter.require(.objectMembers, UInt64(left.count)); try context.meter.retain(80 + UInt64(left.count) * 24); try context.work(UInt64(left.count)) var index = JSONExactKeyIndex() for position in left.members.indices { _ = try index.insert(left.members[position].key, position: position, meter: &context.meter) } @@ -108,14 +108,14 @@ package enum FilterOperators { } stack.append(current) } - throw CLIError(.resource, "mergeInvariant") + throw TransformRuntimeFailure(.resource, "mergeInvariant") } - package static func split(_ text: String, separator: String, context: inout CLIJobContext, collectionCount: inout UInt64) throws -> [JSON] { + package static func split(_ text: String, separator: String, context: inout TransformRuntimeContext, collectionCount: inout UInt64) throws -> [JSON] { var pattern: [Unicode.Scalar] = [], scalars: [Unicode.Scalar] = [] for scalar in separator.unicodeScalars { try context.work(); try context.meter.retain(4); pattern.append(scalar) } for scalar in text.unicodeScalars { try context.work(); try context.meter.retain(4); scalars.append(scalar) } var results: [JSON] = [], part = "", position = 0 - func append(_ text: String, context: inout CLIJobContext) throws { + func append(_ text: String, context: inout TransformRuntimeContext) throws { try context.charge(.collectionValues); collectionCount += 1 try context.meter.require(.arrayItems, collectionCount); try context.meter.retain(16) results.append(.string(text)) diff --git a/Sources/JSONFilterRuntime/FilterPaths.swift b/Sources/JSONFilterRuntime/FilterPaths.swift index a6edda8..b3af8d6 100644 --- a/Sources/JSONFilterRuntime/FilterPaths.swift +++ b/Sources/JSONFilterRuntime/FilterPaths.swift @@ -3,17 +3,17 @@ import JSON /// Path arrays retain their dialect identity, independent of Pointer token syntax. package enum FilterPaths { - package static func segments(_ value:JSON,context:inout CLIJobContext)throws->[JSON]{ + package static func segments(_ value:JSON,context:inout TransformRuntimeContext)throws->[JSON]{ guard case .array(let values)=value else{throw FilterLanguageFailure("expectedPathArray")} for segment in values{try context.work();switch segment{case .string:break;case .number(let n):guard n.isInteger else{throw FilterLanguageFailure("invalidPathSegment")};default:throw FilterLanguageFailure("invalidPathSegment")}} return values } - package static func get(_ input:JSON,path:[JSON],context:inout CLIJobContext)throws->JSON{ + package static func get(_ input:JSON,path:[JSON],context:inout TransformRuntimeContext)throws->JSON{ var value=input for segment in path{try context.work();value=try FilterValues.index(value,key:segment,context:&context)} return value } - private static func index(_ number:JSONNumber,count:Int,creating:Bool,context:inout CLIJobContext)throws->Int?{ + private static func index(_ number:JSONNumber,count:Int,creating:Bool,context:inout TransformRuntimeContext)throws->Int?{ guard number.isInteger else{throw FilterLanguageFailure("invalidPathSegment")} if number.isNegative{ if try number.compare(to:JSONNumber(integer:-count),meter:&context.meter) == .orderedAscending{return nil} @@ -31,7 +31,7 @@ package enum FilterPaths { } return try number.integer(as:Int.self,meter:&context.meter) } - package static func set(_ input:JSON,path:[JSON],replacement:JSON?,context:inout CLIJobContext)throws->FilterYield?{ + package static func set(_ input:JSON,path:[JSON],replacement:JSON?,context:inout TransformRuntimeContext)throws->FilterYield?{ try context.checkpoint();let entry=context.meter.used(.retainedBytes) var owned:UInt64=0,collections:UInt64=0,transferred=false defer{try? context.meter.release(context.meter.used(.retainedBytes)-entry-owned);if !transferred{try? context.release(.collectionValues,collections)}} @@ -91,15 +91,15 @@ package enum FilterPaths { guard let candidate else{return nil} let result=try FilterValues.produced(candidate,collections:collections,context:&context);owned=result.lease?.retained ?? 0;transferred=true;return result } - package static func same(_ a:[JSON],_ b:[JSON],context:inout CLIJobContext)throws->Bool{ + package static func same(_ a:[JSON],_ b:[JSON],context:inout TransformRuntimeContext)throws->Bool{ guard a.count==b.count else{return false} for index in a.indices{try context.work();if try !FilterValues.equals(a[index],b[index],context:&context){return false}};return true } - package static func prefix(_ a:[JSON],_ b:[JSON],context:inout CLIJobContext)throws->Bool{ + package static func prefix(_ a:[JSON],_ b:[JSON],context:inout TransformRuntimeContext)throws->Bool{ guard a.count[JSON]{ + package static func normalized(_ input:JSON,path:[JSON],context:inout TransformRuntimeContext)throws->[JSON]{ var result:[JSON]=[],current=input for segment in path{ try context.work();var normalized=segment @@ -108,7 +108,7 @@ package enum FilterPaths { } return result } - package static func targets(_ input:JSON,paths:[[JSON]],context:inout CLIJobContext)throws->[[JSON]]{ + package static func targets(_ input:JSON,paths:[[JSON]],context:inout TransformRuntimeContext)throws->[[JSON]]{ var result:[[JSON]]=[] for path in paths{ try context.work();let normalized=try normalized(input,path:path,context:&context) @@ -118,7 +118,7 @@ package enum FilterPaths { } return result } - package static func deletionOrder(_ paths:[[JSON]],context:inout CLIJobContext)throws->[Int]{ + package static func deletionOrder(_ paths:[[JSON]],context:inout TransformRuntimeContext)throws->[Int]{ var result:[Int]=[] for index in paths.indices{ try context.work();try context.meter.retain(8);var position=result.count diff --git a/Sources/JSONFilterRuntime/FilterPrimitiveBuiltins.swift b/Sources/JSONFilterRuntime/FilterPrimitiveBuiltins.swift index 8c17ab1..d13dd23 100644 --- a/Sources/JSONFilterRuntime/FilterPrimitiveBuiltins.swift +++ b/Sources/JSONFilterRuntime/FilterPrimitiveBuiltins.swift @@ -2,7 +2,7 @@ import JSON package enum FilterPrimitiveBuiltins { - package static func apply(_ name:String,value:JSON,argument:JSON?=nil,context:inout CLIJobContext)throws->FilterYield? { + package static func apply(_ name:String,value:JSON,argument:JSON?=nil,context:inout TransformRuntimeContext)throws->FilterYield? { try context.work() switch name { case "empty":return nil diff --git a/Sources/JSONFilterRuntime/FilterStringBuiltins.swift b/Sources/JSONFilterRuntime/FilterStringBuiltins.swift index c2e7985..6af3b95 100644 --- a/Sources/JSONFilterRuntime/FilterStringBuiltins.swift +++ b/Sources/JSONFilterRuntime/FilterStringBuiltins.swift @@ -5,7 +5,7 @@ import JSON /// charged; no Foundation regex/canonical string matching is an authority. package enum FilterStringBuiltins { package static let arities: [String: Set] = ["contains":[1],"startswith":[1],"endswith":[1],"split":[1],"join":[1],"indices":[1],"index":[1],"rindex":[1],"tostring":[0],"tonumber":[0],"ascii_upcase":[0],"ascii_downcase":[0],"ltrimstr":[1],"rtrimstr":[1],"explode":[0],"implode":[0]] - package static func apply(_ name: String, value: JSON, argument: JSON? = nil, context: inout CLIJobContext) throws -> FilterYield { + package static func apply(_ name: String, value: JSON, argument: JSON? = nil, context: inout TransformRuntimeContext) throws -> FilterYield { try context.checkpoint() let entry = context.meter.used(.retainedBytes) var owned: UInt64 = 0, collections: UInt64 = 0, transferred = false @@ -13,12 +13,12 @@ package enum FilterStringBuiltins { try? context.meter.release(context.meter.used(.retainedBytes) - entry - owned) if !transferred { try? context.release(.collectionValues, collections) } } - func append(_ scalar: Unicode.Scalar, to text: inout String, context: inout CLIJobContext) throws { + func append(_ scalar: Unicode.Scalar, to text: inout String, context: inout TransformRuntimeContext) throws { let size = UInt64(scalar.utf8.count) try context.work(size); try context.meter.require(.stringBytes, UInt64(text.utf8.count) + size); try context.meter.retain(size) text.unicodeScalars.append(scalar) } - func arraySlot(context: inout CLIJobContext) throws { + func arraySlot(context: inout TransformRuntimeContext) throws { try context.charge(.collectionValues); collections += 1 try context.meter.require(.arrayItems, collections); try context.meter.retain(16) } @@ -70,7 +70,7 @@ package enum FilterStringBuiltins { for scalar in source.unicodeScalars { let point=scalar.value let mapped=name == "ascii_upcase" && (97...122).contains(point) ? point-32 : name == "ascii_downcase" && (65...90).contains(point) ? point+32 : point - guard let scalar=Unicode.Scalar(mapped) else { throw CLIError(.resource,"scalarInvariant") } + guard let scalar=Unicode.Scalar(mapped) else { throw TransformRuntimeFailure(.resource,"scalarInvariant") } try append(scalar,to:&output,context:&context) } result = .string(output) @@ -83,7 +83,7 @@ package enum FilterStringBuiltins { if name == "split" { result = .array(try FilterOperators.split(source, separator: separator, context: &context, collectionCount: &collections)) } else { let scalars=try scalarArray(source,context:&context),pattern=try scalarArray(separator,context:&context) - func match(_ position:Int,context:inout CLIJobContext)throws->Bool { + func match(_ position:Int,context:inout TransformRuntimeContext)throws->Bool { guard position>=0,position<=scalars.count,pattern.count<=scalars.count-position else{return false} for index in pattern.indices {try context.work();if scalars[position+index].value != pattern[index].value{return false}} return true @@ -113,15 +113,15 @@ package enum FilterStringBuiltins { let output=try FilterValues.produced(result,collections:collections,context:&context) owned=output.lease?.retained ?? 0;transferred=true;return output } - private static func scalarArray(_ text:String,context:inout CLIJobContext)throws->[Unicode.Scalar] { + private static func scalarArray(_ text:String,context:inout TransformRuntimeContext)throws->[Unicode.Scalar] { var result:[Unicode.Scalar]=[] for scalar in text.unicodeScalars {try context.work();try context.meter.retain(4);result.append(scalar)} return result } /// The callback consumes text while its construction reservation is live. /// New callback storage is separately charged and survives the scratch refund. - package static func usingText(_ value:JSON,context:inout CLIJobContext, - _ body:(String,inout CLIJobContext)throws->Result)throws->Result { + package static func usingText(_ value:JSON,context:inout TransformRuntimeContext, + _ body:(String,inout TransformRuntimeContext)throws->Result)throws->Result { if case .string(let text)=value{return try body(text,&context)} try context.checkpoint();let entry=context.meter.used(.retainedBytes) var handedOff=false,scratch:UInt64=0 @@ -130,7 +130,7 @@ package enum FilterStringBuiltins { scratch=context.meter.used(.retainedBytes)-entry;handedOff=true return try body(source,&context) } - package static func text(_ value:JSON,context:inout CLIJobContext)throws->String { + package static func text(_ value:JSON,context:inout TransformRuntimeContext)throws->String { if case .string(let text)=value{return text} try context.meter.resetValueBounds() let bytes=try JSONWriter(options:.init(limits:context.meter.limits,numberStyle:.preserved)).write(value,meter:&context.meter) diff --git a/Sources/JSONFilterRuntime/FilterVM.swift b/Sources/JSONFilterRuntime/FilterVM.swift index 44760c7..9097f0c 100644 --- a/Sources/JSONFilterRuntime/FilterVM.swift +++ b/Sources/JSONFilterRuntime/FilterVM.swift @@ -2,7 +2,7 @@ import JSON package protocol FilterInput { - mutating func next(context: inout CLIJobContext) throws -> FilterYield? + mutating func next(context: inout TransformRuntimeContext) throws -> FilterYield? } /// Explicit, demand-driven continuations. The arena reuses slots; no language /// recursion becomes Swift call-stack recursion. Only immutable plans are shared. @@ -54,11 +54,11 @@ package struct FilterVM { private var available: [Int] = [] private var root = 0 private var finished = false - package init(program: FilterProgram, input: FilterYield, bindings: [String: FilterYield] = [:], context: inout CLIJobContext) throws { + package init(program: FilterProgram, input: FilterYield, bindings: [String: FilterYield] = [:], context: inout TransformRuntimeContext) throws { self.program = program root = try create(program.root, input: input, bindings: bindings, context: &context) } - private mutating func create(_ node: Int, input: FilterYield, bindings: [String: FilterYield], functions:[Int:FunctionClosure]=[:],parameters:[String:FilterThunk]=[:], context: inout CLIJobContext) throws -> Int { + private mutating func create(_ node: Int, input: FilterYield, bindings: [String: FilterYield], functions:[Int:FunctionClosure]=[:],parameters:[String:FilterThunk]=[:], context: inout TransformRuntimeContext) throws -> Int { try context.checkpoint() let entry = context.meter.used(.retainedBytes) try context.charge(.runtimeFrames) @@ -75,7 +75,7 @@ package struct FilterVM { if let id = available.popLast() { frames[id] = frame; complete = true; return id } try context.meter.retain(64); frames.append(frame); complete = true; return frames.count - 1 } - private mutating func dispose(_ id: Int, context: inout CLIJobContext) throws { + private mutating func dispose(_ id: Int, context: inout TransformRuntimeContext) throws { // Cleanup work and its bounded integer stack are prepaid at frame creation. var pending = [id] while let id = pending.popLast() { @@ -89,13 +89,13 @@ package struct FilterVM { } try context.reclaimValueLeases() } - package mutating func close(context: inout CLIJobContext) throws { + package mutating func close(context: inout TransformRuntimeContext) throws { if !finished { try dispose(root, context: &context) } try context.meter.release(UInt64(frames.count) * 64) frames = []; available = []; finished = true try context.reclaimValueLeases() } - package mutating func next(input: inout Input, context: inout CLIJobContext) throws -> FilterYield? { + package mutating func next(input: inout Input, context: inout TransformRuntimeContext) throws -> FilterYield? { if finished { return nil } var demands: [Int] = [], current = root defer { try? context.meter.release(UInt64(demands.count) * 8) } @@ -120,24 +120,24 @@ package struct FilterVM { } } } - private mutating func child(_ node: Int, frame: inout Frame, value: FilterYield? = nil, bindings: [String: FilterYield]? = nil,functions:[Int:FunctionClosure]?=nil,parameters:[String:FilterThunk]?=nil, context: inout CLIJobContext) throws -> Int { + private mutating func child(_ node: Int, frame: inout Frame, value: FilterYield? = nil, bindings: [String: FilterYield]? = nil,functions:[Int:FunctionClosure]?=nil,parameters:[String:FilterThunk]?=nil, context: inout TransformRuntimeContext) throws -> Int { let id = try create(node, input: value ?? frame.input, bindings: bindings ?? frame.bindings,functions:functions ?? frame.functions,parameters:parameters ?? frame.parameters, context: &context) frame.children.append(id); return id } private func event(_ frame: inout Frame) -> Event? { let event = frame.event; frame.event = nil; return event } - private func capture(_ value: FilterYield, frame: inout Frame, context: inout CLIJobContext) throws { + private func capture(_ value: FilterYield, frame: inout Frame, context: inout TransformRuntimeContext) throws { try context.charge(.collectionValues); frame.collectionSlots += 1 try context.meter.retain(24); frame.slots += 24 frame.values.append(value) } - private func releaseCache(_ frame: inout Frame, context: inout CLIJobContext, transferring: UInt64 = 0) throws { + private func releaseCache(_ frame: inout Frame, context: inout TransformRuntimeContext, transferring: UInt64 = 0) throws { frame.values = []; frame.members = [] try context.meter.release(frame.slots); frame.slots = 0 - guard transferring <= frame.collectionSlots else { throw CLIError(.usage, "invalidCollectionTransfer") } + guard transferring <= frame.collectionSlots else { throw TransformRuntimeFailure(.usage, "invalidCollectionTransfer") } try context.release(.collectionValues, frame.collectionSlots - transferring); frame.collectionSlots = 0 try context.reclaimValueLeases() } - private mutating func step(_ id: Int, input: inout Input, context: inout CLIJobContext) throws -> Action { + private mutating func step(_ id: Int, input: inout Input, context: inout TransformRuntimeContext) throws -> Action { var frame = frames[id]! frames[id] = nil // Unique mutation of the frame's COW buffers. defer { frames[id] = frame } @@ -418,12 +418,12 @@ package struct FilterVM { return try object(fields, frame: &frame, incoming: incoming, context: &context) } } - private func captureGroup(_ value: FilterYield, group: Int, frame: inout Frame, context: inout CLIJobContext) throws { + private func captureGroup(_ value: FilterYield, group: Int, frame: inout Frame, context: inout TransformRuntimeContext) throws { try context.charge(.collectionValues); frame.collectionSlots += 1 try context.meter.retain(24); frame.slots += 24 frame.groups[group].append(value) } - private func advance(_ selections: inout [Int], counts: [Int], context: inout CLIJobContext) throws -> Bool { + private func advance(_ selections: inout [Int], counts: [Int], context: inout TransformRuntimeContext) throws -> Bool { for offset in selections.indices.reversed() { try context.work(); selections[offset] += 1 if selections[offset] < counts[offset] { return true } @@ -431,7 +431,7 @@ package struct FilterVM { } return false } - private mutating func slice(_ base: Int, lower: Int?, upper: Int?, frame: inout Frame, incoming: Event?, context: inout CLIJobContext) throws -> Action { + private mutating func slice(_ base: Int, lower: Int?, upper: Int?, frame: inout Frame, incoming: Event?, context: inout TransformRuntimeContext) throws -> Action { let nodes = [base, lower, upper] if frame.phase == 0 { try context.meter.retain(120); frame.slots += 120 @@ -460,7 +460,7 @@ package struct FilterVM { frame.flag = try !advance(&frame.selections, counts: frame.groups.map(\.count), context: &context) return .emit(.value(result)) } - private mutating func object(_ fields: [(Int, Int)], frame: inout Frame, incoming: Event?, context: inout CLIJobContext) throws -> Action { + private mutating func object(_ fields: [(Int, Int)], frame: inout Frame, incoming: Event?, context: inout TransformRuntimeContext) throws -> Action { if frame.phase == 0 { try context.meter.retain(UInt64(fields.count) * 40 + 48); frame.slots += UInt64(fields.count) * 40 + 48 frame.groups = [[], []]; frame.index = 0; frame.phase = 1 @@ -524,7 +524,7 @@ package struct FilterVM { frame.flag = try !advance(&frame.selections, counts: frame.choices.map(\.count), context: &context) return .emit(.value(result)) } - private mutating func call(_ name: String, arguments: [Int], frame: inout Frame, incoming: Event?, input: inout Input, context: inout CLIJobContext) throws -> Action { + private mutating func call(_ name: String, arguments: [Int], frame: inout Frame, incoming: Event?, input: inout Input, context: inout TransformRuntimeContext) throws -> Action { if name == "paths" { return try paths(arguments.first,frame:&frame,incoming:incoming,context:&context) } if name == "setpath" { return try setpath(arguments,frame:&frame,incoming:incoming,context:&context) } if name == "del" { return try deleteExpression(arguments[0],frame:&frame,incoming:incoming,context:&context) } @@ -672,7 +672,7 @@ package struct FilterVM { } throw FilterCompileFailure("unsupportedBuiltin", at: program.sourceRanges[frame.node].lowerBound) } - private mutating func destructure(_ source:Int,pattern:FilterBindingPattern,after:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func destructure(_ source:Int,pattern:FilterBindingPattern,after:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{frame.phase=1;return .pull(try child(source,frame:&frame,context:&context))} if let incoming{ switch incoming{ @@ -700,7 +700,7 @@ package struct FilterVM { } return .pull(frame.children.last!) } - private func pathChildren(_ value:FilterYield,frame:inout Frame,context:inout CLIJobContext)throws { + private func pathChildren(_ value:FilterYield,frame:inout Frame,context:inout TransformRuntimeContext)throws { guard let location=value.location else{throw FilterLanguageFailure("invalidPath")} switch value.value{ case .array(let values): @@ -710,7 +710,7 @@ package struct FilterVM { default:break } } - private mutating func paths(_ predicate:Int?,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func paths(_ predicate:Int?,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{let root=try FilterLocationRoot(context:&context);frame.pathRoot=root;var value=frame.input;value.location = .init(root:root);try pathChildren(value,frame:&frame,context:&context);frame.phase=1} if let incoming{ switch incoming{ @@ -730,7 +730,7 @@ package struct FilterVM { guard let location=value.location else{throw FilterLanguageFailure("invalidPath")} return .emit(.value(try FilterValues.array(location.segments,context:&context))) } - private mutating func setpath(_ arguments:[Int],frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func setpath(_ arguments:[Int],frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{frame.phase=1;return .pull(try child(arguments[0],frame:&frame,context:&context))} if let incoming{ switch incoming{ @@ -745,7 +745,7 @@ package struct FilterVM { } return .pull(frame.children.last!) } - private mutating func deletePaths(_ value:JSON,frame:inout Frame,context:inout CLIJobContext)throws->Action { + private mutating func deletePaths(_ value:JSON,frame:inout Frame,context:inout TransformRuntimeContext)throws->Action { guard case .array(let values)=value else{throw FilterLanguageFailure("expectedPathList")} let entry=context.meter.used(.retainedBytes);var scratch:UInt64=0,ready=false defer{try? context.meter.release(ready ? scratch:context.meter.used(.retainedBytes)-entry)} @@ -761,7 +761,7 @@ package struct FilterVM { } return .emit(.value(candidate)) } - private mutating func deleteExpression(_ expression:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func deleteExpression(_ expression:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{let root=try FilterLocationRoot(context:&context);frame.pathRoot=root;var value=frame.input;value.location = .init(root:root);frame.phase=1;return .pull(try child(expression,frame:&frame,value:value,context:&context))} if let incoming{ switch incoming{ @@ -773,7 +773,7 @@ package struct FilterVM { } return .pull(frame.children[0]) } - private mutating func assignment(_ op:String,targets:Int,update:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func assignment(_ op:String,targets:Int,update:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{let root=try FilterLocationRoot(context:&context);frame.pathRoot=root;var value=frame.input;value.location = .init(root:root);frame.phase=1;return .pull(try child(targets,frame:&frame,value:value,context:&context))} if let incoming{ switch incoming{ @@ -820,7 +820,7 @@ package struct FilterVM { } return .pull(frame.children.last!) } - private mutating func path(_ expression:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func path(_ expression:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{ let root=try FilterLocationRoot(context:&context);frame.pathRoot=root var value=frame.input;value.location = .init(root:root);frame.phase=1 @@ -835,7 +835,7 @@ package struct FilterVM { } return .pull(frame.children[0]) } - private mutating func interpolate(_ parts:[Int],format:String?,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func interpolate(_ parts:[Int],format:String?,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{let bytes=UInt64(parts.count)*32;try context.meter.retain(bytes);frame.slots += bytes;frame.groups=Array(repeating:[],count:parts.count);frame.selections=Array(repeating:0,count:parts.count);frame.phase=1} if let incoming{ switch incoming{case .value(let value):try captureGroup(value,group:frame.index,frame:&frame,context:&context);return .pull(frame.children[0]);case .end:try dispose(frame.children.removeLast(),context:&context);frame.index += 1;case .failure(let error):throw error} @@ -847,7 +847,7 @@ package struct FilterVM { if frame.flag{frame.phase=99;return .emit(.end)} var output="",bytes:UInt64=0 defer{try? context.meter.release(bytes)} - func append(_ text:String,context:inout CLIJobContext)throws{ + func append(_ text:String,context:inout TransformRuntimeContext)throws{ for scalar in text.unicodeScalars{let size=UInt64(scalar.utf8.count);try context.work(size);try context.meter.require(.stringBytes,bytes+size);try context.meter.retain(size);output.unicodeScalars.append(scalar);bytes += size} } for index in parts.indices{ @@ -862,7 +862,7 @@ package struct FilterVM { frame.flag=try !advance(&frame.selections,counts:frame.groups.map(\.count),context:&context) return .emit(.value(result)) } - private mutating func walk(_ filter:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func walk(_ filter:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { let count:Int,object:JSONObject? switch frame.input.value{case .array(let values):count=values.count;object=nil;case .object(let value):count=value.count;object=value;default: if frame.phase==0{frame.phase=3;return .pull(try child(filter,frame:&frame,context:&context))} @@ -908,7 +908,7 @@ package struct FilterVM { try releaseCache(&frame,context:&context,transferring:transfer) frame.current=result;frame.phase=3;return .pull(try child(filter,frame:&frame,value:result,context:&context)) } - private mutating func loop(_ name:String,arguments:[Int],frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func loop(_ name:String,arguments:[Int],frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{frame.phase=1;return .pull(try child(arguments[0],frame:&frame,context:&context))} if let incoming{ switch incoming{ @@ -931,7 +931,7 @@ package struct FilterVM { if frame.phase==2{frame.phase=3;return .pull(try child(arguments[1],frame:&frame,context:&context))} return .pull(frame.children.last!) } - private mutating func recurse(_ arguments:[Int],frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func recurse(_ arguments:[Int],frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{frame.phase=1;return .emit(.value(frame.input))} if frame.phase==1{frame.phase=2;return .pull(try child(arguments[0],frame:&frame,context:&context))} if let incoming{ @@ -956,7 +956,7 @@ package struct FilterVM { } return .pull(frame.children.last!) } - private mutating func reduction(_ source:Int,name:String,initial:Int,update:Int,extract:Int?,foreach:Bool,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func reduction(_ source:Int,name:String,initial:Int,update:Int,extract:Int?,foreach:Bool,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{frame.phase=1;return .pull(try child(initial,frame:&frame,context:&context))} if let incoming{ switch incoming{ @@ -997,9 +997,9 @@ package struct FilterVM { } return .pull(frame.children.last!) } - private mutating func truthReduction(_ name:String,arguments:[Int],frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func truthReduction(_ name:String,arguments:[Int],frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { let identity=name=="all" - func verdict(_ truth:Bool,context:inout CLIJobContext)throws->Action{try .emit(.value(FilterValues.produced(.boolean(truth),context:&context)))} + func verdict(_ truth:Bool,context:inout TransformRuntimeContext)throws->Action{try .emit(.value(FilterValues.produced(.boolean(truth),context:&context)))} if arguments.isEmpty{ if frame.phase>0{return .emit(.end)};frame.phase=99 guard case .array(let values)=frame.input.value else{throw FilterLanguageFailure("expectedArray")} @@ -1034,7 +1034,7 @@ package struct FilterVM { } return .pull(frame.children.last!) } - private mutating func nth(_ arguments:[Int],frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func nth(_ arguments:[Int],frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{ if arguments.count==1 { guard case .array=frame.input.value else{throw FilterLanguageFailure("expectedArray")} } try context.meter.retain(48);frame.slots += 48;frame.groups=[[],[]];frame.phase=1;return .pull(try child(arguments[0],frame:&frame,context:&context)) @@ -1055,7 +1055,7 @@ package struct FilterVM { if order == .orderedDescending{allPassed=false} } if allPassed{try dispose(frame.children.removeLast(),context:&context);frame.phase=3} - else{let (next,overflow)=frame.ordinal.addingReportingOverflow(1);guard !overflow else{throw CLIError(.resource,"counterOverflow",metric:"totalWorkUnits",overflow:true)};frame.ordinal=next;return .pull(frame.children[0])} + else{let (next,overflow)=frame.ordinal.addingReportingOverflow(1);guard !overflow else{throw TransformRuntimeFailure(.resource,"counterOverflow",metric:"totalWorkUnits",overflow:true)};frame.ordinal=next;return .pull(frame.children[0])} case .end: try dispose(frame.children.removeLast(),context:&context) if frame.phase==1{ @@ -1087,7 +1087,7 @@ package struct FilterVM { } return .pull(frame.children[0]) } - private mutating func terminalSelection(_ name:String,argument:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func terminalSelection(_ name:String,argument:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { if frame.phase==0{frame.phase=1;return .pull(try child(argument,frame:&frame,context:&context))} if let incoming { switch incoming{ @@ -1102,7 +1102,7 @@ package struct FilterVM { } return .pull(frame.children[0]) } - private mutating func keyed(_ name:String,argument:Int,frame:inout Frame,incoming:Event?,context:inout CLIJobContext)throws->Action { + private mutating func keyed(_ name:String,argument:Int,frame:inout Frame,incoming:Event?,context:inout TransformRuntimeContext)throws->Action { guard case .array(let values)=frame.input.value else{throw FilterLanguageFailure("expectedArray")} if frame.phase==0 { let bytes=UInt64(values.count)*24 @@ -1124,7 +1124,7 @@ package struct FilterVM { let result=try FilterCollectionBuiltins.keyed(name,values:keyed,context:&context) frame.phase=99;return .emit(.value(result)) } - private mutating func range(_ arguments: [Int], frame: inout Frame, incoming: Event?, context: inout CLIJobContext) throws -> Action { + private mutating func range(_ arguments: [Int], frame: inout Frame, incoming: Event?, context: inout TransformRuntimeContext) throws -> Action { if frame.phase == 0 { let bytes = UInt64(arguments.count) * 32 try context.meter.retain(bytes); frame.slots += bytes @@ -1160,7 +1160,7 @@ package struct FilterVM { frame.flag = try !advance(&frame.selections, counts: frame.groups.map(\.count), context: &context) } } - private func languageError(_ value: JSON, frame: inout Frame, context: inout CLIJobContext) throws -> FilterLanguageFailure { + private func languageError(_ value: JSON, frame: inout Frame, context: inout TransformRuntimeContext) throws -> FilterLanguageFailure { try context.checkpoint() let entry = context.meter.used(.retainedBytes) var building = true, scratch: UInt64 = 0 diff --git a/Sources/JSONFilterRuntime/FilterValues.swift b/Sources/JSONFilterRuntime/FilterValues.swift index 562e35f..4c43b59 100644 --- a/Sources/JSONFilterRuntime/FilterValues.swift +++ b/Sources/JSONFilterRuntime/FilterValues.swift @@ -3,31 +3,31 @@ import Foundation import JSON package enum FilterValues { - package static func produced(_ value:JSON, collections:UInt64=0, context:inout CLIJobContext)throws->FilterYield { + package static func produced(_ value:JSON, collections:UInt64=0, context:inout TransformRuntimeContext)throws->FilterYield { try context.checkpoint() let entry=context.meter.used(.retainedBytes) do { try context.meter.resetValueBounds() try JSONAdmission.value(value,meter:&context.meter,owned:true) let bytes=context.meter.used(.retainedBytes)-entry - let lease=try CLIValueLease(ownedRetained:bytes,collections:collections,context:&context) + let lease=try TransformValueLease(ownedRetained:bytes,collections:collections,context:&context) return .init(value:value,lease:lease,collectedValues:collections) } catch { try context.meter.release(context.meter.used(.retainedBytes)-entry) throw error } } - package static func integer(_ value:Int, context:inout CLIJobContext)throws->FilterYield { + package static func integer(_ value:Int, context:inout TransformRuntimeContext)throws->FilterYield { try context.work(20) return try produced(.number(JSONNumber(integer:value)),context:&context) } - package static func array(_ values:[JSON], admittedCollectionCount:Bool=false, context:inout CLIJobContext)throws->FilterYield { + package static func array(_ values:[JSON], admittedCollectionCount:Bool=false, context:inout TransformRuntimeContext)throws->FilterYield { let count=UInt64(values.count) if !admittedCollectionCount { try context.charge(.collectionValues,count) } do { return try produced(.array(values),collections:count,context:&context) } catch { if !admittedCollectionCount { try context.release(.collectionValues,count) };throw error } } - package static func index(_ value:JSON,key:JSON,context:inout CLIJobContext)throws->JSON { + package static func index(_ value:JSON,key:JSON,context:inout TransformRuntimeContext)throws->JSON { switch key { case .string(let text): switch value {case .null:return .null;case .object(let object):return try object.lookup(text,meter:&context.meter) ?? .null;default:throw FilterLanguageFailure("cannotIndex")} @@ -45,13 +45,13 @@ package enum FilterValues { default:throw FilterLanguageFailure("invalidIndexKind") } } - package static func equals(_ a: JSON, _ b: JSON, context: inout CLIJobContext) throws -> Bool { + package static func equals(_ a: JSON, _ b: JSON, context: inout TransformRuntimeContext) throws -> Bool { try context.checkpoint() let entry = context.meter.used(.retainedBytes) defer { try? context.meter.release(context.meter.used(.retainedBytes) - entry) } return try JSONComparison.equals(a, b, meter: &context.meter) } - package static func has(_ value:JSON,key:JSON,context:inout CLIJobContext)throws->Bool { + package static func has(_ value:JSON,key:JSON,context:inout TransformRuntimeContext)throws->Bool { switch (value,key) { case (.object(let object),.string(let text)):return try object.lookup(text,meter:&context.meter) != nil case (.array(let values),.number(let number)): @@ -60,7 +60,7 @@ package enum FilterValues { default:throw FilterLanguageFailure("invalidHasDomain") } } - package static func textCompare(_ a:String,_ b:String,context:inout CLIJobContext)throws->Int { + package static func textCompare(_ a:String,_ b:String,context:inout TransformRuntimeContext)throws->Int { var left=a.utf8.makeIterator(),right=b.utf8.makeIterator() while true { try context.work();let a=left.next(),b=right.next() @@ -69,7 +69,7 @@ package enum FilterValues { return a[JSONMember] { + package static func sortedMembers(_ object:JSONObject,context:inout TransformRuntimeContext)throws->[JSONMember] { let bytes=UInt64(object.count)*JSONLogicalWeights.member try context.meter.retain(bytes*2) var completed = false @@ -95,7 +95,7 @@ package enum FilterValues { try context.meter.release(bytes); completed = true return values } - package static func compare(_ a:JSON,_ b:JSON,context:inout CLIJobContext)throws->Int { + package static func compare(_ a:JSON,_ b:JSON,context:inout TransformRuntimeContext)throws->Int { func rank(_ v:JSON)->Int {switch v{case .null:0;case .boolean(false):1;case .boolean(true):2;case .number:3;case .string:4;case .array:5;case .object:6}} var pending:[(JSON,JSON)]=[] try context.meter.retain(16);pending.append((a,b)) @@ -127,7 +127,7 @@ package enum FilterValues { } return 0 } - package static func bound(_ value:JSON?,length:Int,default fallback:Int,context:inout CLIJobContext)throws->Int { + package static func bound(_ value:JSON?,length:Int,default fallback:Int,context:inout TransformRuntimeContext)throws->Int { guard let value else{return fallback} guard case .number(let number)=value,number.isInteger else{throw FilterLanguageFailure("invalidSliceBound")} if try number.compare(to:JSONNumber(integer:length),meter:&context.meter) != .orderedAscending{return length} @@ -135,7 +135,7 @@ package enum FilterValues { let index=try number.integer(as:Int.self,meter:&context.meter) return index<0 ? length+index:index } - package static func slice(_ value:JSON,start:JSON?,end:JSON?,context:inout CLIJobContext)throws->FilterYield { + package static func slice(_ value:JSON,start:JSON?,end:JSON?,context:inout TransformRuntimeContext)throws->FilterYield { if case .null=value{return .init(value:.null)} switch value { case .array(let values): diff --git a/Sources/JSONFilterRuntime/RangeState.swift b/Sources/JSONFilterRuntime/RangeState.swift index 847282e..e4542e1 100644 --- a/Sources/JSONFilterRuntime/RangeState.swift +++ b/Sources/JSONFilterRuntime/RangeState.swift @@ -7,20 +7,20 @@ package struct FilterRangeState { private let end: JSONNumber private let step: JSONNumber private var advance = false - package init(start: JSONNumber, end: JSONNumber, step: JSONNumber, context: inout CLIJobContext) throws { + package init(start: JSONNumber, end: JSONNumber, step: JSONNumber, context: inout TransformRuntimeContext) throws { guard !step.isZero else { throw FilterLanguageFailure("zeroRangeStep") } // Admission precedes sparse progression, including a range with no results. for number in [start,end,step] { try context.meter.resetValueBounds(); try JSONAdmission.value(.number(number), meter: &context.meter) } current = try FilterValues.produced(.number(start), context: &context) self.end = end; self.step = step } - package mutating func next(context: inout CLIJobContext) throws -> FilterYield? { + package mutating func next(context: inout TransformRuntimeContext) throws -> FilterYield? { try context.work() if advance { - guard case .number(let number) = current.value else { throw CLIError(.resource, "rangeInvariant") } + guard case .number(let number) = current.value else { throw TransformRuntimeFailure(.resource, "rangeInvariant") } current = try ExactDecimal.binary("+", number, step, context: &context) } - guard case .number(let number) = current.value else { throw CLIError(.resource, "rangeInvariant") } + guard case .number(let number) = current.value else { throw TransformRuntimeFailure(.resource, "rangeInvariant") } let order = try number.compare(to: end, meter: &context.meter) if step.isNegative ? order != .orderedDescending : order != .orderedAscending { try current.release(context: &context); return nil } advance = true; return current diff --git a/Sources/JSONFilterRuntime/CLIJobContext.swift b/Sources/JSONFilterRuntime/TransformRuntimeContext.swift similarity index 73% rename from Sources/JSONFilterRuntime/CLIJobContext.swift rename to Sources/JSONFilterRuntime/TransformRuntimeContext.swift index 85a003a..afe17ac 100644 --- a/Sources/JSONFilterRuntime/CLIJobContext.swift +++ b/Sources/JSONFilterRuntime/TransformRuntimeContext.swift @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 import JSON -/// CLI-only quotas. Core metric identifiers and their 49 meanings remain unchanged. -package enum CLIResourceMetric: String, CaseIterable, Sendable { +/// Shared private operation quotas. CLI names and all 49 native metrics remain unchanged. +package enum TransformRuntimeMetric: String, CaseIterable, Sendable { case argvBytes, querySourceBytes, queryTokens, astNodes, queryDepth case runtimeFrames, bindings, collectionValues, totalInputBytes, totalOutputBytes case totalRecords, totalResults, totalWorkUnits, retainedBytes, diagnosticBytes, diskStagingBytes @@ -33,7 +33,7 @@ package enum CLIResourceMetric: String, CaseIterable, Sendable { } } } -package struct CLILimits: Sendable, Equatable { +package struct TransformRuntimeLimits: Sendable, Equatable { package var core: JSONLimits package let profileName: String private var caps: [UInt64] @@ -45,15 +45,15 @@ package struct CLILimits: Sendable, Equatable { case .offline: profileName = "offline"; caps = [1048576,1048576,262144,131072,2048,32768,4096,8388608,17179869184,17179869184,8388608,8388608,2147483648,536870912,262144,17179869184] } } - package subscript(_ metric: CLIResourceMetric) -> UInt64 { + package subscript(_ metric: TransformRuntimeMetric) -> UInt64 { get { caps[metric.index] } set { caps[metric.index] = newValue } } package func validate() throws { try core.validate() - for metric in CLIResourceMetric.allCases { + for metric in TransformRuntimeMetric.allCases { let cap = self[metric] - guard cap < UInt64.max, metric.cumulative || cap < UInt64(Int.max) else { throw CLIError(.usage, "invalidLimits") } + guard cap < UInt64.max, metric.cumulative || cap < UInt64(Int.max) else { throw TransformRuntimeFailure(.usage, "invalidLimits") } } } package var effectiveCore: JSONLimits { @@ -63,7 +63,7 @@ package struct CLILimits: Sendable, Equatable { return result } } -package struct CLIError: Error, Sendable, Equatable { +package struct TransformRuntimeFailure: Error, Sendable, Equatable { package enum Category: String, Sendable { case usage, compile, language, input, resource, io, cancelled } package let category: Category package let code: String @@ -77,35 +77,26 @@ package struct CLIError: Error, Sendable, Equatable { self.category = category; self.code = code; self.metric = metric self.limit = limit; self.attemptedUsage = attemptedUsage; self.overflow = overflow; self.byteOffset = byteOffset } - package var exit: Int32 { - switch category { - case .usage: 2 - case .compile: 3 - case .language, .input: 5 - case .resource: 6 - case .io: 7 - case .cancelled: 130 - } - } + } -/// One noncopyable job spans argv, files, native grammars, query execution and output. +/// One noncopyable operation scope. Hosts may compose parsing, transformation and writing. /// Mutable runtime state belongs to the invocation; immutable plans may be shared. -package struct CLIJobContext: ~Copyable { +package struct TransformRuntimeContext: ~Copyable { package var meter: JSONWorkMeter - package let leaseRefunds: CLILeaseRefunds - package private(set) var limits: CLILimits - private var counters = Array(repeating: UInt64(0), count: CLIResourceMetric.allCases.count) + package let leaseRefunds: TransformLeaseRefunds + package private(set) var limits: TransformRuntimeLimits + private var counters = Array(repeating: UInt64(0), count: TransformRuntimeMetric.allCases.count) private var failure: (any Error)? package var limitsFileApplied = false package var maxIssues: UInt64? = nil package var roundedDivisions: UInt64 = 0 package var divisionScale: Int? = nil package var committedResults: UInt64 = 0 - package init(limits: CLILimits = .init()) throws { + package init(limits: TransformRuntimeLimits = .init()) throws { try limits.validate() meter = try .init(limits: limits.effectiveCore, domain: .parsing, trackAcceptedCapacities: true) self.limits = limits - leaseRefunds = CLILeaseRefunds() + leaseRefunds = TransformLeaseRefunds() } /// Refund dead value storage even when data operations are terminal. Work is /// never refunded; the single native ledger remains authoritative. @@ -123,24 +114,24 @@ package struct CLIJobContext: ~Copyable { catch { latch(error); throw error } } package mutating func latch(_ error: any Error) { if failure == nil { failure = error; meter.recordFailure(error) } } - package func used(_ metric: CLIResourceMetric) -> UInt64 { + package func used(_ metric: TransformRuntimeMetric) -> UInt64 { switch metric { case .totalWorkUnits: meter.used(.workUnits) case .retainedBytes: meter.used(.retainedBytes) default: counters[metric.index] } } - package mutating func require(_ metric: CLIResourceMetric, _ attempted: UInt64) throws { + package mutating func require(_ metric: TransformRuntimeMetric, _ attempted: UInt64) throws { try checkpoint() guard attempted <= limits[metric] else { - let error = CLIError(.resource, "limitExceeded", metric: metric.rawValue, limit: limits[metric], attemptedUsage: attempted) + let error = TransformRuntimeFailure(.resource, "limitExceeded", metric: metric.rawValue, limit: limits[metric], attemptedUsage: attempted) latch(error); throw error } } - package mutating func charge(_ metric: CLIResourceMetric, _ amount: UInt64 = 1) throws { + package mutating func charge(_ metric: TransformRuntimeMetric, _ amount: UInt64 = 1) throws { try checkpoint() // Refund dead live capacities before computing the next charge. let (next, overflow) = used(metric).addingReportingOverflow(amount) - guard !overflow else { let error = CLIError(.resource, "counterOverflow", metric: metric.rawValue, limit: limits[metric], overflow: true); latch(error); throw error } + guard !overflow else { let error = TransformRuntimeFailure(.resource, "counterOverflow", metric: metric.rawValue, limit: limits[metric], overflow: true); latch(error); throw error } try require(metric, next) switch metric { case .totalWorkUnits: try meter.work(amount) @@ -148,8 +139,8 @@ package struct CLIJobContext: ~Copyable { default: counters[metric.index] = next } } - package mutating func release(_ metric: CLIResourceMetric, _ amount: UInt64) throws { - guard !metric.cumulative, amount <= used(metric) else { throw CLIError(.usage, "invalidRefund") } + package mutating func release(_ metric: TransformRuntimeMetric, _ amount: UInt64) throws { + guard !metric.cumulative, amount <= used(metric) else { throw TransformRuntimeFailure(.usage, "invalidRefund") } if metric == .retainedBytes { try meter.release(amount) } else { counters[metric.index] -= amount } } @@ -161,14 +152,14 @@ package struct CLIJobContext: ~Copyable { /// The only post-terminal accounting mutation spends previously reserved diagnostic work. /// It cannot reopen data operations, refund work or exceed aggregate diagnostic capacity. package mutating func acceptTerminalDiagnostics(_ bytes: UInt64) -> Bool { - let (next, overflow) = counters[CLIResourceMetric.diagnosticBytes.index].addingReportingOverflow(bytes) + let (next, overflow) = counters[TransformRuntimeMetric.diagnosticBytes.index].addingReportingOverflow(bytes) guard !overflow, next <= limits[.diagnosticBytes] else { return false } - counters[CLIResourceMetric.diagnosticBytes.index] = next + counters[TransformRuntimeMetric.diagnosticBytes.index] = next return true } - package mutating func configure(_ requested: CLILimits) throws { + package mutating func configure(_ requested: TransformRuntimeLimits) throws { try requested.validate(); try checkpoint() - for metric in CLIResourceMetric.allCases { guard used(metric) <= requested[metric] else { throw CLIError(.resource, "limitExceeded", metric: metric.rawValue, limit: requested[metric], attemptedUsage: used(metric)) } } + for metric in TransformRuntimeMetric.allCases { guard used(metric) <= requested[metric] else { throw TransformRuntimeFailure(.resource, "limitExceeded", metric: metric.rawValue, limit: requested[metric], attemptedUsage: used(metric)) } } try meter.configure(requested.effectiveCore) limits = requested } diff --git a/Sources/JSONFilterRuntime/CLIValueLease.swift b/Sources/JSONFilterRuntime/TransformValueLease.swift similarity index 76% rename from Sources/JSONFilterRuntime/CLIValueLease.swift rename to Sources/JSONFilterRuntime/TransformValueLease.swift index 07696b7..3ece35a 100644 --- a/Sources/JSONFilterRuntime/CLIValueLease.swift +++ b/Sources/JSONFilterRuntime/TransformValueLease.swift @@ -3,7 +3,7 @@ import JSON /// Invocation-only ARC notifications. The job drains refunds before its next /// checkpoint; destructors cannot replace a meter or reopen terminal health. -package final class CLILeaseRefunds { +package final class TransformLeaseRefunds { package var retained: UInt64 = 0 package var collections: UInt64 = 0 package var failure: (any Error)? @@ -11,18 +11,18 @@ package final class CLILeaseRefunds { let (nextBytes, byteOverflow) = retained.addingReportingOverflow(bytes) let (nextValues, valueOverflow) = collections.addingReportingOverflow(values) if byteOverflow || valueOverflow { - failure = CLIError(.resource, "counterOverflow", metric: "valueLeaseRefund") + failure = TransformRuntimeFailure(.resource, "counterOverflow", metric: "valueLeaseRefund") } else { retained = nextBytes; collections = nextValues } } } /// A lease holds reservations for one admitted immutable value graph. Selectors /// share its lifetime; newly constructed graphs undergo full owned admission. It is deliberately not Sendable; only plans are shared. -package final class CLIValueLease { - private let refunds: CLILeaseRefunds +package final class TransformValueLease { + private let refunds: TransformLeaseRefunds package let retained: UInt64 private let collections: UInt64 package init(ownedRetained: UInt64, collections: UInt64 = 0, - context: inout CLIJobContext) throws { + context: inout TransformRuntimeContext) throws { try context.checkpoint() let metadata = UInt64(48) try context.meter.retain(metadata) @@ -36,16 +36,16 @@ package final class CLIValueLease { /// reservation rather than refunding a source still borrowed by another result. package struct FilterYield { package var value: JSON - package var lease: CLIValueLease? + package var lease: TransformValueLease? package var location:FilterLocation? package var collectedValues: UInt64 - package init(value: JSON, lease: CLIValueLease? = nil, collectedValues: UInt64 = 0,location:FilterLocation?=nil) { self.value = value; self.lease = lease; self.collectedValues = collectedValues;self.location=location } + package init(value: JSON, lease: TransformValueLease? = nil, collectedValues: UInt64 = 0,location:FilterLocation?=nil) { self.value = value; self.lease = lease; self.collectedValues = collectedValues;self.location=location } package func selecting(_ value: JSON) -> Self { let retainedCollections: UInt64 switch value { case .array, .object: retainedCollections = collectedValues; default: retainedCollections = 0 } return .init(value: value, lease: lease, collectedValues: retainedCollections,location:location) } - package mutating func release(context: inout CLIJobContext) throws { + package mutating func release(context: inout TransformRuntimeContext) throws { value = .null; lease = nil;location=nil; collectedValues = 0 try context.reclaimValueLeases() } diff --git a/Sources/JSONTransform/JSONTransformExecution.swift b/Sources/JSONTransform/JSONTransformExecution.swift new file mode 100644 index 0000000..98ad253 --- /dev/null +++ b/Sources/JSONTransform/JSONTransformExecution.swift @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONFilterRuntime + +/// One exact output plus its invocation-owned source reservation. +/// +/// Keep this handle while using the accounted alias. JSON copied out of `value` +/// is caller-owned storage. The handle is deliberately not Sendable. +public struct JSONTransformOutput { + /// The exact native result; null is a legitimate value, never EOF. + public let value: JSON + private let lease: TransformValueLease? + init(_ value: FilterYield) { self.value = value.value; lease = value.lease } +} + +private struct SuppliedInput: FilterInput { + var source: (() throws -> JSON?)? + var ended = false + mutating func next(context: inout TransformRuntimeContext) throws -> FilterYield? { + try context.checkpoint() + guard !ended, let source else { return nil } + let value: JSON? + do { value = try source() } + catch { + let failure = error is CancellationError ? JSONTransformFailure(.cancelled, "cancelled") : JSONTransformFailure(.provider, "providerFailed") + context.latch(failure); throw failure + } + try context.checkpoint() // Cancellation after caller-owned work. + guard let value else { ended = true; self.source = nil; return nil } + try context.charge(.totalRecords) + return try FilterValues.produced(value, context: &context) + } +} + +/// A noncopyable, non-Sendable pull execution borrowed inside `withExecution`. +/// +/// All pulls, provider values and results share one finite meter. No mutable state +/// is retained in the Sendable plan. Arbitrary callback work is not instrumented. +public struct JSONTransformExecution: ~Copyable { + private var context: TransformRuntimeContext + private var vm: FilterVM? + private var input: SuppliedInput + private var planReservation: UInt64 = 0 + private var bindingReservation: UInt64 = 0 + private var initialBindings: UInt64 = 0 + private var failure: JSONTransformFailure? + private var cleanupFailure: JSONTransformFailure? + private var closed = false + + private init(context: consuming TransformRuntimeContext, source: (() throws -> JSON?)?) { + self.context = consume context + input = .init(source: source) + vm = nil + } + static func make(plan: JSONTransformProgram, input root: JSON, bindings: [String: JSON], + limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> Self { + do { + let context = try TransformRuntimeContext(limits: limits.runtime) + var execution = Self(context: consume context, source: source) + try execution.prepare(plan: plan, root: root, bindings: bindings) + return consume execution + } catch { throw JSONTransformFailure.classify(error) } + } + private mutating func prepare(plan: JSONTransformProgram, root: JSON, bindings: [String: JSON]) throws { + do { + try context.meter.retain(plan.planReservation) + planReservation = plan.planReservation + guard bindings.count == plan.variables.count else { throw JSONTransformFailure(.configuration, "invalidBindings") } + var supplied: [String: FilterYield] = [:] + for (name, value) in bindings { + try context.charge(.bindings) + initialBindings += 1 + let before = context.meter.used(.retainedBytes) + try JSONAdmission.text(name, key: true, meter: &context.meter, owned: true) + bindingReservation += context.meter.used(.retainedBytes) - before + guard plan.variables.contains(name) else { throw JSONTransformFailure(.configuration, "invalidBindings") } + supplied[name] = try FilterValues.produced(value, context: &context) + } + try context.charge(.totalRecords) + let value = try FilterValues.produced(root, context: &context) + vm = try FilterVM(program: plan.program, input: value, bindings: supplied, context: &context) + } catch { throw JSONTransformFailure.classify(error) } + } + + /// Pulls the next ordered exact output, or nil after completed evaluation. + /// + /// A resource, provider or cancellation failure latches and bypasses query + /// catches. Later pulls cannot create a fresh successful execution. + public mutating func next() throws -> JSONTransformOutput? { + do { + try context.checkpoint() + if let failure { throw failure } + guard !closed, var active = vm else { return nil } + vm = nil + defer { vm = active } + guard let value = try active.next(input: &input, context: &context) else { return nil } + try context.charge(.totalResults) + return .init(value) + } catch { + let failed = JSONTransformFailure.classify(error) + if failure == nil { failure = failed } + context.latch(failed) + throw failed + } + } + /// Current cumulative native statistics, also available after a failed pull. + public var statistics: JSONOperationStatistics { context.meter.statistics() } + /// Remaining shared owned work; this is not a caller instruction quota. + public var remainingWorkUnits: UInt64 { + let cap = min(context.limits.core[.workUnits], context.limits[.totalWorkUnits]) + return cap - min(cap, context.used(.totalWorkUnits)) + } + /// Reads a transform counter without exposing its mutable meter. + /// - Parameter metric: The cumulative or live transform counter. + public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { context.used(metric.runtime) } + /// Latches cancellation. The next pull and scope completion both throw. + public mutating func cancel() { + let cancelled = JSONTransformFailure(.cancelled, "cancelled") + if failure == nil { failure = cancelled } + context.latch(cancelled) + } + mutating func checkCompletion() throws { + try context.checkpoint() + if let failure { throw failure } + if let cleanupFailure { throw cleanupFailure } + } + mutating func recordCleanupFailure(_ error: any Error) { cleanupFailure = .classify(error) } + mutating func close() throws { + guard !closed else { return } + closed = true + input.source = nil + if var active = vm { vm = nil; try active.close(context: &context) } + try context.reclaimValueLeases() + // Remaining compilation/binding reservations end with the owner scope; + // returned output handles retain their own deferred lease reservations. + try context.meter.release(planReservation + bindingReservation) + try context.release(.bindings, initialBindings) + planReservation = 0; bindingReservation = 0; initialBindings = 0 + } +} diff --git a/Sources/JSONTransform/JSONTransformFailure.swift b/Sources/JSONTransform/JSONTransformFailure.swift new file mode 100644 index 0000000..67268fa --- /dev/null +++ b/Sources/JSONTransform/JSONTransformFailure.swift @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONFilterRuntime + +/// A typed operational or language error without CLI exits or rejected payloads. +public struct JSONTransformFailure: Error, Sendable, Equatable { + /// The boundary at which the operation failed. + public enum Category: String, Sendable { + /// Query grammar or declaration failure. + case compilation + /// An uncaught query-language exception. + case language + /// Inclusive finite resource ceiling or checked-counter failure. + case resource + /// Cancellation observed in owned work. + case cancelled + /// A caller-supplied input callback failed. + case provider + /// Invalid limits, bindings or execution state. + case configuration + } + /// The stable error category. + public let category: Category + /// The engine's fixed reason identifier; never an instance payload. + public let code: String + /// Query UTF-8 byte offset when compilation provided one. + public let byteOffset: UInt64? + /// Transform or native resource counter, if known. + public let metric: String? + /// Inclusive violated resource ceiling. + public let limit: UInt64? + /// Attempted usage, when representable. + public let attemptedUsage: UInt64? + /// Whether checked arithmetic overflowed. + public let overflow: Bool + + init(_ category: Category, _ code: String, byteOffset: UInt64? = nil, + metric: String? = nil, limit: UInt64? = nil, attemptedUsage: UInt64? = nil, overflow: Bool = false) { + self.category = category; self.code = code; self.byteOffset = byteOffset + self.metric = metric; self.limit = limit; self.attemptedUsage = attemptedUsage; self.overflow = overflow + } + static func classify(_ error: any Error) -> Self { + if let failure = error as? Self { return failure } + if error is CancellationError { return .init(.cancelled, "cancelled") } + if let failure = error as? FilterCompileFailure { return .init(.compilation, failure.code, byteOffset: UInt64(failure.offset)) } + if let failure = error as? FilterLanguageFailure { return .init(.language, failure.code) } + if let failure = error as? TransformRuntimeFailure { + let category: Category + switch failure.category { + case .usage: category = .configuration + case .compile: category = .compilation + case .language: category = .language + case .input, .io: category = .provider + case .resource: category = .resource + case .cancelled: category = .cancelled + } + let metric = JSONTransformLimits.Metric.allCases.first { $0.runtime.rawValue == failure.metric }?.rawValue ?? failure.metric + return .init(category, failure.code, byteOffset: failure.byteOffset, metric: metric, + limit: failure.limit, attemptedUsage: failure.attemptedUsage, overflow: failure.overflow) + } + if let failure = error as? JSONFailure { + return .init(failure.category == .resource ? .resource : .configuration, failure.code.rawValue, + byteOffset: failure.location?.byteOffset, metric: failure.resource?.metric.rawValue, + limit: failure.resource?.limit, attemptedUsage: failure.resource?.attemptedUsage, + overflow: failure.resource?.overflow ?? (failure.code == .counterOverflow)) + } + return .init(.configuration, "operationFailed") + } +} diff --git a/Sources/JSONTransform/JSONTransformLimits.swift b/Sources/JSONTransform/JSONTransformLimits.swift new file mode 100644 index 0000000..ba71bbf --- /dev/null +++ b/Sources/JSONTransform/JSONTransformLimits.swift @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONFilterRuntime + +/// Inclusive finite limits for compilation and one synchronous execution scope. +/// +/// Native JSON limits remain additional ceilings. Work is cumulative across all +/// pulls and provider inputs. Logical retention is not process RSS. Provider +/// callback instructions and already allocated caller values remain caller work. +public struct JSONTransformLimits: Sendable, Equatable { + /// A transformation-specific capacity or cumulative work counter. + public enum Metric: String, CaseIterable, Sendable { + /// UTF-8 query source bytes admitted by compilation. + case sourceBytes + /// Tokens admitted by compilation. + case tokens + /// Flat compiler nodes. + case planNodes + /// Query syntax nesting. + case depth + /// Simultaneously live evaluation frames. + case frames + /// Simultaneously live variable bindings. + case bindings + /// Simultaneously live collection slots. + case collectionValues + /// Total admitted root and provider values in an execution. + case inputValues + /// Total yielded results in an execution. + case results + /// Cumulative owned work, also limited by the native work ceiling. + case workUnits + /// Logical live bytes, also limited by the native retention ceiling. + case retainedBytes + + var runtime: TransformRuntimeMetric { + switch self { + case .sourceBytes: .querySourceBytes + case .tokens: .queryTokens + case .planNodes: .astNodes + case .depth: .queryDepth + case .frames: .runtimeFrames + case .bindings: .bindings + case .collectionValues: .collectionValues + case .inputValues: .totalRecords + case .results: .totalResults + case .workUnits: .totalWorkUnits + case .retainedBytes: .retainedBytes + } + } + } + var runtime: TransformRuntimeLimits + /// Native admission, numeric, cancellation and work ceilings. + public var json: JSONLimits { + get { runtime.core } + set { runtime.core = newValue } + } + /// Creates finite policy from an existing native resource profile. + /// - Parameter profile: The finite default policy to use. + public init(profile: JSONResourceProfile = .balanced) { runtime = .init(profile: profile) } + /// Reads or tightens a transform-specific inclusive maximum. + /// - Parameter metric: The transform counter to configure. + public subscript(metric: Metric) -> UInt64 { + get { runtime[metric.runtime] } + set { runtime[metric.runtime] = newValue } + } +} diff --git a/Sources/JSONTransform/JSONTransformProgram.swift b/Sources/JSONTransform/JSONTransformProgram.swift new file mode 100644 index 0000000..821f198 --- /dev/null +++ b/Sources/JSONTransform/JSONTransformProgram.swift @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONFilterRuntime + +/// An immutable, checked-Sendable compiled program in the existing query dialect. +/// +/// No input source, mutable invocation, file or process state is captured. This +/// provisional extraction API uses the same compiler and VM as `swift-json query`. +public struct JSONTransformProgram: Sendable { + let program: FilterProgram + let planReservation: UInt64 + let variables: Set + + /// Compiles explicit source using finite native and transform limits. + /// - Parameters: + /// - source: Query source in the documented jq-inspired subset. + /// - variables: Names supplied by each invocation, without `$` prefixes. + /// - limits: Finite compilation policy; execution has its own bounded scope. + public static func compile(_ source: String, variables: Set = [], + limits: JSONTransformLimits = .init()) throws -> Self { + do { + var context = try TransformRuntimeContext(limits: limits.runtime) + for name in variables { + try context.charge(.bindings) + try JSONAdmission.text(name, key: true, meter: &context.meter, owned: true) + } + let program = try FilterCompiler.compile(source, variables: variables, context: &context) + return .init(program: program, planReservation: context.meter.used(.retainedBytes), variables: variables) + } catch { throw JSONTransformFailure.classify(error) } + } + + /// Runs a synchronous, invocation-owned throwing pull scope. + /// + /// A present null result is distinct from EOF. The scope releases frames, + /// provider and owned bindings on exit. Extracted JSON copies become caller + /// storage; keep the output handle alive while using its accounted alias. + /// A caught operational failure still prevents successful scope completion. + /// - Parameters: + /// - input: Exact native root, admitted under this invocation's limits. + /// - bindings: Exact values for precisely the declared variable names. + /// - limits: Finite cumulative invocation policy. + /// - source: Optional host callback for `input`/`inputs`; no ambient I/O. + /// - body: Synchronous caller work pulling ordered zero/one/many outputs. + public func withExecution(input: JSON, bindings: [String: JSON] = [:], + limits: JSONTransformLimits = .init(), + source: (() throws -> JSON?)? = nil, + _ body: (inout JSONTransformExecution) throws -> Result) throws -> JSONOperationReport { + var execution = try JSONTransformExecution.make(plan: self, input: input, bindings: bindings, limits: limits, source: source) + let result: Result + do { result = try body(&execution) } + catch { + // Caller callback errors keep their identity. Cleanup cannot replace + // the originating failure or reopen the operation ledger. + do { try execution.close() } catch { execution.recordCleanupFailure(error) } + throw error + } + do { + try execution.checkCompletion() + try execution.close() + return .init(result: result, statistics: execution.statistics) + } catch { + let failure = JSONTransformFailure.classify(error) + do { try execution.close() } catch { execution.recordCleanupFailure(error) } + throw failure + } + } + + /// Compiles on the CLI host's existing cumulative context; no fresh meter. + package static func compileAccounted(_ source: String, variables: Set, + context: inout TransformRuntimeContext) throws -> Self { + let program = try FilterCompiler.compile(source, variables: variables, context: &context) + return .init(program: program, planReservation: 0, variables: variables) + } + /// Starts the same VM while borrowing the CLI job's accounting and leases. + package func makeAccountedExecution(input: FilterYield, bindings: [String: FilterYield], + context: inout TransformRuntimeContext) throws -> FilterVM { + try FilterVM(program: program, input: input, bindings: bindings, context: &context) + } +} diff --git a/Tests/JSONTransformTests/TransformSpikeTests.swift b/Tests/JSONTransformTests/TransformSpikeTests.swift new file mode 100644 index 0000000..1d7cd39 --- /dev/null +++ b/Tests/JSONTransformTests/TransformSpikeTests.swift @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +import JSONTransform + +private func values(_ source: String, input: JSON = .null) throws -> [JSON] { + let plan = try JSONTransformProgram.compile(source) + return try plan.withExecution(input: input) { execution in + var result: [JSON] = [] + while let output = try execution.next() { result.append(output.value) } + return result + }.result +} + +@Test func jtxZeroNullAndManyAreDistinctOrderedPullResults() throws { + #expect(try values("empty").isEmpty) + #expect(try values("null") == [.null]) + #expect(try values("1,2,3") == [1,2,3]) +} +@Test func jtxExactNumbersAndScalarKeysKeepCoreAuthority() throws { + let big = try JSON.parse("9007199254740994") + #expect(try values("9007199254740993 + 1, 0.1+0.2") == [big, JSON.parse("0.3")]) + let input = try JSON.parse(#"{"é":1,"e\u0301":2}"#) + #expect(try values(#".["é"], .["e\u0301"]"#, input: input) == [1,2]) + #expect(try values("length", input: .string("e\u{301}")) == [2]) +} +@Test func jtxTypedCompileLocationAndLanguageFailureStayDistinct() throws { + do { _ = try JSONTransformProgram.compile("["); Issue.record("Expected compilation failure") } + catch let failure as JSONTransformFailure { #expect(failure.category == .compilation && failure.byteOffset == 0) } + do { _ = try values("1/3"); Issue.record("Expected nonterminating decimal failure") } + catch let failure as JSONTransformFailure { #expect(failure.category == .language) } + #expect(try values("try (1/3) catch null") == [.null]) +} +@Test func jtxBindingsAndProviderAreExplicitAndLazy() throws { + let plan = try JSONTransformProgram.compile("$n, input, inputs", variables: ["n"]) + var pulls = 0 + let result = try plan.withExecution(input: .null, bindings: ["n": 9], source: { + pulls += 1 + return pulls <= 2 ? .number(JSONNumber(integer: pulls)) : nil + }) { execution in + #expect(pulls == 0) + var outputs: [JSON] = [] + while let output = try execution.next() { outputs.append(output.value) } + #expect(execution.used(.inputValues) == 3) + return outputs + } + #expect(result.result == [9,1,2] && pulls == 3) + #expect(result.statistics[.workUnits] > 0) +} +@Test func jtxProviderFailureIsOperationalAndNeverCaughtByLanguageTry() throws { + struct SourceFailure: Error {} + let plan = try JSONTransformProgram.compile("try inputs catch null") + var calls = 0 + do { + _ = try plan.withExecution(input: .null, source: { calls += 1; throw SourceFailure() }) { execution in + for _ in 0..<2 { + do { _ = try execution.next(); Issue.record("Expected provider failure") } + catch let failure as JSONTransformFailure { #expect(failure.category == .provider) } + } + return 0 // Catching the pull error must not make completion successful. + } + Issue.record("Expected latched scope failure") + } catch let failure as JSONTransformFailure { #expect(failure.category == .provider) } + #expect(calls == 1) +} +@Test func jtxCumulativeProviderAndResultCapsCannotResetPerPull() throws { + let plan = try JSONTransformProgram.compile("try inputs catch null") + var limits = JSONTransformLimits(); limits[.inputValues] = 2 + var calls = 0 + do { + _ = try plan.withExecution(input: .null, limits: limits, source: { calls += 1; return .null }) { execution in + let first = try execution.next() + #expect(first?.value == .null && execution.used(.inputValues) == 2) + return try execution.next() + } + Issue.record("Expected cumulative input cap") + } catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == "inputValues") } + #expect(calls == 2) + limits = .init(); limits[.results] = 1 + do { _ = try JSONTransformProgram.compile("1,2").withExecution(input: .null, limits: limits) { execution in + _ = try execution.next(); return try execution.next() + }; Issue.record("Expected result cap") } + catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == "results") } +} +@Test func jtxResourceErrorBypassesCatchAndLatchesAfterHostCatch() throws { + let plan = try JSONTransformProgram.compile("try [1,2,3] catch null") + var limits = JSONTransformLimits(); limits[.collectionValues] = 2 + do { + _ = try plan.withExecution(input: .null, limits: limits) { execution in + for _ in 0..<2 { + do { _ = try execution.next(); Issue.record("Expected resource failure") } + catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == "collectionValues") } + } + } + Issue.record("Expected scope failure") + } catch let failure as JSONTransformFailure { #expect(failure.category == .resource) } +} +@Test func jtxCancellationLatchesAndStopsProviderCalls() throws { + let plan = try JSONTransformProgram.compile("inputs") + var calls = 0 + do { + _ = try plan.withExecution(input: .null, source: { calls += 1; return .null }) { execution in + execution.cancel() + do { _ = try execution.next(); Issue.record("Expected cancellation") } + catch let failure as JSONTransformFailure { #expect(failure.category == .cancelled) } + } + Issue.record("Expected cancellation at completion") + } catch let failure as JSONTransformFailure { #expect(failure.category == .cancelled) } + #expect(calls == 0) +} +@Test func jtxSelectedAliasKeepsSourceReservationUntilLastHandleDies() throws { + let plan = try JSONTransformProgram.compile(".a") + let root = try JSON.parse("{\"a\":1,\"padding\":\"" + String(repeating: "x", count: 4096) + "\"}") + _ = try plan.withExecution(input: root) { execution in + var result = try execution.next() + #expect(result?.value == 1) + let retained = execution.statistics[.retainedBytes] + let end = try execution.next() + #expect(end == nil) + #expect(execution.statistics[.retainedBytes] > 4096) + result = nil + _ = try execution.next() // Drains ARC notifications, not work refunds. + #expect(execution.statistics[.retainedBytes] < retained - 4096) + } +} +@Test func jtxPlanIsSendableAndExecutionsHaveIndependentHealth() async throws { + let plan = try JSONTransformProgram.compile(". + 1") + func requireSendable(_: T) {} + requireSendable(plan) + try await withThrowingTaskGroup(of: Bool.self) { group in + for n in 0..<16 { + group.addTask { + let result = try plan.withExecution(input: .number(JSONNumber(integer: n))) { execution in + let output = try execution.next() + let end = try execution.next() + return output?.value == .number(JSONNumber(integer: n + 1)) && end == nil + } + return result.result && result.statistics[.workUnits] > 0 + } + } + for try await good in group { #expect(good) } + } +} +@Test func jtxConfiguredCompilerGrowthHasInclusiveBoundaries() throws { + for metric in [JSONTransformLimits.Metric.sourceBytes, .tokens, .planNodes, .depth] { + var limits = JSONTransformLimits(); limits[metric] = 1 + _ = try JSONTransformProgram.compile(".", limits: limits) + limits[metric] = 0 + do { _ = try JSONTransformProgram.compile(".", limits: limits); Issue.record("Expected compiler cap") } + catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == metric.rawValue) } + } +} diff --git a/Tools/TransformQualification/Package.swift b/Tools/TransformQualification/Package.swift new file mode 100644 index 0000000..c44c91c --- /dev/null +++ b/Tools/TransformQualification/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "TransformQualification", platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "TransformConsumer", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), + .product(name: "JSONTransform", package: "Swift-JSON") + ])], swiftLanguageModes: [.v6]) diff --git a/Tools/TransformQualification/README.md b/Tools/TransformQualification/README.md new file mode 100644 index 0000000..9d92447 --- /dev/null +++ b/Tools/TransformQualification/README.md @@ -0,0 +1,11 @@ +# Public transformation consumer + +This separate SwiftPM package imports JSON and JSONTransform only. It exercises +11 witnesses: zero/null/multiple results, exact numbers/scalar keys, plan reuse, +compile/language errors, cumulative provider limits, exact bindings, cancellation +and independent concurrent invocations. These are executable assertions, not +full dialect, platform or release qualification. + +Run `swift run --package-path Tools/TransformQualification TransformConsumer`. +The private return additionally copies this source into a fresh directory outside +the checkout, changing only its path dependency location. diff --git a/Tools/TransformQualification/Sources/TransformConsumer/Main.swift b/Tools/TransformQualification/Sources/TransformConsumer/Main.swift new file mode 100644 index 0000000..c809cb0 --- /dev/null +++ b/Tools/TransformQualification/Sources/TransformConsumer/Main.swift @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform + +func require(_ value: Bool) { precondition(value) } + +func collect(_ plan: JSONTransformProgram, input: JSON = .null) throws -> [JSON] { + try plan.withExecution(input: input) { execution in + var result: [JSON] = [] + while let output = try execution.next() { result.append(output.value) } + return result + }.result +} + +@main struct TransformConsumer { + static func main() async throws { + var witnesses = 0 + try require(collect(JSONTransformProgram.compile("empty")).isEmpty); witnesses += 1 + try require(collect(JSONTransformProgram.compile("null")) == [.null]); witnesses += 1 + let plan = try JSONTransformProgram.compile(".[] | . + 1") + let root = try JSON.parse("[9007199254740993,0.1]") + let expected = try JSON.parse("[9007199254740994,1.1]").arrayValue! + try require(collect(plan, input: root) == expected); witnesses += 1 + try require(collect(plan, input: root) == expected); witnesses += 1 + let scalar = try JSON.parse(#"{"é":1,"e\u0301":2}"#) + try require(collect(JSONTransformProgram.compile("length"), input: scalar) == [2]); witnesses += 1 + do { _ = try JSONTransformProgram.compile("["); preconditionFailure("compile accepted") } + catch let failure as JSONTransformFailure { precondition(failure.category == .compilation && failure.byteOffset == 0) }; witnesses += 1 + do { _ = try collect(JSONTransformProgram.compile("1/3")); preconditionFailure("division approximated") } + catch let failure as JSONTransformFailure { precondition(failure.category == .language) }; witnesses += 1 + var calls = 0 + let inputs = try JSONTransformProgram.compile("try inputs catch null") + var limits = JSONTransformLimits(); limits[.inputValues] = 2 + do { + _ = try inputs.withExecution(input: .null, limits: limits, source: { calls += 1; return .null }) { execution in + let first = try execution.next(); precondition(first?.value == .null) + precondition(execution.used(.inputValues) == 2 && execution.remainingWorkUnits > 0) + _ = try execution.next() + } + preconditionFailure("input cap reset") + } catch let failure as JSONTransformFailure { precondition(failure.category == .resource && failure.metric == "inputValues") } + precondition(calls == 2); witnesses += 1 + let binding = try JSONTransformProgram.compile("$v", variables: ["v"]) + let bound = try binding.withExecution(input: .null, bindings: ["v": root]) { execution in try execution.next()?.value } + precondition(bound.result == root && bound.statistics[.workUnits] > 0); witnesses += 1 + do { + _ = try inputs.withExecution(input: .null) { execution in execution.cancel(); _ = try execution.next() } + preconditionFailure("cancel accepted") + } catch let failure as JSONTransformFailure { precondition(failure.category == .cancelled) }; witnesses += 1 + try await withThrowingTaskGroup(of: Bool.self) { group in + for _ in 0..<8 { group.addTask { try collect(plan, input: root) == expected } } + for try await match in group { precondition(match) } + }; witnesses += 1 + print("{\"consumer\":\"JSON+JSONTransform\",\"witnesses\":\(witnesses),\"status\":\"passed\"}") + } +} From 9497d681bc7c736c631468225ef72243f078e103 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:35:55 +0200 Subject: [PATCH 09/25] Preserve borrowed-scope probe and Swift 6.4 escape failure --- .../Architecture/Release1.7/JSONTransform.md | 12 ++++ Scripts/qualify_transform_ownership.py | 32 ++++++++++ Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../JSONTransformExecution.swift | 64 +++++++++++++------ .../JSONTransform/JSONTransformProgram.swift | 6 +- .../JSONTransform/DoubleConsume.swift | 8 +++ .../JSONTransform/EscapeScope.swift | 8 +++ .../JSONTransform/ReplaceScope.swift | 8 +++ .../JSONTransform/SendExecution.swift | 4 ++ 9 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 Scripts/qualify_transform_ownership.py create mode 100644 Tests/CompileNegative/JSONTransform/DoubleConsume.swift create mode 100644 Tests/CompileNegative/JSONTransform/EscapeScope.swift create mode 100644 Tests/CompileNegative/JSONTransform/ReplaceScope.swift create mode 100644 Tests/CompileNegative/JSONTransform/SendExecution.swift diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md index 2109793..f9f62e6 100644 --- a/Documentation/Architecture/Release1.7/JSONTransform.md +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -45,3 +45,15 @@ their exact source revisions; a working spike is not full release qualification. JTX-2 through JTX-7 remain separate qualification gates. No merge, tag, publication, Linux/WASI runtime support, complete conformance, controlled performance or final API acceptance follows from this checkpoint. + +## Rejected ownership experiments + +The first inout execution callback compiled but allowed nested scope swapping: +two results escaped an outer results cap of one. Its local prototype checkpoint +is retained as rejected; its green behavior tests do not qualify ownership. +A noncopyable borrowing facade prevented replacement and passed eleven behavior +tests. However Swift 6.4 (swiftlang-6.4.0.34.1) crashed with exit -6 in SILGenCleanup +when a callback captured the borrowed facade into an escaping closure. That is +a failed compiler gate, not a valid negative diagnostic or a compiler fix. +The next bounded experiment is a guarded, non-Sendable reference handle; copies +must share one state and scope exit must invalidate it and release root/provider. diff --git a/Scripts/qualify_transform_ownership.py b/Scripts/qualify_transform_ownership.py new file mode 100644 index 0000000..094a534 --- /dev/null +++ b/Scripts/qualify_transform_ownership.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Compile actual public transformation ownership boundaries, never import failures.""" +import argparse, hashlib, json, pathlib, subprocess, sys +ROOT = pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + parser.add_argument('--modules', default=str(ROOT / '.build/debug')) + parser.add_argument('--module-cache') + args = parser.parse_args() + output = pathlib.Path(args.output).resolve(); output.mkdir(parents=True, exist_ok=True) + cache = pathlib.Path(args.module_cache).resolve() if args.module_cache else output / 'ModuleCache' + sdk = subprocess.check_output(['xcrun', '--sdk', 'macosx', '--show-sdk-path'], text=True).strip() + base = ['xcrun', 'swiftc', '-swift-version', '6', '-sdk', sdk, '-target', 'arm64-apple-macos27.0', '-module-cache-path', str(cache), '-I', str(pathlib.Path(args.modules).resolve()), '-c'] + positive = output / 'PublicConsumer.swift' + positive.write_text('import JSON\nimport JSONTransform\nfunc send(_: T) {}\nfunc check(_ p: JSONTransformProgram) throws { send(p); _ = try p.withExecution(input: .null) { e in try e.next()?.value } }\n') + cases = [('public-consumer', positive, True, None)] + for name, diagnostic in [('ReplaceScope', 'cannot pass immutable value as inout argument'), ('DoubleConsume', 'consumed more than once'), ('SendExecution', 'does not conform to the'), ('EscapeScope', 'escaping closure')]: + cases.append((name, ROOT / 'Tests/CompileNegative/JSONTransform' / (name + '.swift'), False, diagnostic)) + rows = [] + for name, source, positive_case, diagnostic in cases: + command = base + [str(source), '-o', str(output / (name + '.o'))] + result = subprocess.run(command, capture_output=True, text=True) + log = output / (name + '.log'); log.write_text(result.stdout + result.stderr) + expected = result.returncode == 0 if positive_case else result.returncode != 0 and diagnostic in result.stderr and 'no such module' not in result.stderr + rows.append(dict(id=name, command=command, exit_code=result.returncode, expected_diagnostic=diagnostic, status='passed' if expected else 'failed', source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + receipt = dict(status='passed' if all(r['status'] == 'passed' for r in rows) else 'failed', observed=5, cases=rows) + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps(receipt)); 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 a244ffd..b9d1c0c 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:5360a44f2590f398047bafb8b0732b531ea7c65d3396b035ec4c290e7207e1e3" + package static let revision = "sha256:c93136e105fcba6d9e595177c27b17f8d7e44d18d1feaccf93f00579055d9bc1" package static let kind = "source-sha256" } diff --git a/Sources/JSONTransform/JSONTransformExecution.swift b/Sources/JSONTransform/JSONTransformExecution.swift index 98ad253..623a44b 100644 --- a/Sources/JSONTransform/JSONTransformExecution.swift +++ b/Sources/JSONTransform/JSONTransformExecution.swift @@ -32,11 +32,37 @@ private struct SuppliedInput: FilterInput { } } -/// A noncopyable, non-Sendable pull execution borrowed inside `withExecution`. +/// A borrowed noncopyable, non-Sendable pull facade for one execution scope. /// -/// All pulls, provider values and results share one finite meter. No mutable state -/// is retained in the Sendable plan. Arbitrary callback work is not instrumented. +/// The private state owns one cumulative meter. A borrowing callback cannot swap +/// or replace this facade with another invocation. Only plans are Sendable. public struct JSONTransformExecution: ~Copyable { + private let state: TransformExecutionState + private init(state: TransformExecutionState) { self.state = state } + static func make(plan: JSONTransformProgram, input: JSON, bindings: [String: JSON], + limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> Self { + .init(state: try TransformExecutionState.make(plan: plan, input: input, bindings: bindings, limits: limits, source: source)) + } + /// Pulls the next ordered output, or nil at completed evaluation. + /// + /// Resource, provider and cancellation failures latch and bypass language + /// catches. A present output containing JSON null is distinct from EOF. + public func next() throws -> JSONTransformOutput? { try state.next() } + /// Current native cumulative statistics, including after a failed pull. + public var statistics: JSONOperationStatistics { state.statistics } + /// Remaining shared work; arbitrary caller instructions are not capped. + public var remainingWorkUnits: UInt64 { state.remainingWorkUnits } + /// Reads a live or cumulative transform counter without exporting a meter. + /// - Parameter metric: The counter whose usage to inspect. + public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { state.used(metric) } + /// Latches cancellation for later pulls and scope completion. + public func cancel() { state.cancel() } + func checkCompletion() throws { try state.checkCompletion() } + func close() throws { try state.close() } + func recordCleanupFailure(_ error: any Error) { state.recordCleanupFailure(error) } +} + +private final class TransformExecutionState { private var context: TransformRuntimeContext private var vm: FilterVM? private var input: SuppliedInput @@ -53,15 +79,16 @@ public struct JSONTransformExecution: ~Copyable { vm = nil } static func make(plan: JSONTransformProgram, input root: JSON, bindings: [String: JSON], - limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> Self { + limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> TransformExecutionState { do { - let context = try TransformRuntimeContext(limits: limits.runtime) - var execution = Self(context: consume context, source: source) + var context = try TransformRuntimeContext(limits: limits.runtime) + try context.meter.retain(128) // Fixed invocation state, before allocation. + let execution = TransformExecutionState(context: consume context, source: source) try execution.prepare(plan: plan, root: root, bindings: bindings) - return consume execution + return execution } catch { throw JSONTransformFailure.classify(error) } } - private mutating func prepare(plan: JSONTransformProgram, root: JSON, bindings: [String: JSON]) throws { + private func prepare(plan: JSONTransformProgram, root: JSON, bindings: [String: JSON]) throws { do { try context.meter.retain(plan.planReservation) planReservation = plan.planReservation @@ -86,11 +113,12 @@ public struct JSONTransformExecution: ~Copyable { /// /// A resource, provider or cancellation failure latches and bypasses query /// catches. Later pulls cannot create a fresh successful execution. - public mutating func next() throws -> JSONTransformOutput? { + func next() throws -> JSONTransformOutput? { do { try context.checkpoint() if let failure { throw failure } - guard !closed, var active = vm else { return nil } + guard !closed else { throw JSONTransformFailure(.configuration, "scopeClosed") } + guard var active = vm else { return nil } vm = nil defer { vm = active } guard let value = try active.next(input: &input, context: &context) else { return nil } @@ -104,28 +132,28 @@ public struct JSONTransformExecution: ~Copyable { } } /// Current cumulative native statistics, also available after a failed pull. - public var statistics: JSONOperationStatistics { context.meter.statistics() } + var statistics: JSONOperationStatistics { context.meter.statistics() } /// Remaining shared owned work; this is not a caller instruction quota. - public var remainingWorkUnits: UInt64 { + var remainingWorkUnits: UInt64 { let cap = min(context.limits.core[.workUnits], context.limits[.totalWorkUnits]) return cap - min(cap, context.used(.totalWorkUnits)) } /// Reads a transform counter without exposing its mutable meter. /// - Parameter metric: The cumulative or live transform counter. - public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { context.used(metric.runtime) } + func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { context.used(metric.runtime) } /// Latches cancellation. The next pull and scope completion both throw. - public mutating func cancel() { + func cancel() { let cancelled = JSONTransformFailure(.cancelled, "cancelled") if failure == nil { failure = cancelled } context.latch(cancelled) } - mutating func checkCompletion() throws { + func checkCompletion() throws { try context.checkpoint() if let failure { throw failure } if let cleanupFailure { throw cleanupFailure } } - mutating func recordCleanupFailure(_ error: any Error) { cleanupFailure = .classify(error) } - mutating func close() throws { + func recordCleanupFailure(_ error: any Error) { cleanupFailure = .classify(error) } + func close() throws { guard !closed else { return } closed = true input.source = nil @@ -133,7 +161,7 @@ public struct JSONTransformExecution: ~Copyable { try context.reclaimValueLeases() // Remaining compilation/binding reservations end with the owner scope; // returned output handles retain their own deferred lease reservations. - try context.meter.release(planReservation + bindingReservation) + try context.meter.release(planReservation + bindingReservation + 128) try context.release(.bindings, initialBindings) planReservation = 0; bindingReservation = 0; initialBindings = 0 } diff --git a/Sources/JSONTransform/JSONTransformProgram.swift b/Sources/JSONTransform/JSONTransformProgram.swift index 821f198..9098d5e 100644 --- a/Sources/JSONTransform/JSONTransformProgram.swift +++ b/Sources/JSONTransform/JSONTransformProgram.swift @@ -44,10 +44,10 @@ public struct JSONTransformProgram: Sendable { public func withExecution(input: JSON, bindings: [String: JSON] = [:], limits: JSONTransformLimits = .init(), source: (() throws -> JSON?)? = nil, - _ body: (inout JSONTransformExecution) throws -> Result) throws -> JSONOperationReport { - var execution = try JSONTransformExecution.make(plan: self, input: input, bindings: bindings, limits: limits, source: source) + _ body: (borrowing JSONTransformExecution) throws -> Result) throws -> JSONOperationReport { + let execution = try JSONTransformExecution.make(plan: self, input: input, bindings: bindings, limits: limits, source: source) let result: Result - do { result = try body(&execution) } + do { result = try body(execution) } catch { // Caller callback errors keep their identity. Cleanup cannot replace // the originating failure or reopen the operation ledger. diff --git a/Tests/CompileNegative/JSONTransform/DoubleConsume.swift b/Tests/CompileNegative/JSONTransform/DoubleConsume.swift new file mode 100644 index 0000000..08e80fa --- /dev/null +++ b/Tests/CompileNegative/JSONTransform/DoubleConsume.swift @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSONTransform +func duplicate(_ execution: consuming JSONTransformExecution) { + let first = consume execution + let second = consume execution + _ = consume first + _ = consume second +} diff --git a/Tests/CompileNegative/JSONTransform/EscapeScope.swift b/Tests/CompileNegative/JSONTransform/EscapeScope.swift new file mode 100644 index 0000000..a7f887e --- /dev/null +++ b/Tests/CompileNegative/JSONTransform/EscapeScope.swift @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform +func escapeScope(_ plan: JSONTransformProgram) throws { + var saved: (() throws -> JSONTransformOutput?)? + _ = try plan.withExecution(input: .null) { execution in saved = { try execution.next() } } + _ = try saved?() +} diff --git a/Tests/CompileNegative/JSONTransform/ReplaceScope.swift b/Tests/CompileNegative/JSONTransform/ReplaceScope.swift new file mode 100644 index 0000000..ea22993 --- /dev/null +++ b/Tests/CompileNegative/JSONTransform/ReplaceScope.swift @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform +func replaceScope(_ plan: JSONTransformProgram) throws { + _ = try plan.withExecution(input: .null) { outer in + _ = try plan.withExecution(input: .null) { inner in swap(&outer, &inner) } + } +} diff --git a/Tests/CompileNegative/JSONTransform/SendExecution.swift b/Tests/CompileNegative/JSONTransform/SendExecution.swift new file mode 100644 index 0000000..e4c9dc5 --- /dev/null +++ b/Tests/CompileNegative/JSONTransform/SendExecution.swift @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSONTransform +func requireSendable(_ value: borrowing T) {} +func sendExecution(_ execution: borrowing JSONTransformExecution) { requireSendable(execution) } From 6c63ee99bd49a2a1cbc919de003d25a834d881df Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:43:00 +0200 Subject: [PATCH 10/25] Guard execution lifetime and preserve one cumulative ledger across handle copies --- AGENTS.md | 8 ++- CHANGELOG.md | 8 +++ .../Architecture/ExtractionManifest.json | 9 +-- .../Architecture/Release1.7/JSONTransform.md | 17 +++++- .../Qualification/TestDiscovery.json | 6 +- README.md | 16 +++++- Scripts/build_documentation_site.py | 6 +- Scripts/documentation_catalog.py | 2 +- Scripts/qualify_transform_ownership.py | 4 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/JSONTransform.md | 22 ++++++++ .../Documentation.docc/ScopedTransforms.md | 56 +++++++++++++++++++ .../JSONTransformExecution.swift | 50 +++++------------ .../JSONTransform/JSONTransformProgram.swift | 6 +- .../JSONTransform/DoubleConsume.swift | 8 --- .../JSONTransform/EscapeScope.swift | 0 .../JSONTransform/HandleAliases.swift | 3 + .../TransformSpikeTests.swift | 33 +++++++++++ 18 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 Sources/JSONTransform/Documentation.docc/JSONTransform.md create mode 100644 Sources/JSONTransform/Documentation.docc/ScopedTransforms.md delete mode 100644 Tests/CompileNegative/JSONTransform/DoubleConsume.swift rename Tests/{CompileNegative => CompilePositive}/JSONTransform/EscapeScope.swift (100%) create mode 100644 Tests/CompilePositive/JSONTransform/HandleAliases.swift diff --git a/AGENTS.md b/AGENTS.md index 2aebea7..c148b5d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,8 +8,8 @@ Swift-JSON requires Swift tools/compiler 6.4+, Swift language mode 6 and macOS/SDK 27+. Read README.md, CONTRIBUTING.md and the relevant contracts in Documentation/Architecture before changing behavior. -- Keep ten public products in this source: JSON, JSONSchema, JSONSchemaExamples, JSONStreaming, - JSONPatch, JSONHTML, JSONBridges, JSONNavigation, JSONInspection and the swift-json executable. +- Keep eleven public products in this feature source: JSON, JSONSchema, JSONSchemaExamples, JSONStreaming, + JSONPatch, JSONHTML, JSONBridges, JSONNavigation, JSONInspection, JSONTransform and the swift-json executable. JSONSchemaExamples is opt-in and depends on JSON and JSONSchema only. JSONFilterRuntime and JSONCLIEngine are package-private. JSONHTML and opt-in JSONBridges depend on JSON only; core never re-exports the bridges. @@ -44,3 +44,7 @@ Documentation/Architecture before changing behavior. Scalar scanning and immutable plan-local regex sharing are selected private optimizations. Source-attached N3 snapshots and P1 tree/sparse-table projections live in JSONInspection. P2 graph/store/set/matrix/retrieval/persistence and JSON text-sequence framing remain deferred. + +JSONTransform is a provisional feature-branch extraction with one private compiler/VM. +It depends on JSON and the package-private JSONFilterRuntime kernel. The CLI borrows +the same ledger through its package adapter. No release or final API freeze is implied. diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf5f57..dacc0b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Provisional JSONTransform extraction + +- Add an optional public compiler/pull API over the single existing private query kernel. +- Preserve CLI ownership through package-only cumulative adapters; add real-engine + lifecycle, exactness, provider and typed failure tests and a standalone consumer. +- Retain rejected inout substitution and Swift 6.4 borrowed-capture compiler failure + evidence; the guarded handle API and release qualification remain provisional. + ## 1.7.0 — Unreleased - Retain admitted opt-in JSONInspection: observation-only array evidence, exact diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 4a191c4..cc6b91b 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -17,7 +17,7 @@ "blob_sha": "25cb8eab982ed0fc41e5f1dde5abeab18113f55e", "destination": "Sources/JSON/Value/JSON.swift", "disposition": "Six enum case concepts selectively adapted; equality/hash traversal and exact object/key storage rewritten.", - "attribution": "Original Thor Fuchs 2024–2026 header retained; Reference LICENSE and NOTICE preserved.", + "attribution": "Original Thor Fuchs 2024\u20132026 header retained; Reference LICENSE and NOTICE preserved.", "validation": [ "Tests/JSONTests/ValueTests.swift", "Tests/JSONTests/PresenceTests.swift" @@ -163,7 +163,7 @@ "blob_sha": null, "destination": "Sources/JSONRegexRuntime/RegexUnicodeTables.swift", "disposition": "407 immutable tables generated by original deterministic Python generator from fixed Unicode data; no generated Regress table copied. Regeneration compares bytes and provenance.", - "attribution": "© 2020 Unicode, Inc.; Unicode-3.0 copyright/permission notice retained in Licenses/Unicode-3.0.txt.", + "attribution": "\u00a9 2020 Unicode, Inc.; Unicode-3.0 copyright/permission notice retained in Licenses/Unicode-3.0.txt.", "validation": "Byte-equal regeneration and native exact-alias/GC/binary/Script/Script_Extensions/IdentifierName vectors", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" }, @@ -174,7 +174,7 @@ "blob_sha": null, "destination": "Sources/JSONSchema/Formats/JSONIDNAData.swift and Tests/Fixtures/Unicode/NormalizationTest-13.0.0.txt", "disposition": "Original generator derives RFC5892/5893, NFC and UTS46 nontransitional STD3 tables; normalization fixture copied byte-for-byte. No external engine or system Unicode authority.", - "attribution": "© 2019–2020 Unicode, Inc.; Unicode-3.0 notice preserved in Licenses/Unicode-3.0.txt.", + "attribution": "\u00a9 2019\u20132020 Unicode, Inc.; Unicode-3.0 notice preserved in Licenses/Unicode-3.0.txt.", "validation": "18853 normalization cases (94265 native comparisons); 874 independent optional format cases; byte-equal regeneration", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" }, @@ -302,7 +302,8 @@ "NAV5 public array-evidence packaging, external consumer and DocC are original Apache-2.0 code. Existing exact shape evidence, exclusive reducer, weights and I01-I17 semantics are unchanged; no upstream engine or dependency is adapted. Promotion is a draft candidate, not a released product.", "NAV5 package-only complete structural snapshot, parent-linked metadata, original source-ownership transfer and S01-S24 synthetic tests are original Apache-2.0 code. The existing read scope admits once; its default public read/traversal/inspection completion behavior is preserved. No persistent identity, upstream algorithm or new dependency is introduced.", "NAV6 snapshot access-level promotion, external consumer, independent 28-record maintainer oracle, terminal-transfer/concurrent lookup proofs and compiled DocC are original Apache-2.0 contributions. No upstream engine, new semantic authority, dependency or additional product is adapted.", - "NAV8 current-main N3/P1 reconciliation, minimal public immutable projection API, independent 16-case consumer and DocC/access-control proofs are original Apache-2.0 contributions. NAV7 donor db991c5/bda8aef and packet SHA-256 57ca72cb817272ed466a7cb8810043d56a079190d830ea7b995abd308070f98d retain provenance. No external engine, dependency or product is added." + "NAV8 current-main N3/P1 reconciliation, minimal public immutable projection API, independent 16-case consumer and DocC/access-control proofs are original Apache-2.0 contributions. NAV7 donor db991c5/bda8aef and packet SHA-256 57ca72cb817272ed466a7cb8810043d56a079190d830ea7b995abd308070f98d retain provenance. No external engine, dependency or product is added.", + "JSONTransform provisional public facade, shared private context naming, guarded invocation handle and tests are original Apache-2.0. The existing query compiler/VM algorithms are reused unchanged; no external interpreter, subprocess wrapper, input packet source or dependency is adapted." ], "jsonhtml_export_original_contributions": { "license": "Apache-2.0", diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md index f9f62e6..c03fffd 100644 --- a/Documentation/Architecture/Release1.7/JSONTransform.md +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -4,8 +4,8 @@ This feature-branch API is provisional. Stable 1.6.1 and development main remain separate authorities. This source adds JSONTransform to the nine development libraries; it does not remove JSONInspection or import another candidate. -The chosen spike uses one private JSONFilterRuntime kernel. JSONTransform owns an -opaque Sendable `JSONTransformProgram` and scoped noncopyable +The chosen extraction prototype uses one private JSONFilterRuntime kernel. JSONTransform owns an +opaque Sendable `JSONTransformProgram` and scoped guarded `JSONTransformExecution`. An execution pulls `JSONTransformOutput?`: nil means EOF, while a present handle may contain JSON null. Handles share source leases and are not Sendable. JSON extracted from a handle is caller-owned; the scope's logical @@ -57,3 +57,16 @@ when a callback captured the borrowed facade into an escaping closure. That is a failed compiler gate, not a valid negative diagnostic or a compiler fix. The next bounded experiment is a guarded, non-Sendable reference handle; copies must share one state and scope exit must invalidate it and release root/provider. + +## Guarded-handle successor + +The current provisional API uses a final, non-Sendable reference handle. Copies +share exactly one private noncopyable ledger. Callback parameters cannot substitute +the invocation owner. Scope exit invalidates next(), clears provider/VM state and +releases reservations; observation remains available. This trades static lifetime +rejection for explicit guarded invalidation. Thirteen real-engine behavior cases +include same-ledger aliases, escaped-handle invalidation and weak provider release. +The rejected noncopyable compiler crash remains an unresolved toolchain finding. +The full native/safety/docs/Linux/WASI/performance/reproduction gates remain open; +source-bound receipts, including failures, are in the private return. Do not infer +release readiness or final API freeze from the prototype. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 1c0679f..a9bd094 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -13,7 +13,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 11 + "JSONTransformTests": 13 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -80,7 +80,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 11 + "JSONTransformTests": 13 }, "wasi_root_functions": { "JSONTests": 190, @@ -94,7 +94,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 11 + "JSONTransformTests": 13 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { diff --git a/README.md b/README.md index d25fe73..fda67e5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This source is **1.7.0-dev**, the unreleased development line. It retains the admitted JSONInspection array-evidence API plus source-attached snapshots and -tree/sparse-table projections alongside the unchanged 56-declaration Navigation API: nine libraries, ten products and nine catalogs. +tree/sparse-table projections alongside the unchanged 56-declaration Navigation API. This feature candidate also prototypes JSONTransform: ten libraries, eleven products and ten source catalogs. The stable 1.6 line has eight libraries and eight catalogs. Do not use a main snapshot as a 1.6 maintenance release. @@ -41,8 +41,8 @@ DocC and qualification assets for the stable reference; see the Historical N0/N1 plans and later candidates are distinguished in the [source authority map](Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md). -This 1.7.0-dev source includes opt-in JSONInspection: nine libraries plus the CLI, -ten products in total. Inspection is absent from stable 1.6 releases. Use this +This feature source includes opt-in JSONInspection and provisional JSONTransform: +ten libraries plus the CLI, eleven products in total. Inspection is absent from stable 1.6 releases. Use this source checkout for Inspection; its inclusion in main does not publish 1.7.0. The 1.5.1 maintenance repairs remain included: best-effort schema diagnostics, @@ -101,6 +101,7 @@ Choose the products your application needs: | **swift-json** | Bounded command-line JSON workflows (available since 1.5.0) | | **JSONBridges** | Opt-in native integer, Double and Foundation Decimal conversions (available in the 1.4.0 tag and 1.5.0 release) | | **JSONNavigation** | Opt-in exact many-node DFS, physical/virtual scopes, finite metadata and explicit coverage, introduced in 1.6.0 | +| **JSONTransform** | Provisional feature: exact query plans, guarded pull execution and finite cumulative limits; see the [extraction checkpoint](Documentation/Architecture/Release1.7/JSONTransform.md) | | **JSONInspection** | Unreleased source: array evidence, complete source-attached snapshots, selected trees and strict sparse object-row tables | | **JSONPatch** | Staged RFC 6902 JSON Patch and RFC 7396 Merge Patch | @@ -714,3 +715,12 @@ location/value/span lookup is an explicitly separate full-source-admitted operat Ordinals are snapshot-local trusted positions, not durable identity. Logical retention is not RSS; G4 controlled performance remains HOLD, with no speed claim. These additions are absent from published 1.6.1 and do not publish 1.7.0. + +## Provisional public transformation library + +The JSONTransform feature candidate exposes the existing query compiler and pull VM +without CLI imports. The CLI uses the same engine. Plans are immutable and Sendable; +scoped handles share one cumulative ledger and invalidate on scope exit. The public +[guide](Sources/JSONTransform/Documentation.docc/ScopedTransforms.md) and +[standalone consumer](Tools/TransformQualification) document the provisional API. +This product is absent from stable 1.6.1 and remains subject to full qualification. diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index 1efbf7d..e9a5dfa 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -30,7 +30,8 @@ 'JSONHTML': ('Exact interactive HTML', 'interactiverendering'), 'JSONBridges': ('Opt-in native conversions', 'numericfidelity'), 'JSONNavigation': ('Exact bounded structural traversal', 'traversal'), - 'JSONInspection': ('Observed array evidence', 'arrayevidence')} + 'JSONInspection': ('Observed array evidence', 'arrayevidence'), + 'JSONTransform': ('Scoped exact transformations', 'scopedtransforms')} DESCRIPTIONS = { 'JSON': 'Exact values, strict UTF-8, native Codable, Pointer, presence and finite resource reports.', 'JSONSchema': 'Draft 2020-12, explicit offline registries, immutable plans, annotations and bounded regex.', @@ -40,7 +41,8 @@ 'JSONHTML': 'Exact standalone HTML with offline disclosure/search and native-byte copy/download.', 'JSONBridges': 'Opt-in trusted integer construction and policy-explicit Double and Foundation Decimal projection.', 'JSONNavigation': 'Exact typed DFS with physical and virtual scopes, explicit coverage and one cumulative budget.', - 'JSONInspection': 'Array evidence, complete source-attached snapshots, tree views and strict sparse tables with finite reports.'} + 'JSONInspection': 'Array evidence, complete source-attached snapshots, tree views and strict sparse tables with finite reports.', + 'JSONTransform': 'Provisional exact query plans and guarded pull executions with cumulative finite limits.'} class Links(HTMLParser): diff --git a/Scripts/documentation_catalog.py b/Scripts/documentation_catalog.py index 06c5267..078673d 100644 --- a/Scripts/documentation_catalog.py +++ b/Scripts/documentation_catalog.py @@ -7,7 +7,7 @@ import urllib.parse ROOT = pathlib.Path(__file__).resolve().parents[1] -PRODUCTS = ('JSON', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation', 'JSONInspection') +PRODUCTS = ('JSON', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation', 'JSONInspection', 'JSONTransform') def public_symbols(graph_dir): diff --git a/Scripts/qualify_transform_ownership.py b/Scripts/qualify_transform_ownership.py index 094a534..a1e8f5d 100644 --- a/Scripts/qualify_transform_ownership.py +++ b/Scripts/qualify_transform_ownership.py @@ -17,8 +17,10 @@ def main(): positive = output / 'PublicConsumer.swift' positive.write_text('import JSON\nimport JSONTransform\nfunc send(_: T) {}\nfunc check(_ p: JSONTransformProgram) throws { send(p); _ = try p.withExecution(input: .null) { e in try e.next()?.value } }\n') cases = [('public-consumer', positive, True, None)] - for name, diagnostic in [('ReplaceScope', 'cannot pass immutable value as inout argument'), ('DoubleConsume', 'consumed more than once'), ('SendExecution', 'does not conform to the'), ('EscapeScope', 'escaping closure')]: + for name, diagnostic in [('ReplaceScope', 'cannot pass immutable value as inout argument'), ('SendExecution', 'does not conform to the')]: cases.append((name, ROOT / 'Tests/CompileNegative/JSONTransform' / (name + '.swift'), False, diagnostic)) + for name in ['EscapeScope', 'HandleAliases']: + cases.append((name, ROOT / 'Tests/CompilePositive/JSONTransform' / (name + '.swift'), True, None)) rows = [] for name, source, positive_case, diagnostic in cases: command = base + [str(source), '-o', str(output / (name + '.o'))] diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index b9d1c0c..cd6f8ff 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:c93136e105fcba6d9e595177c27b17f8d7e44d18d1feaccf93f00579055d9bc1" + package static let revision = "sha256:b2c799359d2555c76f9d6223a8f9bea88e6da8e13ec77b877b7cb6860d260426" package static let kind = "source-sha256" } diff --git a/Sources/JSONTransform/Documentation.docc/JSONTransform.md b/Sources/JSONTransform/Documentation.docc/JSONTransform.md new file mode 100644 index 0000000..eb28700 --- /dev/null +++ b/Sources/JSONTransform/Documentation.docc/JSONTransform.md @@ -0,0 +1,22 @@ +# ``JSONTransform`` + +Compile the existing exact query dialect and pull bounded native JSON results. + +This is a provisional 1.7.0 feature-branch product. It shares one private compiler +and VM with `swift-json query`; it does not introduce a new interpreter or numeric +authority. Plans are immutable and Sendable. Each synchronous invocation has one +finite cumulative meter and a guarded, non-Sendable execution handle. + +## Topics + +### Start an invocation + +- +- ``JSONTransformProgram`` +- ``JSONTransformExecution`` +- ``JSONTransformOutput`` + +### Limits and failures + +- ``JSONTransformLimits`` +- ``JSONTransformFailure`` diff --git a/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md b/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md new file mode 100644 index 0000000..a31d082 --- /dev/null +++ b/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md @@ -0,0 +1,56 @@ +# Pull exact transformation results + +Compile explicit source, then pull zero, one or many ordered native values. + +```swift +import JSON +import JSONTransform + +let plan = try JSONTransformProgram.compile(".[] | . + 1") +let root = try JSON.parse("[9007199254740993,0.1]") +let report = try plan.withExecution(input: root) { execution in + var values: [JSON] = [] + while let output = try execution.next() { values.append(output.value) } + return values +} +``` + +`empty` yields no result. `null` yields one present output containing JSON null. +The plan uses the same exact decimal and decoded-scalar rules as JSON and the CLI. +Nonterminating division is a language error; no approximate arithmetic is added. +The standalone public consumer at `Tools/TransformQualification` is available +in this feature candidate; it is absent from stable 1.6.1. + +## Ownership and completion + +All copies of an execution handle share one meter. A handle cannot replace the +owner of another invocation. Scope exit clears frames and the provider, releases +owned root reservations and invalidates data access through escaped handles. +Escaping is permitted but `next()` afterwards throws `configuration/scopeClosed`. +Only plans are Sendable; handles and output leases are not. The native compiler +rejected an initializer shape and crashed on a rejected borrowed escaping-closure +experiment; neither result is represented as a compiler fix or successful negative. + +Keep each output handle alive while using its accounted source alias. A JSON copy +extracted from it, or a value handed off when the scope ends, is caller-owned. +A callback may stop early. Transform completion does not drain or certify an +external source: the CLI separately checks syntax, closing boundaries and EOF. + +## Finite work and explicit providers + +`JSONTransformLimits` supplies finite native and transform ceilings. Compilation +has its own bounded ledger. Each execution conservatively reserves compiler-held +plan bytes and shares work across root admission, pulls, provider values and +results. `used(_:)`, `statistics` and `remainingWorkUnits` expose cumulative counts +without exporting a mutable meter. Logical bytes are not RSS. Provider callback +instructions and upstream allocations are caller work. + +Use `variables` at compilation and matching exact `bindings` at execution. +The optional `source` callback returns `JSON?`: nil means provider EOF; `.null` +is an input value. Only that callback supplies `input` and `inputs`. The engine +opens no files and reads no ambient process input. Async providers are not offered. + +`JSONTransformFailure` distinguishes compilation, language, resource, cancellation, +provider and configuration errors. Operational failures bypass language catches +and latch. Catching a failed pull cannot yield a successful operation report. +`cancel()` likewise prevents later pulls and successful scope completion. diff --git a/Sources/JSONTransform/JSONTransformExecution.swift b/Sources/JSONTransform/JSONTransformExecution.swift index 623a44b..db28d52 100644 --- a/Sources/JSONTransform/JSONTransformExecution.swift +++ b/Sources/JSONTransform/JSONTransformExecution.swift @@ -32,37 +32,12 @@ private struct SuppliedInput: FilterInput { } } -/// A borrowed noncopyable, non-Sendable pull facade for one execution scope. +/// A guarded, non-Sendable pull handle borrowed by one synchronous scope. /// -/// The private state owns one cumulative meter. A borrowing callback cannot swap -/// or replace this facade with another invocation. Only plans are Sendable. -public struct JSONTransformExecution: ~Copyable { - private let state: TransformExecutionState - private init(state: TransformExecutionState) { self.state = state } - static func make(plan: JSONTransformProgram, input: JSON, bindings: [String: JSON], - limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> Self { - .init(state: try TransformExecutionState.make(plan: plan, input: input, bindings: bindings, limits: limits, source: source)) - } - /// Pulls the next ordered output, or nil at completed evaluation. - /// - /// Resource, provider and cancellation failures latch and bypass language - /// catches. A present output containing JSON null is distinct from EOF. - public func next() throws -> JSONTransformOutput? { try state.next() } - /// Current native cumulative statistics, including after a failed pull. - public var statistics: JSONOperationStatistics { state.statistics } - /// Remaining shared work; arbitrary caller instructions are not capped. - public var remainingWorkUnits: UInt64 { state.remainingWorkUnits } - /// Reads a live or cumulative transform counter without exporting a meter. - /// - Parameter metric: The counter whose usage to inspect. - public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { state.used(metric) } - /// Latches cancellation for later pulls and scope completion. - public func cancel() { state.cancel() } - func checkCompletion() throws { try state.checkCompletion() } - func close() throws { try state.close() } - func recordCleanupFailure(_ error: any Error) { state.recordCleanupFailure(error) } -} - -private final class TransformExecutionState { +/// Copies share one invocation and cannot reset its cumulative meter. Scope exit +/// releases the provider and VM and invalidates data access through escaped +/// handles. Reports remain observable. Only immutable plans are Sendable. +public final class JSONTransformExecution { private var context: TransformRuntimeContext private var vm: FilterVM? private var input: SuppliedInput @@ -79,11 +54,11 @@ private final class TransformExecutionState { vm = nil } static func make(plan: JSONTransformProgram, input root: JSON, bindings: [String: JSON], - limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> TransformExecutionState { + limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> JSONTransformExecution { do { var context = try TransformRuntimeContext(limits: limits.runtime) try context.meter.retain(128) // Fixed invocation state, before allocation. - let execution = TransformExecutionState(context: consume context, source: source) + let execution = JSONTransformExecution(context: consume context, source: source) try execution.prepare(plan: plan, root: root, bindings: bindings) return execution } catch { throw JSONTransformFailure.classify(error) } @@ -113,7 +88,7 @@ private final class TransformExecutionState { /// /// A resource, provider or cancellation failure latches and bypasses query /// catches. Later pulls cannot create a fresh successful execution. - func next() throws -> JSONTransformOutput? { + public func next() throws -> JSONTransformOutput? { do { try context.checkpoint() if let failure { throw failure } @@ -132,17 +107,18 @@ private final class TransformExecutionState { } } /// Current cumulative native statistics, also available after a failed pull. - var statistics: JSONOperationStatistics { context.meter.statistics() } + public var statistics: JSONOperationStatistics { context.meter.statistics() } /// Remaining shared owned work; this is not a caller instruction quota. - var remainingWorkUnits: UInt64 { + public var remainingWorkUnits: UInt64 { let cap = min(context.limits.core[.workUnits], context.limits[.totalWorkUnits]) return cap - min(cap, context.used(.totalWorkUnits)) } /// Reads a transform counter without exposing its mutable meter. /// - Parameter metric: The cumulative or live transform counter. - func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { context.used(metric.runtime) } + public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { context.used(metric.runtime) } /// Latches cancellation. The next pull and scope completion both throw. - func cancel() { + public func cancel() { + guard !closed else { return } let cancelled = JSONTransformFailure(.cancelled, "cancelled") if failure == nil { failure = cancelled } context.latch(cancelled) diff --git a/Sources/JSONTransform/JSONTransformProgram.swift b/Sources/JSONTransform/JSONTransformProgram.swift index 9098d5e..843a95b 100644 --- a/Sources/JSONTransform/JSONTransformProgram.swift +++ b/Sources/JSONTransform/JSONTransformProgram.swift @@ -29,9 +29,9 @@ public struct JSONTransformProgram: Sendable { } catch { throw JSONTransformFailure.classify(error) } } - /// Runs a synchronous, invocation-owned throwing pull scope. + /// Runs a synchronous, invocation-owned guarded pull scope. /// - /// A present null result is distinct from EOF. The scope releases frames, + /// A present null result is distinct from EOF. Copies of the execution handle share one cumulative ledger. The scope releases frames, /// provider and owned bindings on exit. Extracted JSON copies become caller /// storage; keep the output handle alive while using its accounted alias. /// A caught operational failure still prevents successful scope completion. @@ -44,7 +44,7 @@ public struct JSONTransformProgram: Sendable { public func withExecution(input: JSON, bindings: [String: JSON] = [:], limits: JSONTransformLimits = .init(), source: (() throws -> JSON?)? = nil, - _ body: (borrowing JSONTransformExecution) throws -> Result) throws -> JSONOperationReport { + _ body: (JSONTransformExecution) throws -> Result) throws -> JSONOperationReport { let execution = try JSONTransformExecution.make(plan: self, input: input, bindings: bindings, limits: limits, source: source) let result: Result do { result = try body(execution) } diff --git a/Tests/CompileNegative/JSONTransform/DoubleConsume.swift b/Tests/CompileNegative/JSONTransform/DoubleConsume.swift deleted file mode 100644 index 08e80fa..0000000 --- a/Tests/CompileNegative/JSONTransform/DoubleConsume.swift +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import JSONTransform -func duplicate(_ execution: consuming JSONTransformExecution) { - let first = consume execution - let second = consume execution - _ = consume first - _ = consume second -} diff --git a/Tests/CompileNegative/JSONTransform/EscapeScope.swift b/Tests/CompilePositive/JSONTransform/EscapeScope.swift similarity index 100% rename from Tests/CompileNegative/JSONTransform/EscapeScope.swift rename to Tests/CompilePositive/JSONTransform/EscapeScope.swift diff --git a/Tests/CompilePositive/JSONTransform/HandleAliases.swift b/Tests/CompilePositive/JSONTransform/HandleAliases.swift new file mode 100644 index 0000000..b70122f --- /dev/null +++ b/Tests/CompilePositive/JSONTransform/HandleAliases.swift @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSONTransform +func aliases(_ execution: JSONTransformExecution) { let first = execution; let second = execution; _ = first; _ = second } diff --git a/Tests/JSONTransformTests/TransformSpikeTests.swift b/Tests/JSONTransformTests/TransformSpikeTests.swift index 1d7cd39..9abb512 100644 --- a/Tests/JSONTransformTests/TransformSpikeTests.swift +++ b/Tests/JSONTransformTests/TransformSpikeTests.swift @@ -150,3 +150,36 @@ private func values(_ source: String, input: JSON = .null) throws -> [JSON] { catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == metric.rawValue) } } } + +@Test func jtxCopiedHandlesShareOneCumulativeLedger() throws { + let plan = try JSONTransformProgram.compile("1,2") + var limits = JSONTransformLimits(); limits[.results] = 1 + do { + _ = try plan.withExecution(input: .null, limits: limits) { execution in + let alias = execution + let first = try alias.next() + #expect(first?.value == 1 && execution.used(.results) == 1) + _ = try execution.next() + } + Issue.record("A handle copy must not reset results") + } catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == "results") } +} +@Test func jtxEscapedHandleInvalidatesAndReleasesProvider() throws { + final class Marker {} + weak var marker: Marker? + var escaped: JSONTransformExecution? + func makeProvider() -> () throws -> JSON? { + let held = Marker(); marker = held + return { _ = held; return nil } + } + _ = try JSONTransformProgram.compile(".").withExecution(input: .array([1,2]), source: makeProvider()) { execution in + escaped = execution + #expect(marker != nil) + // Early exit deliberately leaves a live VM. Scope exit must clear it. + } + #expect(marker == nil) + let handle = try #require(escaped) + #expect(handle.statistics[.retainedBytes] == 0) + do { _ = try handle.next(); Issue.record("Escaped scope accepted data access") } + catch let failure as JSONTransformFailure { #expect(failure.category == .configuration && failure.code == "scopeClosed") } +} From 90318a4ba338b251fad576fb74e227c237ff15de Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:19:06 +0200 Subject: [PATCH 11/25] Reject callback re-entry and qualify guarded transform health --- .../Architecture/Release1.7/JSONTransform.md | 15 ++ .../Qualification/TestDiscovery.json | 6 +- Scripts/ci.py | 2 + Scripts/qualify_transform.py | 38 +++++ Scripts/qualify_transform_ownership.py | 14 +- Scripts/wasm_support.py | 2 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/ScopedTransforms.md | 12 ++ .../JSONTransformExecution.swift | 88 +++++++++-- .../JSONTransform/JSONTransformProgram.swift | 12 +- .../JSONTransform/PrivateContext.swift | 4 + .../JSONTransform/PrivateVM.swift | 4 + .../TransformSpikeTests.swift | 137 ++++++++++++++++++ 13 files changed, 310 insertions(+), 26 deletions(-) create mode 100644 Scripts/qualify_transform.py create mode 100644 Tests/CompileNegative/JSONTransform/PrivateContext.swift create mode 100644 Tests/CompileNegative/JSONTransform/PrivateVM.swift diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md index c03fffd..7c5559c 100644 --- a/Documentation/Architecture/Release1.7/JSONTransform.md +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -70,3 +70,18 @@ The rejected noncopyable compiler crash remains an unresolved toolchain finding. The full native/safety/docs/Linux/WASI/performance/reproduction gates remain open; source-bound receipts, including failures, are in the private return. Do not infer release readiness or final API freeze from the prototype. + +## Callback re-entry repair + +A subprocess reproduced an exclusivity trap when an input provider re-entered +its public handle during the VM's inout context loan. This is a concrete rejected +checkpoint, not a passing negative case. The successor uses a separate private +health cell to reject re-entry before context access and latch the first failure. +The provider adapter observes health before/after callback execution. Callback +statistics are a bounded entry snapshot; cancellation does not borrow the loaned +context. Snapshot work (66 units) and fixed owner/health/snapshot storage (1,152 +logical bytes) are admitted in the same invocation. No unsafe isolation, lock or +alternate evaluator is introduced. Public reports use the existing core domain. +Twenty public-engine behavior functions cover the repaired boundary, typed runtime +caps and a twenty-row exact dialect matrix. Independent qualification remains +source-bound; older successful receipts cannot qualify changed code. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index a9bd094..2acfd8a 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -13,7 +13,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 13 + "JSONTransformTests": 20 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -80,7 +80,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 13 + "JSONTransformTests": 20 }, "wasi_root_functions": { "JSONTests": 190, @@ -94,7 +94,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 13 + "JSONTransformTests": 20 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { diff --git a/Scripts/ci.py b/Scripts/ci.py index a0ce7f4..24bb1d8 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -159,6 +159,8 @@ def commands(lane, output): ['swift', 'run', '-c', 'release', '--package-path', 'Tools/CLIQualification', 'ProviderConsumer'], ['python3', '-B', 'Scripts/qualify_schema_providers.py', '--binary', '.build/release/swift-json', '--output', str(output / 'providers.json')], ['python3', '-B', 'Scripts/qualify_cli_ownership.py', '--output', str(output / 'cli-ownership')], + ['python3', '-B', 'Scripts/qualify_transform_ownership.py', '--output', str(output / 'transform-ownership')], + ['python3', '-B', 'Scripts/qualify_transform.py', '--output', str(output / 'transform-consumer')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'ownership', '--output', str(output / 'ownership.json')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'reader', '--output', str(output / 'reader.json')], ['swift', 'run', '-c', 'release', '--package-path', 'Examples/Consumer', 'Consumer'], diff --git a/Scripts/qualify_transform.py b/Scripts/qualify_transform.py new file mode 100644 index 0000000..dd7999c --- /dev/null +++ b/Scripts/qualify_transform.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Run the actual JSON/JSONTransform-only consumer outside any Git checkout.""" +import argparse, hashlib, json, pathlib, subprocess, tempfile +ROOT = pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + args = parser.parse_args() + output = (ROOT / args.output).resolve(); output.mkdir(parents=True, exist_ok=True) + source = ROOT / 'Tools/TransformQualification' + rows = [] + with tempfile.TemporaryDirectory(prefix='swift-json-transform-consumer-') as temporary: + consumer = pathlib.Path(temporary) + for file in sorted(source.rglob('*')): + if not file.is_file() or any(x in file.relative_to(source).parts for x in ['.build', '.swiftpm']): continue + target = consumer / file.relative_to(source); target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(file.read_bytes()) + manifest = consumer / 'Package.swift' + manifest.write_text(manifest.read_text().replace('path: "../.."', 'path: ' + json.dumps(str(ROOT)))) + inputs = [{'path':p.relative_to(consumer).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in sorted(consumer.rglob('*')) if p.is_file()] + for mode in ['debug', 'release']: + command = ['swift', 'run', '-c', mode, '--package-path', str(consumer), '--jobs', '2', 'TransformConsumer'] + result = subprocess.run(command, capture_output=True, text=True) + log = output / (mode + '.log'); log.write_text(result.stdout + result.stderr) + reports = [] + for line in result.stdout.splitlines(): + try: reports.append(json.loads(line)) + except ValueError: continue + expected = dict(consumer='JSON+JSONTransform', witnesses=11, status='passed') + passed = result.returncode == 0 and reports == [expected] + rows.append(dict(command=command, exit_code=result.returncode, status='passed' if passed else 'failed', witnesses=reports, log=log.name, log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + if not passed: break + receipt = dict(status='passed' if len(rows) == 2 and all(r['status'] == 'passed' for r in rows) else 'failed', source_files=inputs, commands=rows, scope='Public path-dependent SwiftPM graph, Debug/Release, no private imports; not a released-version resolution') + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + print(json.dumps(receipt)); return 0 if receipt['status'] == 'passed' else 1 +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_transform_ownership.py b/Scripts/qualify_transform_ownership.py index a1e8f5d..a677f91 100644 --- a/Scripts/qualify_transform_ownership.py +++ b/Scripts/qualify_transform_ownership.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 """Compile actual public transformation ownership boundaries, never import failures.""" -import argparse, hashlib, json, pathlib, subprocess, sys +import argparse, hashlib, json, pathlib, platform, subprocess, sys ROOT = pathlib.Path(__file__).resolve().parents[1] def main(): @@ -12,12 +12,16 @@ def main(): args = parser.parse_args() output = pathlib.Path(args.output).resolve(); output.mkdir(parents=True, exist_ok=True) cache = pathlib.Path(args.module_cache).resolve() if args.module_cache else output / 'ModuleCache' - sdk = subprocess.check_output(['xcrun', '--sdk', 'macosx', '--show-sdk-path'], text=True).strip() - base = ['xcrun', 'swiftc', '-swift-version', '6', '-sdk', sdk, '-target', 'arm64-apple-macos27.0', '-module-cache-path', str(cache), '-I', str(pathlib.Path(args.modules).resolve()), '-c'] + base = ['swiftc', '-swift-version', '6'] + if sys.platform == 'darwin': + sdk = subprocess.check_output(['xcrun', '--sdk', 'macosx', '--show-sdk-path'], text=True).strip() + base = ['xcrun', 'swiftc', '-swift-version', '6', '-sdk', sdk, + '-target', platform.machine() + '-apple-macos27.0'] + base += ['-module-cache-path', str(cache), '-I', str(pathlib.Path(args.modules).resolve()), '-c'] positive = output / 'PublicConsumer.swift' positive.write_text('import JSON\nimport JSONTransform\nfunc send(_: T) {}\nfunc check(_ p: JSONTransformProgram) throws { send(p); _ = try p.withExecution(input: .null) { e in try e.next()?.value } }\n') cases = [('public-consumer', positive, True, None)] - for name, diagnostic in [('ReplaceScope', 'cannot pass immutable value as inout argument'), ('SendExecution', 'does not conform to the')]: + for name, diagnostic in [('ReplaceScope', 'cannot pass immutable value as inout argument'), ('SendExecution', 'does not conform to the'), ('PrivateVM', "cannot find type 'FilterVM'"), ('PrivateContext', "cannot find type 'CLIJobContext'")]: cases.append((name, ROOT / 'Tests/CompileNegative/JSONTransform' / (name + '.swift'), False, diagnostic)) for name in ['EscapeScope', 'HandleAliases']: cases.append((name, ROOT / 'Tests/CompilePositive/JSONTransform' / (name + '.swift'), True, None)) @@ -28,7 +32,7 @@ def main(): log = output / (name + '.log'); log.write_text(result.stdout + result.stderr) expected = result.returncode == 0 if positive_case else result.returncode != 0 and diagnostic in result.stderr and 'no such module' not in result.stderr rows.append(dict(id=name, command=command, exit_code=result.returncode, expected_diagnostic=diagnostic, status='passed' if expected else 'failed', source_sha256=hashlib.sha256(source.read_bytes()).hexdigest(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) - receipt = dict(status='passed' if all(r['status'] == 'passed' for r in rows) else 'failed', observed=5, cases=rows) + receipt = dict(status='passed' if all(r['status'] == 'passed' for r in rows) else 'failed', observed=len(cases), cases=rows) (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') print(json.dumps(receipt)); return 0 if receipt['status'] == 'passed' else 1 if __name__ == '__main__': sys.exit(main()) diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index d5bd26a..ab232a4 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -68,7 +68,7 @@ def binary(scratch, config, name): if preamble and not re.fullmatch(r"Another instance of SwiftPM .*waiting until that process has finished execution\.\.\.", preamble): raise ValueError('Unexpected manifest preamble: ' + preamble) manifest = json.loads(manifest_text[start:]) - if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONBridges', 'JSONHTML', 'JSONInspection', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'swift-json']: + if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONBridges', 'JSONHTML', 'JSONInspection', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONTransform', 'swift-json']: raise ValueError('Changed public products/dependencies') def invoke(engine, path, mode='command', arguments=(), repo=True, resources=True): if engine == 'node': diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index cd6f8ff..00d2cfa 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:b2c799359d2555c76f9d6223a8f9bea88e6da8e13ec77b877b7cb6860d260426" + package static let revision = "sha256:da48405743a566c82e727eb0bb2927fd714eaf147cb9c952342ef624b1732d07" package static let kind = "source-sha256" } diff --git a/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md b/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md index a31d082..d231c53 100644 --- a/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md +++ b/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md @@ -54,3 +54,15 @@ opens no files and reads no ambient process input. Async providers are not offer provider and configuration errors. Operational failures bypass language catches and latch. Catching a failed pull cannot yield a successful operation report. `cancel()` likewise prevents later pulls and successful scope completion. + +## Callback health + +Input callbacks may read accounting observations and request cancellation. Their +observations are the fixed snapshot captured on callback entry, with the same +cumulative owned ledger. Snapshot work is charged before caller code runs. +Re-entering `next()` on the same execution throws `configuration/reentrantExecution` +and latches terminal health even if the callback catches it. Cancellation after +owned progress is checked before a callback value or EOF can be accepted. +The invocation reserves 1,152 logical bytes for owner/health/observation storage; +this logical weight is neither an allocation measurement nor a process RSS cap. +ASCII identifier names follow the compiler grammar and omit the `$` prefix. diff --git a/Sources/JSONTransform/JSONTransformExecution.swift b/Sources/JSONTransform/JSONTransformExecution.swift index db28d52..bfe72f4 100644 --- a/Sources/JSONTransform/JSONTransformExecution.swift +++ b/Sources/JSONTransform/JSONTransformExecution.swift @@ -13,19 +13,55 @@ public struct JSONTransformOutput { init(_ value: FilterYield) { self.value = value.value; lease = value.lease } } +// A separate invocation-owned cell is safe to inspect while the VM exclusively +// borrows its context. It is never Sendable and never owns the execution handle. +private final class ExecutionHealth { + struct Observation { + let statistics: JSONOperationStatistics + let remaining: UInt64 + let counters: [UInt64] + } + var pulling = false + var failure: JSONTransformFailure? + var observation: Observation? + func latch(_ failure: JSONTransformFailure) { + if self.failure == nil { self.failure = failure } + } + func check(context: inout TransformRuntimeContext) throws { + if let failure { context.latch(failure); throw failure } + } + func observe(context: inout TransformRuntimeContext) throws { + // Fixed snapshot work/storage is admitted before invoking caller code. + try context.work(66) // 49 native counters + 16 runtime counters + handoff. + let cap = min(context.limits.core[.workUnits], context.limits[.totalWorkUnits]) + observation = .init(statistics: context.meter.statistics(), + remaining: cap - min(cap, context.used(.totalWorkUnits)), + counters: TransformRuntimeMetric.allCases.map { context.used($0) }) + } +} + private struct SuppliedInput: FilterInput { var source: (() throws -> JSON?)? + let health: ExecutionHealth var ended = false mutating func next(context: inout TransformRuntimeContext) throws -> FilterYield? { + try health.check(context: &context) try context.checkpoint() guard !ended, let source else { return nil } + try health.observe(context: &context) + defer { health.observation = nil } let value: JSON? do { value = try source() } catch { - let failure = error is CancellationError ? JSONTransformFailure(.cancelled, "cancelled") : JSONTransformFailure(.provider, "providerFailed") - context.latch(failure); throw failure + let proposed = error is CancellationError ? JSONTransformFailure(.cancelled, "cancelled") : JSONTransformFailure(.provider, "providerFailed") + health.latch(proposed) + try health.check(context: &context) + throw proposed } - try context.checkpoint() // Cancellation after caller-owned work. + // Re-entry/cancel may have been caught inside the callback. Neither may + // reopen the ledger, publish a returned value or pull the provider again. + try health.check(context: &context) + try context.checkpoint() // Task cancellation after caller-owned work. guard let value else { ended = true; self.source = nil; return nil } try context.charge(.totalRecords) return try FilterValues.produced(value, context: &context) @@ -44,20 +80,24 @@ public final class JSONTransformExecution { private var planReservation: UInt64 = 0 private var bindingReservation: UInt64 = 0 private var initialBindings: UInt64 = 0 - private var failure: JSONTransformFailure? + private let health: ExecutionHealth + private static var stateReservation: UInt64 { 1152 } private var cleanupFailure: JSONTransformFailure? private var closed = false private init(context: consuming TransformRuntimeContext, source: (() throws -> JSON?)?) { self.context = consume context - input = .init(source: source) + let health = ExecutionHealth() + self.health = health + input = .init(source: source, health: health) vm = nil } static func make(plan: JSONTransformProgram, input root: JSON, bindings: [String: JSON], limits: JSONTransformLimits, source: (() throws -> JSON?)?) throws -> JSONTransformExecution { do { var context = try TransformRuntimeContext(limits: limits.runtime) - try context.meter.retain(128) // Fixed invocation state, before allocation. + try context.domain(.core) + try context.meter.retain(Self.stateReservation) // Owner + health + fixed observation, before allocation. let execution = JSONTransformExecution(context: consume context, source: source) try execution.prepare(plan: plan, root: root, bindings: bindings) return execution @@ -89,9 +129,18 @@ public final class JSONTransformExecution { /// A resource, provider or cancellation failure latches and bypasses query /// catches. Later pulls cannot create a fresh successful execution. public func next() throws -> JSONTransformOutput? { + // Check before touching the exclusively borrowed context. Caller callbacks + // can reach this handle; same-thread re-entry must throw, never trap. + guard !health.pulling else { + let failure = JSONTransformFailure(.configuration, "reentrantExecution") + health.latch(failure) + throw health.failure ?? failure + } + health.pulling = true + defer { health.pulling = false } do { + try health.check(context: &context) try context.checkpoint() - if let failure { throw failure } guard !closed else { throw JSONTransformFailure(.configuration, "scopeClosed") } guard var active = vm else { return nil } vm = nil @@ -101,31 +150,40 @@ public final class JSONTransformExecution { return .init(value) } catch { let failed = JSONTransformFailure.classify(error) - if failure == nil { failure = failed } - context.latch(failed) + health.latch(failed) + try health.check(context: &context) throw failed } } /// Current cumulative native statistics, also available after a failed pull. - public var statistics: JSONOperationStatistics { context.meter.statistics() } + /// During an input callback this is the snapshot taken at callback entry. + public var statistics: JSONOperationStatistics { + health.observation?.statistics ?? context.meter.statistics() + } /// Remaining shared owned work; this is not a caller instruction quota. public var remainingWorkUnits: UInt64 { + if let observation = health.observation { return observation.remaining } let cap = min(context.limits.core[.workUnits], context.limits[.totalWorkUnits]) return cap - min(cap, context.used(.totalWorkUnits)) } /// Reads a transform counter without exposing its mutable meter. /// - Parameter metric: The cumulative or live transform counter. - public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { context.used(metric.runtime) } + public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { + if let observation = health.observation { return observation.counters[metric.runtime.index] } + return context.used(metric.runtime) + } /// Latches cancellation. The next pull and scope completion both throw. public func cancel() { guard !closed else { return } let cancelled = JSONTransformFailure(.cancelled, "cancelled") - if failure == nil { failure = cancelled } - context.latch(cancelled) + health.latch(cancelled) + // During a provider callback context is loaned to the VM. The adapter + // observes this cell before accepting another value or reporting EOF. + if !health.pulling { context.latch(cancelled) } } func checkCompletion() throws { try context.checkpoint() - if let failure { throw failure } + try health.check(context: &context) if let cleanupFailure { throw cleanupFailure } } func recordCleanupFailure(_ error: any Error) { cleanupFailure = .classify(error) } @@ -137,7 +195,7 @@ public final class JSONTransformExecution { try context.reclaimValueLeases() // Remaining compilation/binding reservations end with the owner scope; // returned output handles retain their own deferred lease reservations. - try context.meter.release(planReservation + bindingReservation + 128) + try context.meter.release(planReservation + bindingReservation + Self.stateReservation) try context.release(.bindings, initialBindings) planReservation = 0; bindingReservation = 0; initialBindings = 0 } diff --git a/Sources/JSONTransform/JSONTransformProgram.swift b/Sources/JSONTransform/JSONTransformProgram.swift index 843a95b..a0c5cfb 100644 --- a/Sources/JSONTransform/JSONTransformProgram.swift +++ b/Sources/JSONTransform/JSONTransformProgram.swift @@ -14,7 +14,7 @@ public struct JSONTransformProgram: Sendable { /// Compiles explicit source using finite native and transform limits. /// - Parameters: /// - source: Query source in the documented jq-inspired subset. - /// - variables: Names supplied by each invocation, without `$` prefixes. + /// - variables: ASCII identifiers supplied by each invocation, without `$` prefixes. /// - limits: Finite compilation policy; execution has its own bounded scope. public static func compile(_ source: String, variables: Set = [], limits: JSONTransformLimits = .init()) throws -> Self { @@ -23,6 +23,16 @@ public struct JSONTransformProgram: Sendable { for name in variables { try context.charge(.bindings) try JSONAdmission.text(name, key: true, meter: &context.meter, owned: true) + var first = true + for byte in name.utf8 { + try context.work() + let letter = byte == 95 || (65...90).contains(byte) || (97...122).contains(byte) + guard letter || (!first && (48...57).contains(byte)) else { + throw JSONTransformFailure(.configuration, "invalidVariableName") + } + first = false + } + guard !first else { throw JSONTransformFailure(.configuration, "invalidVariableName") } } let program = try FilterCompiler.compile(source, variables: variables, context: &context) return .init(program: program, planReservation: context.meter.used(.retainedBytes), variables: variables) diff --git a/Tests/CompileNegative/JSONTransform/PrivateContext.swift b/Tests/CompileNegative/JSONTransform/PrivateContext.swift new file mode 100644 index 0000000..5f276f0 --- /dev/null +++ b/Tests/CompileNegative/JSONTransform/PrivateContext.swift @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform +let privateType: CLIJobContext? = nil diff --git a/Tests/CompileNegative/JSONTransform/PrivateVM.swift b/Tests/CompileNegative/JSONTransform/PrivateVM.swift new file mode 100644 index 0000000..d418fc3 --- /dev/null +++ b/Tests/CompileNegative/JSONTransform/PrivateVM.swift @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform +let privateType: FilterVM? = nil diff --git a/Tests/JSONTransformTests/TransformSpikeTests.swift b/Tests/JSONTransformTests/TransformSpikeTests.swift index 9abb512..760ca58 100644 --- a/Tests/JSONTransformTests/TransformSpikeTests.swift +++ b/Tests/JSONTransformTests/TransformSpikeTests.swift @@ -183,3 +183,140 @@ private func values(_ source: String, input: JSON = .null) throws -> [JSON] { do { _ = try handle.next(); Issue.record("Escaped scope accepted data access") } catch let failure as JSONTransformFailure { #expect(failure.category == .configuration && failure.code == "scopeClosed") } } + +@Test func jtxReentrantProviderFailureLatchesWithoutAccessConflict() throws { + let plan = try JSONTransformProgram.compile("try inputs catch null") + var handle: JSONTransformExecution? + var calls = 0 + do { + _ = try plan.withExecution(input: .null, source: { + calls += 1 + do { _ = try handle!.next(); Issue.record("Expected re-entry rejection") } + catch let error as JSONTransformFailure { #expect(error.category == .configuration && error.code == "reentrantExecution") } + return 1 // Catching the error must not publish this value. + }) { execution in + handle = execution + for _ in 0..<2 { + do { _ = try execution.next(); Issue.record("Expected terminal re-entry failure") } + catch let error as JSONTransformFailure { #expect(error.code == "reentrantExecution") } + } + } + Issue.record("Expected terminal scope failure") + } catch let error as JSONTransformFailure { #expect(error.code == "reentrantExecution") } + #expect(calls == 1 && handle?.statistics[.retainedBytes] == 0) +} +@Test func jtxProviderCanObserveSnapshotAndCancelAfterOwnedProgress() throws { + let plan = try JSONTransformProgram.compile("inputs") + var handle: JSONTransformExecution? + var calls = 0 + do { + _ = try plan.withExecution(input: .null, source: { + calls += 1 + let execution = handle! + #expect(execution.statistics.domain == .core) + #expect(execution.used(.workUnits) == execution.statistics[.workUnits]) + #expect(execution.statistics[.workUnits] > 0 && execution.remainingWorkUnits > 0) + #expect(execution.used(.inputValues) == 1) + execution.cancel() + return 1 + }) { execution in + handle = execution + _ = try execution.next() + } + Issue.record("Expected callback cancellation") + } catch let error as JSONTransformFailure { #expect(error.category == .cancelled) } + #expect(calls == 1 && handle?.used(.results) == 0 && handle?.statistics[.retainedBytes] == 0) +} +@Test func jtxTaskCancellationIsObservedAfterProviderOwnedProgress() async throws { + let plan = try JSONTransformProgram.compile("inputs") + let checked = await Task { + var handle: JSONTransformExecution? + var calls = 0 + do { + _ = try plan.withExecution(input: .null, source: { + calls += 1 + withUnsafeCurrentTask { $0?.cancel() } + return 1 + }) { execution in handle = execution; _ = try execution.next() } + return false + } catch let failure as JSONTransformFailure { + return failure.category == .cancelled && calls == 1 && handle?.used(.results) == 0 && handle?.statistics[.retainedBytes] == 0 + } catch { return false } + }.value + #expect(checked) +} +@Test func jtxReportsUseCoreDomainAndCallerErrorRetainsIdentity() throws { + enum CallerError: Error, Equatable { case sentinel } + let plan = try JSONTransformProgram.compile(".") + var limits = JSONTransformLimits(); limits[.workUnits] = 4096 + var work: UInt64 = 0 + let report = try plan.withExecution(input: 1, limits: limits) { execution in + _ = try execution.next(); _ = try execution.next() + work = execution.used(.workUnits) + #expect(execution.remainingWorkUnits == 4096 - work) + } + #expect(report.statistics.domain == .core && report.statistics[.workUnits] == work) + #expect(report.statistics[.retainedBytes] == 0) + var escaped: JSONTransformExecution? + do { + _ = try plan.withExecution(input: .array([1,2])) { execution in + escaped = execution + _ = try execution.next() + throw CallerError.sentinel + } + Issue.record("Expected caller error") + } catch let error as CallerError { #expect(error == .sentinel) } + #expect(escaped?.statistics[.retainedBytes] == 0) +} +@Test func jtxRuntimeCapsRejectBeforePublishingAndRemainTyped() throws { + for metric in [JSONTransformLimits.Metric.frames, .bindings, .retainedBytes, .workUnits] { + let plan = try JSONTransformProgram.compile("$v", variables: ["v"]) + var limits = JSONTransformLimits(); limits[metric] = 0 + do { + _ = try plan.withExecution(input: .null, bindings: ["v": 1], limits: limits) { execution in try execution.next() } + Issue.record("Expected finite runtime cap") + } catch let error as JSONTransformFailure { + #expect(error.category == .resource && error.metric == metric.rawValue && error.limit == 0) + } + } + var invalid = JSONTransformLimits(); invalid[.results] = UInt64.max + do { _ = try JSONTransformProgram.compile(".", limits: invalid); Issue.record("Expected finite policy") } + catch let error as JSONTransformFailure { #expect(error.category == .configuration) } +} +@Test func jtxPublicEngineMatchesNativeExactDialectMatrix() throws { + let rows: [(String,String,String)] = [ + (".a,.b", "{\"a\":1,\"b\":2}", "[1,2]"), + (".missing,.present", "{\"present\":null}", "[null,null]"), + ("map(.+1)", "[1,2]", "[[2,3]]"), + ("map_values(.+1)", "{\"a\":1}", "[{\"a\":2}]"), + (".[] | select(.>1)", "[1,2,3]", "[2,3]"), + ("sort_by(.k)", "[{\"k\":2,\"v\":0},{\"k\":1},{\"k\":2,\"v\":1}]", "[[{\"k\":1},{\"k\":2,\"v\":0},{\"k\":2,\"v\":1}]]"), + ("group_by(.)", "[2,1,2]", "[[[1],[2,2]]]"), + ("unique", "[2,1,2]", "[[1,2]]"), + ("first(.[]),last(.[])", "[1,2]", "[1,2]"), + ("reduce .[] as $n (0; .+$n)", "[1,2,3]", "[6]"), + ("foreach .[] as $n (0; .+$n)", "[1,2,3]", "[1,3,6]"), + (".a += 2", "{\"a\":1}", "[{\"a\":3}]"), + ("setpath([\"a\"];2)", "{}", "[{\"a\":2}]"), + ("def inc: .+1; inc", "2", "[3]"), + (". as $x | $x+1", "2", "[3]"), + ("walk(if type == \"number\" then .+1 else . end)", "{\"a\":[1]}", "[{\"a\":[2]}]"), + (".[1:3]", "\"e\\u0301x\"", "[\"\\u0301x\"]"), + (". < \"z\"", "\"é\"", "[false]"), + ("{a:1,a:2}", "null", "[{\"a\":2}]"), + ("-0,1.00", "null", "[0,1]"), + ] + for (query, input, expected) in rows { + #expect(try values(query, input: JSON.parse(input)) == JSON.parse(expected).arrayValue!, "\(query)") + } +} + +@Test func jtxBindingNamesUseTheCompilerIdentifierGrammar() throws { + for name in ["", "é", "a-b", "$x", "1a"] { + do { _ = try JSONTransformProgram.compile(".", variables: [name]); Issue.record("Expected invalid variable name") } + catch let error as JSONTransformFailure { #expect(error.category == .configuration && error.code == "invalidVariableName") } + } + let plan = try JSONTransformProgram.compile("$a_1", variables: ["a_1"]) + let report = try plan.withExecution(input: .null, bindings: ["a_1": 7]) { execution in try execution.next()?.value } + #expect(report.result == 7) +} From 61125ed8f7d147fce9bcfcb031117299cc9ff923 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:52:06 +0200 Subject: [PATCH 12/25] Drain terminal alias refunds and reconcile transform documentation --- Documentation/Architecture/API.md | 51 +++++++++++++++++-- .../Architecture/Release1.7/JSONTransform.md | 23 ++++++++- Documentation/CLI.md | 11 ++++ .../Qualification/TestDiscovery.json | 6 +-- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../JSONTransformExecution.swift | 3 +- .../TransformSpikeTests.swift | 28 ++++++++++ 7 files changed, 115 insertions(+), 9 deletions(-) diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index fb8fd0e..f6f20c9 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -1,8 +1,12 @@ # Public API contract -This document describes the implemented public interface. Native compiler symbol -graphs supply the complete declarations in the eight DocC catalogs. Examples -in the [README](../../README.md) are compiled and executed by the public consumer. +The product table below records the published 1.6 line: eight libraries plus the +CLI and eight DocC catalogs. The current unreleased 1.7 source also contains +JSONInspection, as described in the README and structural authority map. This +feature candidate adds JSONTransform as a tenth library and tenth catalog; its +additive contract appears below. Existing published contracts remain unchanged. +Native compiler symbol graphs supply the complete declarations. Examples in the +[README](../../README.md) are compiled and executed by public consumers. ## Products and ownership @@ -12,6 +16,7 @@ in the [README](../../README.md) are compiled and executed by the public consume | JSONNavigation | JSONTraversal, JSONTraversalScope, JSONTraversalOptions | Exact many-node DFS, strict physical/virtual scopes, bounded metadata and explicit coverage; one core read-scope meter | | JSONBridges | BinaryInteger.jsonNumber, FixedWidthInteger/Double/Decimal.init(jsonNumber:), Double.jsonNumberFromRoundTripText | Optional one-delegate numeric conversions; explicit fidelity, no new authority | | JSONSchema | JSONSchemaDocument, JSONSchemaRegistry, JSONSchemaCompiler, CompiledJSONSchema, JSONSchemaValidator | Offline immutable plan, invocation-owned evaluation and private native regex | +| JSONSchemaExamples | JSONSchemaExampleGenerator, JSONSchemaExampleResult | Conservative deterministic witnesses certified by the existing native validator; separate finite preparation and cumulative generation | | JSONStreaming | JSONEventSequence, JSONValueSequence, NDJSONValueSequence, JSONTopLevelArraySequence, JSONAsyncWriter | Exclusive pull state, shared codec, strict framing and awaited handoffs | | JSONPatch | JSONPatch, JSONPatchOperation, JSONPatchResult, JSONMergePatch | Transaction staging, exact comparison and iterative editing | | swift-json | format, validate, pointer, patch, query, stream, schema, render | Bounded process/file adapters and private CLI engine/filter runtime; no public query library | @@ -288,3 +293,43 @@ the generated identity file itself. It is not a Git commit, binary digest or cla of publication. Generation/checking needs no Git, time, environment or network. JSONNavigation product promotion preserves all 56 N1 public declarations and resource semantics. Full-source admission precedes invalid depth/record-option validation, including oversized sources; N1Promotion.md records this preserved precedence. JSON imports no Navigation symbols implicitly. + +## Provisional JSONTransform extraction (1.7 feature branch) + +JSONTransform is an opt-in public product depending on JSON and the private +JSONFilterRuntime kernel. JSON and JSONSchema do not import or re-export it. +JSONCLIEngine uses its package-only accounted adapters; both callers execute the +same FilterCompiler/FilterVM. The public interface contains no CLI context, file, +process, exit-code or private-runtime signature. + +- `JSONTransformProgram.compile(_:variables:limits:)` creates an immutable Sendable + plan under finite compilation limits. Variable names are ASCII identifiers, + without `$`; JSON values retain exact native number and scalar-key semantics. +- `withExecution(input:bindings:limits:source:_:)` creates a synchronous guarded + invocation and returns `JSONOperationReport`. Root admission, bindings, + all pulls/provider values and output results share one cumulative ledger. +- `JSONTransformExecution.next()` returns `JSONTransformOutput?`. Nil means EOF; + a present handle may contain JSON null. Handles share one owner and are not + Sendable. Scope exit invalidates data access and releases provider/frame/root + ownership; output aliases retain their source reservation until last release. +- `statistics`, `used(_:)` and `remainingWorkUnits` expose finite accounting without + exporting a mutable meter. During provider callbacks they report the bounded + callback-entry snapshot. `cancel()` latches terminal cancellation. +- `JSONTransformLimits` combines the existing native policy with eleven mapped + transform ceilings. `JSONTransformFailure` distinguishes compilation, language, + resource, cancelled, provider and configuration failures. Owned operational + failures bypass query catches and remain terminal after caller catches. + +Provider work/allocations and extracted JSON copies belong to the caller; logical +retention does not cap process RSS. Re-entering next() from the same provider +throws `configuration/reentrantExecution` before accessing its borrowed context. +The CLI retains one ledger across parsing, transformation, writing and diagnostics, +its sixteen existing metric names and its own completion/EOF duties. Public +compilation and execution are separate finite scopes, not one combined claim. + +See the [actual API/ownership decision](Release1.7/JSONTransform.md), +[DocC guide](../../Sources/JSONTransform/Documentation.docc/ScopedTransforms.md) +and [standalone public SwiftPM consumer](../../Tools/TransformQualification). +This is an unreleased candidate, absent from published 1.6.1. No merge, tag, +publication, new dialect, implicit floating bridge or feature from another draft +follows from this contract. diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md index 7c5559c..b3e9880 100644 --- a/Documentation/Architecture/Release1.7/JSONTransform.md +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -82,6 +82,27 @@ statistics are a bounded entry snapshot; cancellation does not borrow the loaned context. Snapshot work (66 units) and fixed owner/health/snapshot storage (1,152 logical bytes) are admitted in the same invocation. No unsafe isolation, lock or alternate evaluator is introduced. Public reports use the existing core domain. -Twenty public-engine behavior functions cover the repaired boundary, typed runtime +Twenty-one public-engine behavior functions cover the repaired boundary, typed runtime caps and a twenty-row exact dialect matrix. Independent qualification remains source-bound; older successful receipts cannot qualify changed code. + +## Common-scope ownership review + +The common private context deliberately retains the CLI-only limits-file marker, +issue cap and committed-result report counters. Public construction cannot set or +observe those host fields. The private kernel imports JSON only and performs no +ambient file/process I/O. Numeric division policy and frame/binding/collection +ownership remain kernel concerns. Retaining this one private scope preserves the +CLI's aggregate parse/compile/execute/write/diagnostic budget and refund authority; +creating a second public-runtime meter would break that composition. The health +cell stores only terminal state and observations, never a second quota ledger. +This is the selected extraction boundary, rather than a pending source move. + +## Terminal output refunds + +A separate public-consumer probe demonstrated that a last output alias released +only after scope failure could leave a pending reservation in observations. The +public pull now drains the existing deferred ARC refund queue before rethrowing +terminal health; work is never refunded and the original error still wins. +A regression covers resource, explicit-cancellation and uncaught-language failures, +including live alias ownership across scope exit and zero live bytes after release. diff --git a/Documentation/CLI.md b/Documentation/CLI.md index 21a101e..c6c8d4e 100644 --- a/Documentation/CLI.md +++ b/Documentation/CLI.md @@ -72,3 +72,14 @@ The [CLI reference](Architecture/Release1.4/CLIReference.md), [arithmetic](Architecture/Release1.4/Arithmetic.md), [advanced filters](Architecture/Release1.4/Advanced.md) and [provider contract](Architecture/Release1.4/Providers.md) define exact semantics. + +## Provisional programmatic transformations + +The 1.7 JSONTransform feature branch offers an optional public library over the +same compiler and pull VM used by `swift-json query`. It does not change the CLI +flags, framing, input cursor, diagnostics, staged files or exit codes documented +here. Public library users supply exact native values and an explicit optional +provider; the CLI retains process/file ownership and closing-syntax/EOF checks. +See the [public API](Architecture/API.md#provisional-jsontransform-extraction-17-feature-branch) +and [runnable consumer](../Tools/TransformQualification). This product is not in +stable 1.6.1 and this note does not publish 1.7.0. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 2acfd8a..adc5ba5 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -13,7 +13,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 20 + "JSONTransformTests": 21 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -80,7 +80,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 20 + "JSONTransformTests": 21 }, "wasi_root_functions": { "JSONTests": 190, @@ -94,7 +94,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 20 + "JSONTransformTests": 21 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 00d2cfa..3ef4e52 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:da48405743a566c82e727eb0bb2927fd714eaf147cb9c952342ef624b1732d07" + package static let revision = "sha256:268f5cbe58b7a1e83320c1d7063895dfa27e42e5a1a745a93edb6445b76cd0d6" package static let kind = "source-sha256" } diff --git a/Sources/JSONTransform/JSONTransformExecution.swift b/Sources/JSONTransform/JSONTransformExecution.swift index bfe72f4..98eb7e2 100644 --- a/Sources/JSONTransform/JSONTransformExecution.swift +++ b/Sources/JSONTransform/JSONTransformExecution.swift @@ -139,8 +139,9 @@ public final class JSONTransformExecution { health.pulling = true defer { health.pulling = false } do { - try health.check(context: &context) + // Drain deferred ARC refunds even when terminal health will throw. try context.checkpoint() + try health.check(context: &context) guard !closed else { throw JSONTransformFailure(.configuration, "scopeClosed") } guard var active = vm else { return nil } vm = nil diff --git a/Tests/JSONTransformTests/TransformSpikeTests.swift b/Tests/JSONTransformTests/TransformSpikeTests.swift index 760ca58..a6a4c3d 100644 --- a/Tests/JSONTransformTests/TransformSpikeTests.swift +++ b/Tests/JSONTransformTests/TransformSpikeTests.swift @@ -320,3 +320,31 @@ private func values(_ source: String, input: JSON = .null) throws -> [JSON] { let report = try plan.withExecution(input: .null, bindings: ["a_1": 7]) { execution in try execution.next()?.value } #expect(report.result == 7) } + +@Test func jtxTerminalAliasesRefundBeforeThrowingTheSameFailure() throws { + let root = try JSON.parse("{\"a\":1,\"padding\":\"" + String(repeating: "x", count: 4096) + "\"}") + for category in [JSONTransformFailure.Category.resource, .cancelled, .language] { + let query = category == .language ? ".a,error(\"bad\")" : ".a,.a" + let plan = try JSONTransformProgram.compile(query) + var limits = JSONTransformLimits() + if category == .resource { limits[.results] = 1 } + var escaped: JSONTransformExecution? + var output: JSONTransformOutput? + do { + _ = try plan.withExecution(input: root, limits: limits) { execution in + escaped = execution + output = try execution.next() + if category == .cancelled { execution.cancel() } + _ = try execution.next() + } + Issue.record("Expected terminal scope failure") + } catch let error as JSONTransformFailure { #expect(error.category == category) } + let execution = try #require(escaped) + #expect(output?.value == 1) + withExtendedLifetime(output) { #expect(execution.statistics[.retainedBytes] > 4096) } + output = nil + do { _ = try execution.next(); Issue.record("Terminal scope resumed") } + catch let error as JSONTransformFailure { #expect(error.category == category) } + #expect(execution.statistics[.retainedBytes] == 0) + } +} From 21199f9af5bf50f1d473c6d3e67b91ae91f75089 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:15:34 +0200 Subject: [PATCH 13/25] Enforce execution value bounds before publishing compiled literals --- .../Architecture/Release1.7/JSONTransform.md | 15 +++++- .../Qualification/TestDiscovery.json | 6 +-- Scripts/qualify_transform.py | 2 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/ScopedTransforms.md | 10 ++++ .../JSONTransformExecution.swift | 18 +++++++ .../JSONTransform/JSONTransformLimits.swift | 2 +- .../TransformSpikeTests.swift | 54 +++++++++++++++++++ Tools/TransformQualification/README.md | 6 +-- .../Sources/TransformConsumer/Main.swift | 5 ++ 10 files changed, 109 insertions(+), 11 deletions(-) diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md index b3e9880..554c839 100644 --- a/Documentation/Architecture/Release1.7/JSONTransform.md +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -90,8 +90,8 @@ source-bound; older successful receipts cannot qualify changed code. The common private context deliberately retains the CLI-only limits-file marker, issue cap and committed-result report counters. Public construction cannot set or -observe those host fields. The private kernel imports JSON only and performs no -ambient file/process I/O. Numeric division policy and frame/binding/collection +observe those host fields. The shared context imports JSON only. Existing kernel files retain portable +Foundation utilities; the facade performs no ambient file/process I/O. Numeric division policy and frame/binding/collection ownership remain kernel concerns. Retaining this one private scope preserves the CLI's aggregate parse/compile/execute/write/diagnostic budget and refund authority; creating a second public-runtime meter would break that composition. The health @@ -106,3 +106,14 @@ public pull now drains the existing deferred ARC refund queue before rethrowing terminal health; work is never refunded and the original error still wins. A regression covers resource, explicit-cancellation and uncaught-language failures, including live alias ownership across scope exit and zero live bytes after release. + +## Independent execution value policy + +A separate real-engine consumer at 61125ed demonstrated that literals admitted +at compilation could bypass tighter invocation number-digit, string-byte and +exponent bounds. The public output boundary now uses native JSON admission on +the existing execution meter before publishing/counting a result. Per-value +capacity retirement preserves cumulative work/statistics. A failed traversal +refunds only its temporary frame reservations. The private compiler/VM and CLI +host path remain unchanged. Regressions qualify inclusive literal boundaries, +terminal latching, zero publication, cumulative traversal work and cleanup. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index adc5ba5..528692e 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -13,7 +13,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 21 + "JSONTransformTests": 23 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -80,7 +80,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 21 + "JSONTransformTests": 23 }, "wasi_root_functions": { "JSONTests": 190, @@ -94,7 +94,7 @@ "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76, - "JSONTransformTests": 21 + "JSONTransformTests": 23 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { diff --git a/Scripts/qualify_transform.py b/Scripts/qualify_transform.py index dd7999c..847b073 100644 --- a/Scripts/qualify_transform.py +++ b/Scripts/qualify_transform.py @@ -28,7 +28,7 @@ def main(): for line in result.stdout.splitlines(): try: reports.append(json.loads(line)) except ValueError: continue - expected = dict(consumer='JSON+JSONTransform', witnesses=11, status='passed') + expected = dict(consumer='JSON+JSONTransform', witnesses=12, status='passed') passed = result.returncode == 0 and reports == [expected] rows.append(dict(command=command, exit_code=result.returncode, status='passed' if passed else 'failed', witnesses=reports, log=log.name, log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) if not passed: break diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 3ef4e52..c92b4e5 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:268f5cbe58b7a1e83320c1d7063895dfa27e42e5a1a745a93edb6445b76cd0d6" + package static let revision = "sha256:bb50a684bcc13618a716a57c093762191f4c50003aa566208b36b54caa141669" package static let kind = "source-sha256" } diff --git a/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md b/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md index d231c53..7bc7ec1 100644 --- a/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md +++ b/Sources/JSONTransform/Documentation.docc/ScopedTransforms.md @@ -66,3 +66,13 @@ owned progress is checked before a callback value or EOF can be accepted. The invocation reserves 1,152 logical bytes for owner/health/observation storage; this logical weight is neither an allocation measurement nor a process RSS cap. ASCII identifier names follow the compiler grammar and omit the `$` prefix. + +## Reusing a plan under tighter limits + +Compilation and execution have separate finite policies. Before publishing each +public output, native JSON admission checks that value under the execution's +policy, including compiled literals. A plan compiled with larger number/text +bounds cannot bypass a tighter invocation. This handoff traversal shares the same +work ledger. Per-value capacities retire into cumulative statistics; work never +resets. Temporary traversal reservations are refunded even on terminal failure. +The CLI retains its existing native writer admission on the shared job ledger. diff --git a/Sources/JSONTransform/JSONTransformExecution.swift b/Sources/JSONTransform/JSONTransformExecution.swift index 98eb7e2..8ef5224 100644 --- a/Sources/JSONTransform/JSONTransformExecution.swift +++ b/Sources/JSONTransform/JSONTransformExecution.swift @@ -128,6 +128,8 @@ public final class JSONTransformExecution { /// /// A resource, provider or cancellation failure latches and bypasses query /// catches. Later pulls cannot create a fresh successful execution. + /// Native value bounds apply at every handoff, including literals compiled + /// under a broader policy; that admission uses the same cumulative meter. public func next() throws -> JSONTransformOutput? { // Check before touching the exclusively borrowed context. Caller callbacks // can reach this handle; same-thread re-entry must throw, never trap. @@ -147,6 +149,9 @@ public final class JSONTransformExecution { vm = nil defer { vm = active } guard let value = try active.next(input: &input, context: &context) else { return nil } + // Compile-time literals were admitted under a different policy. Every + // public handoff must satisfy this invocation's native value bounds. + try admitOutput(value.value) try context.charge(.totalResults) return .init(value) } catch { @@ -156,6 +161,19 @@ public final class JSONTransformExecution { throw failed } } + private func admitOutput(_ value: JSON) throws { + let before = context.meter.used(.retainedBytes) + do { + // Retire per-value capacities, preserving cumulative work/statistics. + try context.meter.resetValueBounds() + try JSONAdmission.value(value, meter: &context.meter) + } catch { + // Native admission may fail with traversal frames still reserved. + // The value/lease remains owned by the VM; refund only temporaries. + try context.meter.release(context.meter.used(.retainedBytes) - before) + throw error + } + } /// Current cumulative native statistics, also available after a failed pull. /// During an input callback this is the snapshot taken at callback entry. public var statistics: JSONOperationStatistics { diff --git a/Sources/JSONTransform/JSONTransformLimits.swift b/Sources/JSONTransform/JSONTransformLimits.swift index ba71bbf..372ce84 100644 --- a/Sources/JSONTransform/JSONTransformLimits.swift +++ b/Sources/JSONTransform/JSONTransformLimits.swift @@ -50,7 +50,7 @@ public struct JSONTransformLimits: Sendable, Equatable { } } var runtime: TransformRuntimeLimits - /// Native admission, numeric, cancellation and work ceilings. + /// Native admission, output-value, numeric, cancellation and work ceilings. public var json: JSONLimits { get { runtime.core } set { runtime.core = newValue } diff --git a/Tests/JSONTransformTests/TransformSpikeTests.swift b/Tests/JSONTransformTests/TransformSpikeTests.swift index a6a4c3d..229c6b8 100644 --- a/Tests/JSONTransformTests/TransformSpikeTests.swift +++ b/Tests/JSONTransformTests/TransformSpikeTests.swift @@ -348,3 +348,57 @@ private func values(_ source: String, input: JSON = .null) throws -> [JSON] { #expect(execution.statistics[.retainedBytes] == 0) } } + +@Test func jtxCompiledLiteralsCannotBypassTighterExecutionValueLimits() throws { + let rows: [(String, JSONResourceMetric, UInt64)] = [ + ("12345", .numberDigits, 5), ("\"abc\"", .stringBytes, 3), ("1e20", .absoluteExponent, 20)] + for (query, metric, bound) in rows { + let plan = try JSONTransformProgram.compile(query) + var limits = JSONTransformLimits(); limits.json[metric] = bound + let accepted = try plan.withExecution(input: .null, limits: limits) { e in try e.next()?.value } + #expect(accepted.result == (try JSON.parse(query))) + limits.json[metric] = bound - 1 + var handle: JSONTransformExecution? + do { + _ = try plan.withExecution(input: .null, limits: limits) { e in + handle = e + for _ in 0..<2 { + do { _ = try e.next(); Issue.record("Expected execution value refusal") } + catch let failure as JSONTransformFailure { + #expect(failure.category == .resource && failure.metric == metric.rawValue) + #expect(failure.limit == bound - 1 && failure.attemptedUsage == bound) + } + } + } + Issue.record("Caught output refusal made scope successful") + } catch let failure as JSONTransformFailure { #expect(failure.category == .resource && failure.metric == metric.rawValue) } + #expect(handle?.used(.results) == 0 && handle?.statistics[.retainedBytes] == 0) + } +} +@Test func jtxOutputAdmissionWorkAndTemporaryFramesStayOnTheOneLedger() throws { + let plan = try JSONTransformProgram.compile(".") + let root = JSON.array(Array(repeating: .string("abc"), count: 64)) + var prepared: UInt64 = 0 + var emitted: UInt64 = 0 + _ = try plan.withExecution(input: root) { e in + prepared = e.used(.workUnits) + let output = try e.next() + #expect(output?.value == root) + emitted = e.used(.workUnits) + } + #expect(emitted > prepared + 64) + var limits = JSONTransformLimits(); let cap = prepared + (emitted - prepared) / 2 + limits[.workUnits] = cap + var began = false + var handle: JSONTransformExecution? + do { + _ = try plan.withExecution(input: root, limits: limits) { e in + began = true; handle = e + _ = try e.next() + } + Issue.record("Expected cumulative output-admission work refusal") + } catch let failure as JSONTransformFailure { + #expect(failure.category == .resource && failure.metric == "workUnits" && failure.limit == cap) + } + #expect(began && handle?.used(.results) == 0 && handle?.statistics[.retainedBytes] == 0) +} diff --git a/Tools/TransformQualification/README.md b/Tools/TransformQualification/README.md index 9d92447..c9992dc 100644 --- a/Tools/TransformQualification/README.md +++ b/Tools/TransformQualification/README.md @@ -1,9 +1,9 @@ # Public transformation consumer This separate SwiftPM package imports JSON and JSONTransform only. It exercises -11 witnesses: zero/null/multiple results, exact numbers/scalar keys, plan reuse, -compile/language errors, cumulative provider limits, exact bindings, cancellation -and independent concurrent invocations. These are executable assertions, not +12 witnesses: zero/null/multiple results, exact numbers/scalar keys, plan reuse, +compile/language errors, cumulative provider limits, exact bindings, cancellation, +independent concurrent invocations and tighter execution value limits. These are executable assertions, not full dialect, platform or release qualification. Run `swift run --package-path Tools/TransformQualification TransformConsumer`. diff --git a/Tools/TransformQualification/Sources/TransformConsumer/Main.swift b/Tools/TransformQualification/Sources/TransformConsumer/Main.swift index c809cb0..522b237 100644 --- a/Tools/TransformQualification/Sources/TransformConsumer/Main.swift +++ b/Tools/TransformQualification/Sources/TransformConsumer/Main.swift @@ -51,6 +51,11 @@ func collect(_ plan: JSONTransformProgram, input: JSON = .null) throws -> [JSON] for _ in 0..<8 { group.addTask { try collect(plan, input: root) == expected } } for try await match in group { precondition(match) } }; witnesses += 1 + var narrow = JSONTransformLimits(); narrow.json[.numberDigits] = 1 + do { + _ = try JSONTransformProgram.compile("12345").withExecution(input: .null, limits: narrow) { e in try e.next() } + preconditionFailure("compiled literal bypassed execution value bounds") + } catch let failure as JSONTransformFailure { precondition(failure.category == .resource && failure.metric == "numberDigits") }; witnesses += 1 print("{\"consumer\":\"JSON+JSONTransform\",\"witnesses\":\(witnesses),\"status\":\"passed\"}") } } From 29742545badeb468e2686f2df51355a28b055001 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:45:38 +0200 Subject: [PATCH 14/25] Prepare unpublished 1.7.0 candidate identity and qualification scope --- AGENTS.md | 8 +- CHANGELOG.md | 19 ++++- CONTRIBUTING.md | 3 +- Documentation/Architecture/API.md | 9 +++ .../Architecture/Release1.7/JSONTransform.md | 15 +++- Documentation/CLI.md | 5 +- Documentation/Qualification/Release1.7.0.md | 75 +++++++++++++++++++ Documentation/Qualification/ReleaseLines.md | 12 ++- Documentation/README.md | 9 ++- README.md | 35 +++++---- RELEASING.md | 10 ++- Scripts/build_documentation_site.py | 6 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- Sources/JSONCLIEngine/CLIReleaseVersion.swift | 2 +- .../Documentation.docc/JSONTransform.md | 3 +- Tests/JSONCLITests/IntegrationTests.swift | 2 +- 16 files changed, 180 insertions(+), 35 deletions(-) create mode 100644 Documentation/Qualification/Release1.7.0.md diff --git a/AGENTS.md b/AGENTS.md index c148b5d..d3d9abe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # Contributor instructions -Development source is 1.7.0-dev; admitted N2 remains unreleased. The stable 1.6 +This source is the unpublished 1.7.0 release candidate; admitted Inspection remains +absent from stable 1.6.1. Qualification exceptions and publication are separate decisions. The stable 1.6 line has eight libraries and no JSONInspection. Preserve the branch-specific inventory and do not publish this development tree as a patch release. @@ -45,6 +46,7 @@ Scalar scanning and immutable plan-local regex sharing are selected private optimizations. Source-attached N3 snapshots and P1 tree/sparse-table projections live in JSONInspection. P2 graph/store/set/matrix/retrieval/persistence and JSON text-sequence framing remain deferred. -JSONTransform is a provisional feature-branch extraction with one private compiler/VM. +JSONTransform is the candidate public library over one private compiler/VM. It depends on JSON and the package-private JSONFilterRuntime kernel. The CLI borrows -the same ledger through its package adapter. No release or final API freeze is implied. +the same ledger through its package adapter. No publication is implied. Preserve the accepted 42 authored Transform declarations; +see Documentation/Qualification/Release1.7.0.md for candidate scope and limitations. diff --git a/CHANGELOG.md b/CHANGELOG.md index dacc0b0..031fdc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ # Changelog -## Provisional JSONTransform extraction +## 1.7.0 — Release candidate (unpublished) + +- Add opt-in JSONTransform over the existing shared compiler and pull VM; retain + immutable Sendable plans, guarded invocation-owned execution, exact native output + admission and package-only cumulative CLI adapters. No second interpreter. +- Include admitted Inspection array evidence, source-attached snapshots, trees and + sparse tables: ten libraries plus CLI, ten catalogs, 1,214 authored declarations. +- Report candidate source version 1.7.0 with deterministic CLI identity; stable + remains 1.6.1. Publication and exact-version consumption are separate later gates. +- Preserve loaded-host timing limits, original 8M refusal and separately approved + private 100k16M profile. Record the explicit one-time allocation-instrumentation + waiver; events, allocated bytes and profiler overhead remain unknown. +- See [candidate qualification](Documentation/Qualification/Release1.7.0.md) for + source-bound gates, Apple/Linux/scoped WASI claims and historical holds. + +### Historical JSONTransform extraction checkpoints - Add an optional public compiler/pull API over the single existing private query kernel. - Preserve CLI ownership through package-only cumulative adapters; add real-engine @@ -8,7 +23,7 @@ - Retain rejected inout substitution and Swift 6.4 borrowed-capture compiler failure evidence; the guarded handle API and release qualification remain provisional. -## 1.7.0 — Unreleased +### Earlier 1.7 development metadata checkpoint (historical) - Retain admitted opt-in JSONInspection: observation-only array evidence, exact first-seen columns and explicit complete/prefix coverage; 44 authored diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f6dc2d..e5f56c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,8 @@ # Contributing Choose the source line deliberately: maintenance changes use the 1.6 line; -new capabilities target unreleased 1.7.0-dev. Keep the lines' API inventories +new capabilities target development. This checkout is the unpublished 1.7.0 +release candidate; keep its accepted API/engine frozen during qualification. Keep the lines' API inventories separate and map applicable maintenance corrections forward by content/commit, without assuming a squashed maintenance tip is an ancestor of main. See the [release-line matrix](Documentation/Qualification/ReleaseLines.md). diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index f6f20c9..9e90cf0 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -333,3 +333,12 @@ and [standalone public SwiftPM consumer](../../Tools/TransformQualification). This is an unreleased candidate, absent from published 1.6.1. No merge, tag, publication, new dialect, implicit floating bridge or feature from another draft follows from this contract. + +## 1.7.0 release-candidate authority + +This unpublished candidate retains the accepted Transform API (42 authored +declarations) and Inspection API (95), with all 56 Navigation declarations +unchanged. Historical provisional headings above remain stable link targets and +checkpoint descriptions; the [candidate contract](../Qualification/Release1.7.0.md) +and final compiler graphs determine current scope. API shape acceptance does not +imply publication, measured allocations or unobserved platform execution. diff --git a/Documentation/Architecture/Release1.7/JSONTransform.md b/Documentation/Architecture/Release1.7/JSONTransform.md index 554c839..4346055 100644 --- a/Documentation/Architecture/Release1.7/JSONTransform.md +++ b/Documentation/Architecture/Release1.7/JSONTransform.md @@ -1,4 +1,17 @@ -# JSONTransform extraction checkpoint +# JSONTransform extraction and release-candidate history + +## Current 1.7.0 candidate disposition + +The accepted 42-declaration public API and one compiler/VM are frozen in the +unpublished 1.7.0 candidate. CLI adapters preserve the original cumulative job +ledger. The former prototype/checkpoint descriptions below remain historical; +the final guarded engine includes terminal refunds and native output admission. +[Candidate qualification](../../Qualification/Release1.7.0.md) requires fresh +source-bound final QA and records the one-time explicit allocation-instrumentation +waiver. Unknown allocation events/bytes/overhead are not passed measurements. +No merge, tag or publication is implied. + +## Historical extraction checkpoints This feature-branch API is provisional. Stable 1.6.1 and development main remain separate authorities. This source adds JSONTransform to the nine development diff --git a/Documentation/CLI.md b/Documentation/CLI.md index c6c8d4e..1065143 100644 --- a/Documentation/CLI.md +++ b/Documentation/CLI.md @@ -46,6 +46,9 @@ source, compiler and architecture; library WASI results are a separate boundary. ## Version and source identity +This candidate reports `1.7.0`; latest stable remains `1.6.1`. The source version +does not establish a published version tag. See the [candidate scope and exception](Qualification/Release1.7.0.md). + The frozen 1.5.0 executable reports `1.4.0-dev` and revision `unknown`; its release notes disclose that limitation. The 1.5.1 maintenance release corrects it, and 1.6.0 reports `1.6.0`. Both use the same source-identity interface: @@ -75,7 +78,7 @@ The [CLI reference](Architecture/Release1.4/CLIReference.md), ## Provisional programmatic transformations -The 1.7 JSONTransform feature branch offers an optional public library over the +The unpublished 1.7.0 candidate offers an optional public library over the same compiler and pull VM used by `swift-json query`. It does not change the CLI flags, framing, input cursor, diagnostics, staged files or exit codes documented here. Public library users supply exact native values and an explicit optional diff --git a/Documentation/Qualification/Release1.7.0.md b/Documentation/Qualification/Release1.7.0.md new file mode 100644 index 0000000..a3c1cd7 --- /dev/null +++ b/Documentation/Qualification/Release1.7.0.md @@ -0,0 +1,75 @@ +# 1.7.0 candidate qualification contract + +**Release candidate; unpublished.** Source version 1.7.0 identifies this candidate, +not a published tag. Latest stable remains 1.6.1. Exact candidate commit/tree, +command receipts, environment and final outcomes belong to the target-bound review +dossier and proposed public qualification assets. This contract does not preclaim +CI success. Merge, tag, release and manual documentation deployment require separate +authorization. Exact-version resolution and independent public downloads follow an +authorized publication; path/revision consumers cannot prove a nonexistent tag. + +## Inventory and semantic authority + +Ten opt-in libraries: JSON, JSONSchema, JSONSchemaExamples, JSONStreaming, JSONPatch, +JSONHTML, JSONBridges, JSONNavigation, JSONInspection and JSONTransform; plus the +swift-json executable. Ten DocC catalogs, 1,214 authored declarations (Inspection95, +Navigation56, Transform42); synthesized declarations are excluded. Root dependencies +remain empty. Regex, filter runtime and CLI engine remain package-private. Original +Apache-2.0 and retained notices/pins apply. Swift6.4+, language6, macOS/iOS27 minima +are unchanged. No new dialect, resolver, fallback, E11/E12 or lower deployment floor. + +JSON owns exact decimals/scalar identity and admission. The accepted Transform engine +uses one private compiler/VM. Public scopes own one finite cumulative execution +ledger; the CLI borrows its existing job ledger through package-only adapters. +Escaped handles invalidate, terminal failure latches, output aliases release and +native output admission precedes publication. Caller callback/comparison work is +not an owned instruction quota; logical retention is not RSS or allocation totals. + +## Required final-source qualification + +Require actual Native Debug/Release, safety/strict/lifetime, docs, Git-free archive, +CLI correctness/limits/oracle, standalone public consumer/API/ownership evidence +and both Linux architectures' behavior/safety on the final source. Preserve all +49 native probes, 16 CLI metrics, discovery/cardinality, fixture pins and policy +exclusions. Prior 21199f9 greens are comparison evidence, not successor passes. +No workflow, compiler flag, timeout, fixture or comparator may be weakened. + +## Explicit performance and allocation decisions + +The maintainer explicitly approved loaded-host observation and the one-time +**1.7.0 allocation-instrumentation waiver**. P4 is waived_explicitly, not passed. Actual allocation +event count, total allocated bytes (including freed allocations) and matched profiler +overhead remain unknown. No allocation-efficiency claim or RSS proxy is allowed. +Adjacent memory-safety and finite-budget evidence do not become allocation data. + +Corrected frozen-source timing medians: 2k +0.6664%, 20k -3.9633%, 100k -2.4996%, +using pre-extraction development 0cb8ca4 as baseline, not published1.6.0/1.6.1. +All observed matched median/retention changes are below absolute >10%; substantial +loaded-host variance precludes a causal speedup/controlled throughput claim. Public +in-memory compile/evaluate costs are absolute observations without a pre1.7 API +baseline. Original offline8M numeric-work refusals after83,333 results remain real; +the privately approved 100k16M measurement profile changes no shipped default. +The distinct historical native stream +10.9932% and its reviewed disposition remain. +Version/docs-only changes can reuse these scopes with explicit engine/flag equality; +any hot-path change requires renewed scoped evidence, never inherited green status. + +## Platform claims and preserved holds + +Final receipts must identify tested macOS27 and Ubuntu24.04/glibc ARM64/x86_64. +Existing wasm32-wasip1 Transform evidence is scoped to23 tests/12 public-consumer +witnesses on the documented Node/WasmKit hosts; unchanged-engine reuse needs an +explicit source-equivalence disposition. It is not full CLI/file-I/O, browser, +Embedded Swift, musl, or a WASI sanitizer pass. No new supported backend is added. + +iOS/iPadOS27 remain declared minima. This candidate makes no new Transform phone/ +tablet runtime qualification claim from macOS tests. Historical phone/simulator +and deferred physical-iPad policies retain their original source/scope. Any stronger +mobile support claim needs actual matching SDK/runtime evidence; lower floors remain +separate. Preserved x86 Swift Build planning crash has unconfirmed cause although +actual unchanged-source safety/lifetime execution passed; inherited NAV8 G4 speed +HOLD and guarded ownership/compiler history remain visible, without invented fixes. + +Final candidate assets must be source-bound and privacy-reviewed: deterministic +source/DocC archives, public qualification/release notes/manifests/checksums. Private +host/runner logs, local paths, approvals and failed traces remain outside public Git +and release uploads. A waived P4 does not waive any other final-source gate. diff --git a/Documentation/Qualification/ReleaseLines.md b/Documentation/Qualification/ReleaseLines.md index 7937767..c7df11f 100644 --- a/Documentation/Qualification/ReleaseLines.md +++ b/Documentation/Qualification/ReleaseLines.md @@ -4,7 +4,7 @@ | --- | --- | --- | | Published 1.6.0 | `f975ac1c5a18af9b422db0994cbc551fd37eee2f` | Eight libraries plus CLI, eight catalogs, 1,077 authored declarations | | Published 1.6.1 (latest stable) | `e431f157ececc0cf3d061192420b0e36a9062f83` | Same eight-library public API and runtime as 1.6.0; no Inspection | -| Unreleased 1.7.0-dev | Main, retaining N2 admitted at `2b24f47e7405821240cf8dfc66b840c9a7f8efa5` | Nine libraries plus CLI, nine catalogs, 1,172 authored declarations (N2 plus N3/P1 additions) | +| Development main before candidate integration | Main, retaining N2 admitted at `2b24f47e7405821240cf8dfc66b840c9a7f8efa5` | Nine libraries plus CLI, nine catalogs, 1,172 authored declarations (N2 plus N3/P1 additions) | Source versions do not establish publication. Consult the [official releases](https://github.com/ThorFuchs/Swift-JSON/releases) for published @@ -36,3 +36,13 @@ within the existing Inspection product; see [the completion contract](../Archite Stable 1.6.1 has neither. Main stays 1.7.0-dev with ten products. G4 controlled performance remains HOLD without speed claims; this is distinct from technical qualification and does not authorize any release/tag. + +## Unpublished 1.7.0 candidate + +The isolated JTX10 successor integrates main +`1a0306d076a08c8c43281f91412a11cd744ff3f6` with accepted Transform +`21199f9af5bf50f1d473c6d3e67b91ae91f75089`. It reports 1.7.0 and has ten libraries +plus CLI, ten catalogs and 1,214 authored declarations (Inspection 95, Transform 42). +It does not change the stable 1.6 line, publish a tag, merge main or retire proposals. +The earlier main/development observations above retain their historical scope. +Use [the candidate contract](Release1.7.0.md) and actual source-bound receipts. diff --git a/Documentation/README.md b/Documentation/README.md index 8f6eefa..f0cd6df 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,7 +1,8 @@ # Swift-JSON documentation The [release-line matrix](Qualification/ReleaseLines.md) separates the eight-library -1.6 maintenance source from nine-library unreleased 1.7.0-dev. The website tracks +1.6 maintenance source from this unpublished ten-library 1.7.0 candidate. +See [candidate qualification and exceptions](Qualification/Release1.7.0.md). The website tracks development main; use matching release assets for a stable compiled reference. Start with the [package README](../README.md) for installation, product selection @@ -72,3 +73,9 @@ Original code is Apache-2.0. See the [license](../LICENSE), [notice](../NOTICE), from the empty library dependency graph. - [Offline provider schemas and request fragments](Architecture/Release1.4/Providers.md), [frozen profile tables](Architecture/Release1.4/ProviderProfiles.json) and the [DocC guide](../Sources/JSONSchema/Documentation.docc/ProviderSchemas.md). + +The candidate also provides [Inspection](../Sources/JSONInspection/Documentation.docc/JSONInspection.md) +and [Transform](../Sources/JSONTransform/Documentation.docc/JSONTransform.md): +ten catalogs and 1,214 authored public declarations in total. These products are +absent from stable 1.6.1. Candidate docs do not establish publication or a platform +runtime pass; source-bound qualification receipts define the tested scope. diff --git a/README.md b/README.md index c8aa3ef..b78fa57 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # Swift-JSON -This source is **1.7.0-dev**, the unreleased development line. It retains the +This source is the **1.7.0 release candidate**, still unpublished. Latest stable +remains **1.6.1**; no 1.7.0 tag or release is implied. It retains the admitted JSONInspection array-evidence API plus source-attached snapshots and -tree/sparse-table projections alongside the unchanged 56-declaration Navigation API. This feature candidate also prototypes JSONTransform: ten libraries, eleven products and ten source catalogs. +tree/sparse-table projections alongside the unchanged 56-declaration Navigation API. This candidate adds the accepted JSONTransform library: ten libraries, eleven products and ten source catalogs. The stable 1.6 line has eight libraries and eight catalogs. Do not use a main snapshot as a 1.6 maintenance release. @@ -32,7 +33,7 @@ eight DocC catalogs and the unchanged 1.6 API/runtime. The optional verified sch examples, structural traversal, structured HTML previews and bounded composition were introduced in 1.6.0. Exact JSON and offline schema validation remain the semantic authorities. -The published CLI reports `1.6.1`; this development source reports `1.7.0-dev`. +The published CLI reports `1.6.1`; this candidate source reports `1.7.0`. Both use a deterministic source-content digest. The [published 1.6.1 release](https://github.com/ThorFuchs/Swift-JSON/releases/tag/1.6.1) is pinned to `e431f157ececc0cf3d061192420b0e36a9062f83`. Use its matching eight-catalog @@ -41,7 +42,7 @@ DocC and qualification assets for the stable reference; see the Historical N0/N1 plans and later candidates are distinguished in the [source authority map](Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md). -This feature source includes opt-in JSONInspection and provisional JSONTransform: +This candidate includes opt-in JSONInspection and JSONTransform: ten libraries plus the CLI, eleven products in total. Inspection is absent from stable 1.6 releases. Use this source checkout for Inspection; its inclusion in main does not publish 1.7.0. @@ -101,7 +102,7 @@ Choose the products your application needs: | **swift-json** | Bounded command-line JSON workflows (available since 1.5.0) | | **JSONBridges** | Opt-in native integer, Double and Foundation Decimal conversions (available in the 1.4.0 tag and 1.5.0 release) | | **JSONNavigation** | Opt-in exact many-node DFS, physical/virtual scopes, finite metadata and explicit coverage, introduced in 1.6.0 | -| **JSONTransform** | Provisional feature: exact query plans, guarded pull execution and finite cumulative limits; see the [extraction checkpoint](Documentation/Architecture/Release1.7/JSONTransform.md) | +| **JSONTransform** | Candidate library: exact query plans, guarded pull execution and finite cumulative limits; see the [extraction checkpoint](Documentation/Architecture/Release1.7/JSONTransform.md) | | **JSONInspection** | Unreleased source: array evidence, complete source-attached snapshots, selected trees and strict sparse object-row tables | | **JSONPatch** | Staged RFC 6902 JSON Patch and RFC 7396 Merge Patch | @@ -543,8 +544,8 @@ The [documentation index](Documentation/README.md) links the behavior contracts. provide public API references, focused guides, parameter descriptions and failure contracts, compiled from the native Swift public symbol graphs. -This source adds unreleased Inspection as a ninth catalog. Build the eight released -catalogs plus the Inspection source catalog on a supported native host: +This candidate has ten catalogs: the eight stable-line products plus Inspection +and Transform. Build the source catalogs on a supported native host: ```sh python3 Scripts/ci.py --lane docs @@ -624,7 +625,7 @@ swift build -c release --product swift-json ### Run exact pull queries -Published 1.6.0 has eight libraries plus the executable; this unreleased source has nine +Published 1.6.0 has eight libraries plus the executable; this candidate has ten libraries plus the executable, with no external root dependencies. Build the selected source version with Swift6.4+ (`swift build -c release --product swift-json`), or use @@ -672,8 +673,8 @@ remain in JSON. Full source admission precedes traversal option validation. See the [compiled traversal guide](Sources/JSONNavigation/Documentation.docc/Traversal.md) and [real external SwiftPM consumer](Tools/NavigationQualification). Navigation is opt-in in the published 1.6.0 release; one-node lookup remains in JSON. This source -also includes unreleased N2 inspection. N3 snapshots and private P1 projections -remain separate unmerged candidates; +also includes unreleased N2 inspection. N3 snapshots and P1 tree/sparse-table projections are admitted in this candidate; +other proposals remain separate; see the [source authority map](Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md). They do not expand the released Navigation API or promote public E11 projection. @@ -703,7 +704,7 @@ numeric matrix or schema admission. See the [compiled evidence guide](Sources/JS ## Complete source-attached structural views (unreleased) -Opt-in JSONInspection on 1.7.0-dev main now combines array observations with a +Opt-in JSONInspection in this candidate combines array observations with a complete parent-linked snapshot, selected physical tree views and strict sparse all-object tables. Use core Pointer/read for one value and Navigation for many-node traversal. Inspection keeps exact typed locations, source order and numeric/key @@ -718,11 +719,15 @@ Ordinals are snapshot-local trusted positions, not durable identity. Logical retention is not RSS; G4 controlled performance remains HOLD, with no speed claim. These additions are absent from published 1.6.1 and do not publish 1.7.0. -## Provisional public transformation library +## Public transformation library candidate -The JSONTransform feature candidate exposes the existing query compiler and pull VM +The JSONTransform 1.7.0 candidate exposes the existing query compiler and pull VM without CLI imports. The CLI uses the same engine. Plans are immutable and Sendable; scoped handles share one cumulative ledger and invalidate on scope exit. The public [guide](Sources/JSONTransform/Documentation.docc/ScopedTransforms.md) and -[standalone consumer](Tools/TransformQualification) document the provisional API. -This product is absent from stable 1.6.1 and remains subject to full qualification. +[standalone consumer](Tools/TransformQualification) document the accepted API. +See the [1.7.0 candidate qualification contract](Documentation/Qualification/Release1.7.0.md) +for source-bound gates, the explicit allocation-instrumentation exception and +platform limits. A path or revision consumer is preparation evidence; exact 1.7.0 +tag consumption is a postauthorization check. +This product is absent from stable 1.6.1 and must pass qualification on its final versioned source before publication. diff --git a/RELEASING.md b/RELEASING.md index 15869bf..8ec2225 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -22,15 +22,19 @@ or releases. Published version tags are protected against updates and deletion. 2. Derive the exact branch-specific product, catalog and API inventory from the candidate manifest and compiler graphs. The 1.6 maintenance line has eight libraries plus CLI, eight catalogs and 1,077 authored declarations; 1.7-dev - has nine libraries plus CLI, nine catalogs and 1,121 declarations. Inspection + candidate has ten libraries plus CLI, ten catalogs and 1,214 authored declarations. Inspection is not a 1.6 patch feature. Review the empty root dependency graph, compiled external consumers, populated pinned suites, policy exclusions, all 49 finite caps, schema/regex behavior and cancellation boundaries. 3. Review licenses, provenance, public file inventory and documentation links. Keep private records, credentials, input clones and build outputs out of source archives and Git history. -4. Measure changed workloads on an idle supported host. A 10% median or retention - change triggers review. Preserve the documented streaming regression disposition; +4. Measure changed workloads on an idle supported host unless a specific maintainer + exception is recorded. For this 1.7.0 candidate, the maintainer explicitly authorized + loaded-host observations and a one-time allocation-instrumentation waiver; actual + allocation events/bytes/overhead remain unknown, not passed measurements. See + Documentation/Qualification/Release1.7.0.md. A 10% median or retention + change still triggers review. Preserve the documented streaming regression disposition; logical retained bytes do not imply an RSS guarantee. 5. Update CHANGELOG.md and compatibility notes, then run `python3 Scripts/archive_candidate.py --output Artifacts/Swift-JSON-public-candidate.tar.gz`. diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index e9a5dfa..b9f95ff 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -20,7 +20,7 @@ REPOSITORY = 'https://github.com/ThorFuchs/Swift-JSON' BASE_PATH = '/Swift-JSON' SITE_URL = 'https://thorfuchs.github.io' + BASE_PATH + '/' -SOURCE_VERSION = '1.7.0-dev' +SOURCE_VERSION = '1.7.0' STABLE_VERSION = '1.6.1' GUIDES = {'JSON': ('Exact values and native coding', 'exactvaluesandcoding'), 'JSONSchema': ('Offline schema validation', 'offlinevalidation'), @@ -42,7 +42,7 @@ 'JSONBridges': 'Opt-in trusted integer construction and policy-explicit Double and Foundation Decimal projection.', 'JSONNavigation': 'Exact typed DFS with physical and virtual scopes, explicit coverage and one cumulative budget.', 'JSONInspection': 'Array evidence, complete source-attached snapshots, tree views and strict sparse tables with finite reports.', - 'JSONTransform': 'Provisional exact query plans and guarded pull executions with cumulative finite limits.'} + 'JSONTransform': 'Exact query plans and guarded pull executions with cumulative finite limits.'} class Links(HTMLParser): @@ -111,7 +111,7 @@ def landing(commit):

Exact JSON. Clear contracts.

Keep decimal values exact, Unicode scalar keys distinct and resource use explicit. Explore {len(PRODUCTS)} Swift libraries that carry those semantics from input bytes to validated, transformed and encoded output.

Swift 6.4+ · Swift language mode 6 · macOS/SDK 27+ · No package dependencies

-

Development main includes JSONInspection. Stable {STABLE_VERSION} has eight library catalogs; use its matching compiled DocC assets or tagged source documentation.

+

Development main includes JSONInspection; this unpublished candidate also includes JSONTransform. Stable {STABLE_VERSION} has eight library catalogs; use its matching compiled DocC assets or tagged source documentation.

{''.join(cards)}

Start with the contract

Read the package overview and executable examples, then choose a reference above. The behavior contracts explain exact numbers, scalar identity, offline schema evaluation, cumulative budgets and stream completion.

diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index c92b4e5..87f43d3 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:bb50a684bcc13618a716a57c093762191f4c50003aa566208b36b54caa141669" + package static let revision = "sha256:1a8c832e81601d2a65ab0ac0c5e17d8827b8a096ecedf060ca19613db6c1c18f" package static let kind = "source-sha256" } diff --git a/Sources/JSONCLIEngine/CLIReleaseVersion.swift b/Sources/JSONCLIEngine/CLIReleaseVersion.swift index edbb5a4..6ed3782 100644 --- a/Sources/JSONCLIEngine/CLIReleaseVersion.swift +++ b/Sources/JSONCLIEngine/CLIReleaseVersion.swift @@ -2,5 +2,5 @@ // Version of this source distribution. Publication is a separate action. package enum CLIReleaseVersion { - package static let value = "1.7.0-dev" + package static let value = "1.7.0" } diff --git a/Sources/JSONTransform/Documentation.docc/JSONTransform.md b/Sources/JSONTransform/Documentation.docc/JSONTransform.md index eb28700..982e599 100644 --- a/Sources/JSONTransform/Documentation.docc/JSONTransform.md +++ b/Sources/JSONTransform/Documentation.docc/JSONTransform.md @@ -2,7 +2,8 @@ Compile the existing exact query dialect and pull bounded native JSON results. -This is a provisional 1.7.0 feature-branch product. It shares one private compiler +This is the optional transformation library in the unpublished 1.7.0 release candidate. +Publication requires a separate maintainer decision. It shares one private compiler and VM with `swift-json query`; it does not introduce a new interpreter or numeric authority. Plans are immutable and Sendable. Each synchronous invocation has one finite cumulative meter and a guarded, non-Sendable execution handle. diff --git a/Tests/JSONCLITests/IntegrationTests.swift b/Tests/JSONCLITests/IntegrationTests.swift index 146931d..5157667 100644 --- a/Tests/JSONCLITests/IntegrationTests.swift +++ b/Tests/JSONCLITests/IntegrationTests.swift @@ -146,7 +146,7 @@ private func integrationDirectory() throws -> URL { @Test func cliVersionIsDeterministicAndNeedsNoRepositoryOrInput() throws { let metadata = try JSONParser().parse(CLIHelp.versionJSON) - #expect(try metadata.requiredMember("version") == .string("1.7.0-dev")) + #expect(try metadata.requiredMember("version") == .string("1.7.0")) #expect(try metadata.requiredMember("revisionKind") == .string("source-sha256")) let revision = try #require(try metadata.requiredMember("revision").stringValue) #expect(revision.hasPrefix("sha256:") && revision.utf8.count == 71) From 6fe80677a5aab9146f45b39d52c72c473568c61f Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:52:33 +0200 Subject: [PATCH 15/25] Reconcile current catalog and CI inventory authority for 1.7.0 RC --- Documentation/CI/README.md | 19 +++++++++++++++++++ Documentation/DocumentationTooling.md | 13 +++++++------ Documentation/README.md | 2 +- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 2c1de90..02cde7c 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,22 @@ +# 1.7.0 candidate qualification authority + +This candidate's exact discovery requires **802 native /786 Linux** functions, +325 parameter invocations each, and 52 native /51 Linux shared-state sanitizer +functions. It has ten library catalogs and eleven products, including the accepted +42-declaration JSONTransform facade over the unchanged compiler/VM. All 49 native +cap probes, 16 CLI metrics, 126 maintenance identities, pinned-suite policies, +public consumers and ownership/cardinality proofs remain required. Native has +four lanes (native/safety/docs/archive); Linux has behavior/safety on both supported +architectures, with composition separate and additive. Actual source-bound receipts +establish execution; discovery expectations and this page are not pass claims. + +The [candidate contract](../Qualification/Release1.7.0.md) records loaded-host timing +and the explicit one-time P4 instrumentation waiver: events/bytes/overhead remain +unknown, not measured zero or a passed allocation gate. It preserves platform +claims, the original x86 planner incident and inherited NAV8 G4 speed-claim HOLD. +WASI Transform evidence is scoped; no full-root or mobile runtime pass is inferred. +Earlier sections below retain their historical source/count contexts. + # NAV8 Navigation V1 integration Current required discovery retains 779 native /763 Linux /675 WASI functions, diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index c8da8bb..9d1ae6c 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -1,7 +1,7 @@ # Building and maintaining the DocC catalogs Swift source comments are the API documentation authority. Native compiler symbol -graphs supply the declarations and links for nine candidate Swift DocC catalogs. Each catalog +graphs supply the declarations and links for ten candidate Swift DocC catalogs. Each catalog combines the product reference with curated Topics and a focused usage guide. See the [Swift DocC documentation](https://www.swift.org/documentation/docc/). @@ -19,7 +19,7 @@ python3 Scripts/ci.py --lane docs The lane records native preflight, extracts fresh public symbol graphs, requires summaries and named-parameter documentation on every authored public declaration, checks README Swift fences against their executable consumer source, runs that -consumer, and compiles all nine candidate DocC catalogs with warnings as errors. Zero public +consumer, and compiles all ten candidate DocC catalogs with warnings as errors. Zero public symbols, missing comments, stale examples or a failed catalog fail the lane. The lane also executes the standalone schema-examples consumer (15 preserved cases and two @@ -40,12 +40,13 @@ statistics; the consumer imports no private meters. | `.build/handoff/docc/JSONBridges.doccarchive` | Opt-in native numeric conversion and fidelity guide | | `.build/handoff/docc/JSONNavigation.doccarchive` | Opt-in exact structural traversal and coverage | | `.build/handoff/docc/JSONInspection.doccarchive` | Observed array evidence and row coverage | +| `.build/handoff/docc/JSONTransform.doccarchive` | Exact compiled plans, guarded pull scopes, finite reports and typed failures | | `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | | `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | | `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | -| `.build/handoff/docc/compiled-catalogs.tar.gz` | All nine candidate compiled catalogs with their exact native filenames | +| `.build/handoff/docc/compiled-catalogs.tar.gz` | All ten candidate compiled catalogs with their exact native filenames | | `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Independently verifiable hashes for every archived catalog file | -| `.build/handoff/pages/site/` | Static site, landing page and all nine candidate references under the project base path | +| `.build/handoff/pages/site/` | Static site, landing page and all ten candidate references under the project base path | | `.build/handoff/pages/receipt.json` | Native transformations, input/output hashes and route/asset verification | Open a `.doccarchive` in Xcode for the compiled reference. To preview a source @@ -74,7 +75,7 @@ The manifest and receipt preserve file-level and archive-level digests. The docs lane uses native `docc process-archive transform-for-static-hosting` to prepare each qualified archive under `/Swift-JSON/`. A shared landing -page links all nine candidate references and guides. The build verifies product/guide routes, +page links all ten candidate references and guides. The build verifies product/guide routes, local HTML links and assets, rejects private-path/credential markers and symlinks, and records a file manifest. Only the generated site is packaged for Pages; source graphs, command logs and private maintainer records are not website content. @@ -91,7 +92,7 @@ native docs lane; run them separately with `python3 -B Scripts/test_documentatio Browser acceptance must also open a guide directly, confirm the sidebar has links without a fetching error, follow a sidebar symbol link and return to a guide. -Repeat direct guide navigation for all nine candidate catalogs and the five schema-example +Repeat direct guide navigation for all ten candidate catalogs and the five schema-example guides. A successful article render or static HTML-link check alone does not prove that the client fetched its navigator index. diff --git a/Documentation/README.md b/Documentation/README.md index f0cd6df..dad5ee5 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -53,7 +53,7 @@ Online references track main; tagged releases include matching compiled catalogu - [Eight-library compiled source example](../Examples/Consumer/README.md) - [Schema example public API](Architecture/Release1.6/API.md), [resource scope](Architecture/Release1.6/SchemaExamplesLimits.md) and [runnable guide examples](../Examples/SchemaExamplesConsumer/README.md) - [Published Navigation and historical N0/N1 source authority](Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md), [original traversal decisions](Architecture/StructuralIntelligence/API-DECISIONS.md) and [N1 promotion history](Architecture/StructuralIntelligence/N1Promotion.md). JSONNavigation is released in 1.6.0; the original N0/N1 stage gates remain dated history. -- [Unreleased N2 Inspection source](Architecture/StructuralIntelligence/Inspection.md) adds opt-in observed array evidence. This source has nine library catalogs; stable 1.6 release tags and assets retain eight. +- [Unreleased N2 Inspection source](Architecture/StructuralIntelligence/Inspection.md) adds opt-in observed array evidence. This candidate has ten library catalogs including Transform; stable 1.6 release tags and assets retain eight. - The source authority map distinguishes this unreleased Inspection source from the dated unmerged-candidate record. N3 snapshots, private P1, composition accounting, root reuse, DSL and deployment retain separate source-bound qualification and approval gates. - [Downstream JSON/JSONSchema interface](ConsumerInterface.md) and [runnable public consumer](../Tools/MCPReadiness/README.md) From 51785a3012a7b115e347ef5f463d7534584ec31c Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:48:59 +0200 Subject: [PATCH 16/25] Prove private metered preparation before source admission --- .../NAV12Preparation.md | 33 ++++ .../Qualification/TestDiscovery.json | 24 ++- Scripts/ci.py | 34 +++- Scripts/qualify_store_preparation.py | 72 +++++++ Scripts/test_qualification_harness.py | 21 +- .../JSON/Reading/JSONReadPreparation.swift | 35 ++++ Sources/JSON/Reading/JSONReadScope.swift | 78 +++++++- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- Sources/JSONNavigation/JSONTraversal.swift | 25 ++- .../PreparedEntryTests.swift | 184 ++++++++++++++++++ 10 files changed, 489 insertions(+), 19 deletions(-) create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md create mode 100644 Scripts/qualify_store_preparation.py create mode 100644 Sources/JSON/Reading/JSONReadPreparation.swift create mode 100644 Tests/JSONNavigationTests/PreparedEntryTests.swift diff --git a/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md b/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md new file mode 100644 index 0000000..0d4ef01 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md @@ -0,0 +1,33 @@ +# Private prepared-entry experiment + +NAV12 D0 adds a package-only preparation strategy and exclusive noncopyable +capability in JSON. Preparation owns no source authority: its mutating methods +only charge/admit profile text, compare exact strings, reserve/release logical +bytes and check/latch original health. Copyable reducer state cannot own it. +The prepared Navigation entry first creates one native read ledger, prepares, +then admits the full original document/source/map on that ledger and runs the +existing owned executor. It never nests a new reader or creates another meter. + +The ordinary entry retains its original eager initializer, counters and admission +precedence. The scope has preparing/admitted/invalidated phases and the existing +one-shot source-transfer designation. Selection, path construction, completion +and transfer refuse before admission and latch failure. No source/cursor/state/ +meter is returned through the preparation capability. Completion refunds source +admission only when the result does not own it; profile output reservations are +not incorrectly included in the input refund. + +Full Debug/Release code generation supplements the unchanged original 2-positive/ +20-negative matrix with 2 positive and 36 ordinary-negative prepared-capability +outcomes. A compiler crash, permitted escape or incidental diagnostic blocks this +experiment. Runtime tests require every-counter no-op parity, one root admission, +cumulative work/exact/one-below quotas, pre-admission refusal, caught-error and +inner-text cancellation priority, one-shot transfer, teardown and independent +concurrent invocations. Logical reservations are not heap/RSS bounds. + +Run `python3 Scripts/ci.py --lane store-preparation` for source-bound receipts. +The lane requires all ten named Swift Testing identities, Release ASan, targeted +TSan, strict-memory compilation and both compiler matrices. Native/Linux retain +the original gates and add the prepared matrix; all 49 core-cap witnesses and +126 maintenance identities remain mandatory. Actual receipts, not this contract, +establish execution. No public declaration/product, store-follow engine, merge +or release is introduced by this preparation increment. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 6f5fc8a..5aaf8b5 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -10,7 +10,7 @@ "JSONHTMLTests": 97, "JSONBridgesTests": 10, "JSONCLITests": 88, - "JSONNavigationTests": 47, + "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76 }, @@ -33,7 +33,7 @@ "JSONHTMLTests": 5, "JSONBridgesTests": 1, "JSONCLITests": 1, - "JSONNavigationTests": 1, + "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, "JSONInspectionTests": 4 }, @@ -74,7 +74,7 @@ "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONCLITests": 88, - "JSONNavigationTests": 47, + "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76 }, @@ -87,7 +87,7 @@ "JSONIntegrationTests": 32, "JSONHTMLTests": 81, "JSONBridgesTests": 10, - "JSONNavigationTests": 47, + "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, "JSONInspectionTests": 76 }, @@ -101,12 +101,24 @@ "JSONHTMLTests": 4, "JSONBridgesTests": 1, "JSONCLITests": 1, - "JSONNavigationTests": 1, + "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, "JSONInspectionTests": 4 }, "export_functions": { "JSONSchemaTests": 1, "JSONHTMLTests": 39 - } + }, + "prepared_entry_function_ids": [ + "JSONNavigationTests.navigationPreparedD0NoopMatchesOriginalEveryCounter()", + "JSONNavigationTests.navigationPreparedD0CumulativeWorkAndSingleSourceAdmission()", + "JSONNavigationTests.navigationPreparedD0ProfileTextRetainedAndCounted()", + "JSONNavigationTests.navigationPreparedD0InnerProfileTextCancellationPrecedesRoot()", + "JSONNavigationTests.navigationPreparedD0CaughtProfileFailurePrecedesAdmission()", + "JSONNavigationTests.navigationPreparedD0CancellationCannotBecomeReplacement()", + "JSONNavigationTests.navigationPreparedD0ForbiddenPreAdmissionAccessLatches()", + "JSONNavigationTests.navigationPreparedD0OneShotAdmissionAndTransfer()", + "JSONNavigationTests.navigationPreparedD0SourceTransferAndInvalidationAllExits()", + "JSONNavigationTests.navigationPreparedD0IndependentConcurrentHealth()" + ] } diff --git a/Scripts/ci.py b/Scripts/ci.py index 096ea07..9127b86 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -13,7 +13,7 @@ SHARED_TESTS += '|compositionIndependentScopesCanBeSharedAcrossTasks' SHARED_TESTS += '|qaStreaming|qaE2E(SharedPlan|RootImport|NDJSONImport|AwaitedSink|ExactWorkflow)' SHARED_TESTS += '|g1SharedPreparedGeneratorHasIndependentConcurrentInvocations|g3SharedTypedGeneratorHasIndependentConcurrentInvocations|g4SharedAdvancedGeneratorHasIndependentConcurrentInvocations|g5SharedPreparationSeparatesConcurrentFailureAndSuccessHealth' -SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' +SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations|navigationPreparedD0IndependentConcurrentHealth' SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics' @@ -25,6 +25,17 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + if lane == 'store-preparation': + return [ + ['python3', '-B', 'Scripts/public_check.py'], + ['swift', 'test', '--test-product', 'JSONNavigationTests', '--filter', 'navigationPreparedD0'], + ['swift', 'test', '--test-product', 'JSONNavigationTests', '-c', 'release', '--filter', 'navigationPreparedD0'], + ['swift', 'test', '--test-product', 'JSONNavigationTests', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/store-preparation-address', '--filter', 'navigationPreparedD0'], + ['swift', 'test', '--test-product', 'JSONNavigationTests', '-c', 'release', '--sanitize', 'thread', '--scratch-path', '.build/store-preparation-thread', '--filter', 'navigationPreparedD0IndependentConcurrentHealth'], + ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/store-preparation-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], + ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'original-owned-codegen')], + ['python3', '-B', 'Scripts/qualify_store_preparation.py', '--output', str(output / 'prepared-codegen')], + ] if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], ['swift', 'test', '--filter', 'inspection|snapshot|projectionP'], @@ -33,6 +44,7 @@ def commands(lane, output): ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/inspection-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], + ['python3', '-B', 'Scripts/qualify_store_preparation.py', '--output', str(output / 'prepared-codegen')], ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')]] return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] @@ -174,6 +186,7 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], + ['python3', '-B', 'Scripts/qualify_store_preparation.py', '--output', str(output / 'prepared-codegen')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -221,6 +234,7 @@ def check_result(command, code, log, events=None): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if events is not None: counts = events['bundle_counts'] + prepared_test = any('navigationPreparedD0' in x for x in command) if command[:2] == ['swift', 'test']: expected = [DISCOVERY['composition_external_functions']] if 'Tools/CompositionQualification' in command else [12] if 'composition|composed|caughtOperationalFailureCannotComplete' in command else [79] if 'JSONSchemaTests' in command else [4 if 'thread' in command else 98] if 'JSONSchemaExamplesTests' in command else [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 'JSONSchemaExamplesTests' in command and events is not None: @@ -236,12 +250,19 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONInspectionTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 and check_projection_cases(events) + if prepared_test: + expected_ids = set(DISCOVERY['prepared_entry_function_ids']) + if 'navigationPreparedD0IndependentConcurrentHealth' in command: + expected_ids = {x for x in expected_ids if 'IndependentConcurrentHealth' in x} + expected = [len(expected_ids)] + if events is not None: + passed = passed and events['module_function_counts'] == {'JSONNavigationTests': len(expected_ids)} and set(events['function_names']) == expected_ids and events['parameter_case_invocations'] == 0 passed = passed and sorted(counts) == expected if 'Tools/CompositionQualification' in command and events is not None: passed = passed and events['module_function_counts'] == {'CompositionScaleTests': DISCOVERY['composition_external_functions']} and events['parameter_case_invocations'] == 0 if 'qa' in command and events is not None: passed = passed and events['module_function_counts'] == DISCOVERY['qa_functions'] and check_restored(events) - if 'thread' in command and 'JSONSchemaExamplesTests' not in command and events is not None: + if 'thread' in command and 'JSONSchemaExamplesTests' not in command and not prepared_test and events is not None: passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) if '--filter' not in command and '--test-product' not in command and not any(x.startswith('Tools/') for x in command): if events is not None: @@ -304,6 +325,11 @@ 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_store_preparation.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + probes = [x for x in data['commands'] if x.get('expected') is not None] + passed = passed and data['status'] == 'passed' and len(data['commands']) == 42 and len(probes) == 36 + passed = passed and all(x['passed'] and not x['compiler_crash'] for x in data['commands']) if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_snapshot.py']: @@ -328,12 +354,12 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=17' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=18' in log.splitlines() return passed, counts def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) + parser.add_argument('--lane', choices=['store-preparation', 'composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) diff --git a/Scripts/qualify_store_preparation.py b/Scripts/qualify_store_preparation.py new file mode 100644 index 0000000..e1ff1eb --- /dev/null +++ b/Scripts/qualify_store_preparation.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Full-codegen prepared-capability ownership matrix. No store engine.""" +import argparse,hashlib,json,pathlib,re,subprocess +from archive_candidate import files +ROOT=pathlib.Path(__file__).resolve().parents[1] +POSITIVE="""import JSON +import JSONNavigation +struct P: JSONReadPreparation, JSONTraversalOwnedReducer { + mutating func prepare(context: inout JSONReadPreparationContext) throws { + try context.admitText("profile"); try context.work(7) + let same = try context.equalStrings("é", "é"); precondition(same) + } + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { try context.work(); return .continue } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> JSONDocument { try context.transferDocumentOwnership() } +} +func legal(_ d: JSONDocument) throws -> JSONOperationReport { + var reducer=P(); return try JSONTraversal(document:d).inspectPreparedOwned(reducer:&reducer) +} +""" +PROBES={ + 'positive':(POSITIVE,None), + 'return':('func illegal(_ c: inout JSONReadPreparationContext) -> JSONReadPreparationContext { consume c }','missing reinitialization of inout parameter'), + 'escape':('func illegal(_ c: inout JSONReadPreparationContext) -> () throws -> Void { { try c.checkpoint() } }',"escaping closure captures 'inout' parameter"), + 'explicit-capture':('func illegal(_ c: inout JSONReadPreparationContext) -> () -> Void { { [c = consume c] in _ = consume c } }', 'missing reinitialization of inout parameter'), + 'sendable':('func illegal(_ c: inout JSONReadPreparationContext) -> @Sendable () throws -> Void { { try c.checkpoint() } }',"capture of 'c' with non-Sendable type"), + 'task':('func illegal(_ c: inout JSONReadPreparationContext) { _ = Task { try c.checkpoint() } }',"escaping closure captures 'inout' parameter"), + 'task-detached':('func illegal(_ c: inout JSONReadPreparationContext) { _ = Task.detached { try c.checkpoint() } }',"escaping closure captures 'inout' parameter"), + 'store':('final class Box { var c: JSONReadPreparationContext? }; func illegal(_ c: inout JSONReadPreparationContext) -> Box { let b=Box(); b.c=consume c; return b }','missing reinitialization of inout parameter'), + 'global':('@MainActor var saved: JSONReadPreparationContext?; @MainActor func illegal(_ c: inout JSONReadPreparationContext) { saved=consume c }','missing reinitialization of inout parameter'), + 'bound-method':('func illegal(_ c: inout JSONReadPreparationContext) -> () throws -> Void { c.checkpoint }',"cannot reference 'mutating' method as function value"), + 'copy':('func illegal(_ c: inout JSONReadPreparationContext) { let a=copy c; _=a }',"'copy' cannot be applied to noncopyable types"), + 'consume':('func take(_ c: consuming JSONReadPreparationContext) {}\nfunc illegal(_ c: inout JSONReadPreparationContext) { take(consume c) }','missing reinitialization of inout parameter'), + 'forge':('func illegal() throws { let s=try JSONReadScope(preparingWith:.balanced); _=JSONReadPreparationContext(scope:s) }','initializer is inaccessible'), + 'state':('func illegal(_ c: inout JSONReadPreparationContext) { _=c.scope }',"'scope' is inaccessible"), + 'select':('func illegal(_ c: inout JSONReadPreparationContext) throws { _=try c.select([]) }',"has no member 'select'"), + 'span':('func illegal(_ c: inout JSONReadPreparationContext) throws { _=try c.sourceSpan(at:[]) }',"has no member 'sourceSpan'"), + 'complete':('func illegal(_ c: inout JSONReadPreparationContext) throws { _=try c.complete(0) }',"has no member 'complete'"), + 'transfer':('func illegal(_ c: inout JSONReadPreparationContext) throws { _=try c.transferDocumentOwnership() }',"has no member 'transferDocumentOwnership'"), + 'external':('func illegal(_ c: inout JSONReadPreparationContext) throws { try c.checkpoint() }',"cannot find type 'JSONReadPreparationContext' in scope"), +} +def main(): + ap=argparse.ArgumentParser();ap.add_argument('--output',required=True);args=ap.parse_args();out=pathlib.Path(args.output) + if not out.is_absolute():out=ROOT/out + out.mkdir(parents=True,exist_ok=True) + receipt={'status':'running','source_inputs':[{'path':p.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()],'commands':[]} + def save(): (out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + def run(name,cmd,expected=None): + p=subprocess.run(cmd,cwd=ROOT,capture_output=True,text=True,timeout=900);data=p.stdout+p.stderr + crash=p.returncode<0 or any(x in data for x in ['Stack dump:','Assertion failed','Found ownership error','fatal error encountered during compilation']) + diagnostics=re.findall(r': error: ([^\n]+)',data) + patterns=expected if isinstance(expected,list) else [expected] + ok=not crash and (p.returncode==0 if expected is None else p.returncode>0 and any(e.lower() in d.lower() for e in patterns for d in diagnostics)) + log=out/(name+'.log');log.write_text(data) + receipt['commands'].append({'name':name,'command':cmd,'exit':p.returncode,'passed':ok,'compiler_crash':crash,'expected':expected,'primary_diagnostics':diagnostics,'log':str(log),'log_sha256':hashlib.sha256(log.read_bytes()).hexdigest()});save() + print(name,p.returncode,'PASS' if ok else 'BLOCKED',flush=True) + if not ok:raise ValueError('D0 blocked: '+name) + return p.stdout + try: + identity=re.sub('[^a-zA-Z0-9_]','_',ROOT.name.lower()) + if identity[0].isdigit():identity='_'+identity + for config in ['debug','release']: + run(config+'-build',['swift','build','-c',config,'--target','JSONInspection','--jobs','2']) + bp=pathlib.Path(run(config+'-binpath',['swift','build','-c',config,'--show-bin-path']).strip());mods=bp/'Modules' if (bp/'Modules').exists() else bp + for name,(body,expected) in PROBES.items(): + source=out/(name+'.swift');source.write_text(body if name=='positive' else 'import JSON\nimport JSONNavigation\n'+body+'\n') + cmd=['swiftc','-swift-version','6','-warnings-as-errors','-package-name','outside_consumer' if name=='external' else identity,'-module-name','StorePreparationProbe','-I',str(mods),'-Onone' if config=='debug' else '-O','-parse-as-library','-c',str(source),'-o',str(out/(config+'-'+name+'.o'))] + run(config+'-'+name,cmd,expected) + receipt['status']='passed' + except (ValueError,OSError,subprocess.TimeoutExpired) as error:receipt.update(status='blocked',error=str(error)) + save();print('STORE_PREPARATION_CODEGEN '+receipt['status']);return 0 if receipt['status']=='passed' else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 843ec53..1f47be1 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -40,6 +40,19 @@ def test_public_projection_receipt_rejects_incomplete_and_incidental_successes(s else: row['work_units'] = 0 self.assertFalse(check_public_projection_receipt(broken)) + def test_prepared_entry_rejects_missing_renamed_and_wrong_module_cases(self): + command = ['swift', 'test', '--filter', 'navigationPreparedD0'] + names = list(DISCOVERY['prepared_entry_function_ids']) + events = dict(bundle_counts=[10], module_function_counts={'JSONNavigationTests': 10}, + parameter_case_invocations=0, function_names=names) + self.assertTrue(check_result(command, 0, '', events)[0]) + for broken in [dict(events, function_names=names[:-1]), + dict(events, function_names=names[:-1]+['Other.renamed()']), + dict(events, parameter_case_invocations=1), + dict(events, bundle_counts=[9]), + dict(events, module_function_counts={'WrongModule': 10})]: + self.assertFalse(check_result(command, 0, '', broken)[0]) + def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(self): command = ['swift', 'test', '--package-path', 'Tools/CompositionQualification'] events = dict(bundle_counts=[5], module_function_counts={'CompositionScaleTests': 5}, @@ -60,7 +73,7 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel # Combined-source checks must retain both independently owned optional products. for module, count, command in [ - ('JSONNavigationTests', 47, ['swift', 'test', '--filter', 'navigation']), + ('JSONNavigationTests', 57, ['swift', 'test', '--filter', 'navigation']), ('JSONInspectionTests', 76, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: @@ -73,12 +86,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel self.assertFalse(check_result(command, 0, '', broken)[0]) policy = DISCOVERY for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): - self.assertEqual(policy[key]['JSONNavigationTests'], 47) + self.assertEqual(policy[key]['JSONNavigationTests'], 57) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) self.assertEqual(policy[key]['JSONInspectionTests'], 76) self.assertEqual(policy[key]['JSONIntegrationTests'], 32) for key in ('shared_state_functions', 'portable_shared_state_functions'): - self.assertEqual(policy[key]['JSONNavigationTests'], 1) + self.assertEqual(policy[key]['JSONNavigationTests'], 2) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 4) self.assertEqual(policy[key]['JSONInspectionTests'], 4) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) @@ -186,7 +199,7 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): identity['function_names'][0] = 'JSONTests.unrelatedReplacement()' self.assertFalse(check_restored(identity)) harness = ['python3', '-B', 'Scripts/test_qualification_harness.py'] - self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=17')[0]) + self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=18')[0]) for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=16', '']: self.assertFalse(check_result(harness, 0, stale)[0]) lifetime = ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py'] diff --git a/Sources/JSON/Reading/JSONReadPreparation.swift b/Sources/JSON/Reading/JSONReadPreparation.swift new file mode 100644 index 0000000..1a46e35 --- /dev/null +++ b/Sources/JSON/Reading/JSONReadPreparation.swift @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 + +/// Package-only preparation strategy; ordinary Copyable state cannot own the capability. +package protocol JSONReadPreparation { + mutating func prepare(context: inout JSONReadPreparationContext) throws +} + +/// Exclusive profile-only capability on the original read ledger, before admission. +/// No source, cursor, state, meter or nonmutating bound operation can leave this type. +package struct JSONReadPreparationContext: ~Copyable { + private let scope: JSONReadScope + internal init(scope: JSONReadScope) { self.scope = scope } + + package mutating func work(_ units: UInt64 = 1) throws { + try scope.runPreparation { try $0.meter.work(units) } + } + package mutating func retain(_ bytes: UInt64) throws { + try scope.runPreparation { try $0.meter.retain(bytes) } + } + package mutating func release(_ bytes: UInt64) throws { + try scope.runPreparation { try $0.meter.release(bytes) } + } + package mutating func checkpoint() throws { + try scope.runPreparation { try $0.meter.checkpoint() } + } + package mutating func admitText(_ text: String, key: Bool = false) throws { + try scope.runPreparation { try JSONAdmission.text(text, key: key, meter: &$0.meter, owned: true) } + } + package mutating func equalStrings(_ lhs: String, _ rhs: String) throws -> Bool { + try scope.runPreparation { try JSONComparison.textEquals(lhs, rhs, meter: &$0.meter) } + } + package mutating func recordFailure(_ error: any Error) throws { + try scope.runPreparation { $0.meter.recordFailure(error); try $0.meter.checkpoint() } + } +} diff --git a/Sources/JSON/Reading/JSONReadScope.swift b/Sources/JSON/Reading/JSONReadScope.swift index 68f8558..3fff894 100644 --- a/Sources/JSON/Reading/JSONReadScope.swift +++ b/Sources/JSON/Reading/JSONReadScope.swift @@ -4,6 +4,8 @@ import Foundation /// Guarded synchronous ownership; deliberately not Sendable. An escaped handle /// stores only a bounded path and this invalidated scope, never a selected DOM. package final class JSONReadScope { + private enum Phase { case preparing, admitted, invalidated } + private var phase = Phase.admitted private let owner = ObjectIdentifier(Thread.current) package var document: JSONDocument? package var state: JSONCodingState? @@ -32,16 +34,77 @@ package final class JSONReadScope { self.document = document; self.state = state rootBytes = state.meter.used(.retainedBytes) } + /// A prepared entry creates the same native ledger but has no source authority yet. + package init(preparingWith limits: JSONLimits) throws { + state = JSONCodingState(options: .init(limits: limits), meter: try JSONWorkMeter( + limits: limits, domain: .core, trackAcceptedCapacities: true)) + phase = .preparing + } + package func runPreparation(_ body: (JSONCodingState) throws -> T) throws -> T { + do { + guard ObjectIdentifier(Thread.current) == owner, phase == .preparing, + document == nil, let state else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try state.check() + return try body(state) + } catch { + state?.meter.recordFailure(error) + if let state { try state.meter.checkpoint() } + throw error + } + } + package func prepare(_ preparation: inout Preparation) throws { + var context = JSONReadPreparationContext(scope: self) + do { try preparation.prepare(context: &context); try context.checkpoint() } + catch { + state?.meter.recordFailure(error) + if let state { try state.meter.checkpoint() } + throw error + } + } + /// Admit once using the prepared ledger. Profile reservations are not input refunds. + package func admitPrepared(_ source: JSONDocument) throws { + try runPreparation { state in + let before = state.meter.used(.retainedBytes) + try JSONAdmission.value(source.root, meter: &state.meter, owned: true) + if let label = source.source { try JSONAdmission.text(label, meter: &state.meter, owned: true) } + if let bytes = source.sourceBytes { + try state.meter.require(.inputBytes, UInt64(bytes.count)) + for _ in bytes { try state.meter.work(); try state.meter.retain(1) } + } + if let map = source.sourceMap { + for entry in map.entries { + try state.meter.work(); try state.meter.charge(.sourceMapEntries) + try state.meter.retain(JSONLogicalWeights.sourceMapEntry) + for part in entry.path { + try state.meter.work(); try state.meter.retain(JSONLogicalWeights.frame) + if case .key(let key) = part { + try JSONAdmission.text(key.string, key: true, meter: &state.meter, owned: true) + } + } + } + } + rootBytes = state.meter.used(.retainedBytes) - before + } + document = source + phase = .admitted + } package func retirePath(_ bytes: UInt64) { if ObjectIdentifier(Thread.current) == owner, state != nil { retiredPathBytes += bytes } } package func check() throws -> JSONCodingState { guard ObjectIdentifier(Thread.current) == owner else { throw JSONFailure(.configuration, code: .invalidOperation) } - guard document != nil, let state else { throw JSONFailure(.configuration, code: .invalidOperation) } + if phase == .preparing { + let failure = JSONFailure(.configuration, code: .invalidOperation) + state?.meter.recordFailure(failure) + throw failure + } + guard phase == .admitted, document != nil, let state else { throw JSONFailure(.configuration, code: .invalidOperation) } if retiredPathBytes != 0 { try state.meter.release(retiredPathBytes); pathBytes -= retiredPathBytes; retiredPathBytes = 0 } try state.check(); return state } - package func invalidate() { document = nil; state = nil } + package func invalidate() { phase = .invalidated; document = nil; state = nil } /// Transfer the original admitted source alias into a source-attached result. /// Only an owned package reducer uses this one-shot completion designation; /// ordinary reads/visits retain their original input-refund behavior. @@ -106,7 +169,11 @@ package final class JSONReadScope { return .init(byteOffset: span.start, line: line, column: column) } package func select(_ path: [JSONLocationComponent], state: JSONCodingState) throws -> JSON { - guard let document else { throw JSONFailure(.configuration, code: .invalidOperation) } + guard phase == .admitted, let document else { + let failure = JSONFailure(.configuration, code: .invalidOperation) + self.state?.meter.recordFailure(failure) + throw failure + } var value = document.root for part in path { try state.meter.work() @@ -125,6 +192,11 @@ package final class JSONReadScope { return value } package func append(_ part: JSONLocationComponent, to path: [JSONLocationComponent], state: JSONCodingState) throws -> JSONReadPath { + guard phase == .admitted else { + let failure = JSONFailure(.configuration, code: .invalidOperation) + self.state?.meter.recordFailure(failure) + throw failure + } try state.meter.require(.pointerTokens, UInt64(path.count) + 1) var pointerBytes: UInt64 = 0, owned: UInt64 = 0 for position in 0...path.count { diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 0a2b127..2863bf4 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:b7cfb84e34289d4bb2342e2897c637cdf4fd19f2455072696d27f396bf8ef735" + package static let revision = "sha256:10f4665817c36cabc08fd7b1e98c64076851e175e0c332f4abeb313d89c4e609" package static let kind = "source-sha256" } diff --git a/Sources/JSONNavigation/JSONTraversal.swift b/Sources/JSONNavigation/JSONTraversal.swift index f6ea1d2..c2d61de 100644 --- a/Sources/JSONNavigation/JSONTraversal.swift +++ b/Sources/JSONNavigation/JSONTraversal.swift @@ -69,6 +69,21 @@ public struct JSONTraversal: Sendable { }) } + /// Profile preparation, admission and the ordinary reducer share one original scope. + package func inspectPreparedOwned( + scope: JSONTraversalScope = .subtree(at: []), + options: JSONTraversalOptions = .init(), reducer: inout Reducer) + throws -> JSONOperationReport { + try execute(scope: scope, options: options, collectionLimit: nil, prepared: true, + prepare: { try $0.prepare(&reducer) }, { hit, value, context in + do { return try reducer.observe(hit, value: value, context: &context) } + catch { try context.recordFailure(error); throw error } + }, finish: { collection, context in + do { return try reducer.finish(collection.summary, context: &context) } + catch { try context.recordFailure(error); throw error } + }) + } + private struct Frame { let value: JSON let path: JSONReadPath @@ -83,14 +98,22 @@ public struct JSONTraversal: Sendable { private func execute(scope selection: JSONTraversalScope, options: JSONTraversalOptions, collectionLimit: Int?, + prepared: Bool = false, + prepare: (JSONReadScope) throws -> Void = { _ in }, _ visitor: (JSONTraversalHit, JSON, inout JSONTraversalContext) throws -> JSONTraversalAction, finish: (JSONTraversalCollection, inout JSONTraversalContext) throws -> Result) throws -> JSONOperationReport { - let scope = try JSONReadScope(document, limits: limits) + let scope = try prepared ? JSONReadScope(preparingWith: limits) + : JSONReadScope(document, limits: limits) defer { scope.invalidate() lifetimeObserver?(scope.document == nil, scope.state == nil) } + if prepared { + scope.state?.meter.checkpointObserver = checkpointObserver + try prepare(scope) + try scope.admitPrepared(document) + } let state = try scope.check() var context = JSONTraversalContext(scope: scope) state.meter.checkpointObserver = checkpointObserver diff --git a/Tests/JSONNavigationTests/PreparedEntryTests.swift b/Tests/JSONNavigationTests/PreparedEntryTests.swift new file mode 100644 index 0000000..94e5a3c --- /dev/null +++ b/Tests/JSONNavigationTests/PreparedEntryTests.swift @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation + +private enum PreparationMarker: Error { case original, replacement } +private struct PreparedProbe: JSONReadPreparation, JSONTraversalOwnedReducer { + enum Mode { case noop, charged, text, caught, cancel, transferred, doubleTransfer } + var mode = Mode.noop + var text = "é" + var prepared = false + mutating func prepare(context: inout JSONReadPreparationContext) throws { + prepared = true + switch mode { + case .charged: try context.work(17) + case .text: try context.admitText(text) + case .caught: + do { try context.recordFailure(PreparationMarker.original) } catch { } + case .cancel: + withUnsafeCurrentTask { $0?.cancel() } + do { try context.checkpoint() } catch { } + throw PreparationMarker.replacement + default: break + } + } + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + #expect(prepared) + return .continue + } + mutating func finish(_ summary: JSONTraversalSummary, + context: inout JSONTraversalContext) throws -> JSONDocument? { + if mode == .charged { try context.work(19) } + if mode == .transferred || mode == .doubleTransfer { + let source = try context.transferDocumentOwnership() + if mode == .doubleTransfer { + do { _ = try context.transferDocumentOwnership() } catch { } + } + return source + } + return nil + } +} + +@Test func navigationPreparedD0NoopMatchesOriginalEveryCounter() throws { + let source = try JSONParser().parseDocument(Data(#"{"x":[1,null],"é":"e\u0301"}"#.utf8)) + let walk = JSONTraversal(document: source) + let original = try walk.visit { _, _ in .continue } + var reducer = PreparedProbe() + let prepared = try walk.inspectPreparedOwned(reducer: &reducer) + for metric in JSONResourceMetric.allCases { #expect(original.statistics[metric] == prepared.statistics[metric]) } + #expect(original.statistics.peakLogicalRetainedBytes == prepared.statistics.peakLogicalRetainedBytes) +} + +@Test func navigationPreparedD0CumulativeWorkAndSingleSourceAdmission() throws { + let walk = JSONTraversal(document: .init(root: [0,1,2])) + let original = try walk.visit { _, _ in .continue } + var reducer = PreparedProbe(mode: .charged) + let report = try walk.inspectPreparedOwned(reducer: &reducer) + #expect(report.statistics[.nodes] == original.statistics[.nodes]) + #expect(report.statistics[.workUnits] == original.statistics[.workUnits] + 36) + var limits = JSONLimits.balanced + limits[.workUnits] = report.statistics[.workUnits] + reducer = .init(mode: .charged) + #expect(try JSONTraversal(document: walk.document, limits: limits).inspectPreparedOwned(reducer: &reducer).statistics[.workUnits] == limits[.workUnits]) + limits[.workUnits] -= 1; reducer = .init(mode: .charged) + do { _ = try JSONTraversal(document: walk.document, limits: limits).inspectPreparedOwned(reducer: &reducer); Issue.record("Expected cumulative work refusal") } + catch let failure as JSONFailure { #expect(failure.resource?.metric == .workUnits) } +} + +@Test func navigationPreparedD0ProfileTextRetainedAndCounted() throws { + var reducer = PreparedProbe(mode: .text) + let result = try JSONTraversal(document: .init(root: .null)).inspectPreparedOwned(reducer: &reducer) + #expect(result.statistics[.aggregateTextBytes] == 2) + #expect(result.statistics[.retainedBytes] == 2) + #expect(result.statistics[.nodes] == 1) + var limits = JSONLimits.balanced + limits[.retainedBytes] = result.statistics.peakLogicalRetainedBytes + reducer = .init(mode: .text) + _ = try JSONTraversal(document: .init(root: .null), limits: limits).inspectPreparedOwned(reducer: &reducer) + limits[.retainedBytes] -= 1; reducer = .init(mode: .text) + do { _ = try JSONTraversal(document: .init(root: .null), limits: limits).inspectPreparedOwned(reducer: &reducer); Issue.record("Expected live retention refusal") } + catch let failure as JSONFailure { #expect(failure.resource?.metric == .retainedBytes) } +} + +@Test func navigationPreparedD0InnerProfileTextCancellationPrecedesRoot() async throws { + let text = String(repeating: "é", count: 1024) + let task = Task { + var limits = JSONLimits.balanced; limits[.nodes] = 0 + var walk = JSONTraversal(document: .init(root: .null), limits: limits) + walk.checkpointObserver = { work in + if work >= 1023 { withUnsafeCurrentTask { $0?.cancel() } } + } + var reducer = PreparedProbe(mode: .text, text: text) + return try walk.inspectPreparedOwned(reducer: &reducer) + } + do { _ = try await task.value; Issue.record("Expected cancellation within profile text") } + catch is CancellationError { } +} + +@Test func navigationPreparedD0CaughtProfileFailurePrecedesAdmission() throws { + var limits = JSONLimits.balanced; limits[.nodes] = 0 + var reducer = PreparedProbe(mode: .caught) + do { _ = try JSONTraversal(document: .init(root: .null), limits: limits).inspectPreparedOwned(reducer: &reducer); Issue.record("Expected original preparation error") } + catch PreparationMarker.original { } +} + +@Test func navigationPreparedD0CancellationCannotBecomeReplacement() async throws { + let task = Task { + var reducer = PreparedProbe(mode: .cancel) + return try JSONTraversal(document: .init(root: .null)).inspectPreparedOwned(reducer: &reducer) + } + do { _ = try await task.value; Issue.record("Expected original cancellation") } + catch is CancellationError { } +} + +@Test func navigationPreparedD0ForbiddenPreAdmissionAccessLatches() throws { + for action in 0..<4 { + let scope = try JSONReadScope(preparingWith: .balanced) + defer { scope.invalidate() } + let state = try #require(scope.state) + do { + switch action { + case 0: _ = try scope.select([], state: state) + case 1: _ = try scope.transferDocumentOwnership() + case 2: _ = try scope.complete(0) + default: _ = try scope.append(.key("x"), to: [], state: state) + } + Issue.record("Unadmitted scope must refuse") + } catch let failure as JSONFailure { #expect(failure.code == .invalidOperation) } + do { try scope.admitPrepared(.init(root: .null)); Issue.record("Invalid phase error must remain terminal") } + catch let failure as JSONFailure { #expect(failure.code == .invalidOperation) } + } +} + +@Test func navigationPreparedD0OneShotAdmissionAndTransfer() throws { + let scope = try JSONReadScope(preparingWith: .balanced) + defer { scope.invalidate() } + try scope.admitPrepared(.init(root: .null)) + do { try scope.admitPrepared(.init(root: .null)); Issue.record("Second admission must fail") } + catch let failure as JSONFailure { #expect(failure.code == .invalidOperation) } + var reducer = PreparedProbe(mode: .doubleTransfer) + do { _ = try JSONTraversal(document: .init(root: .null)).inspectPreparedOwned(reducer: &reducer); Issue.record("Caught second transfer cannot publish") } + catch let failure as JSONFailure { #expect(failure.code == .invalidOperation) } +} + +@Test func navigationPreparedD0SourceTransferAndInvalidationAllExits() throws { + let facts = Mutex<[(Bool,Bool)]>([]) + let source = try JSONParser().parseDocument(Data("[0,1,2]".utf8)) + var walk = JSONTraversal(document: source) + walk.lifetimeObserver = { root, state in facts.withLock { $0.append((root,state)) } } + var reducer = PreparedProbe(mode: .transferred) + let report = try walk.inspectPreparedOwned(reducer: &reducer) + #expect(report.result?.sourceBytes == source.sourceBytes) + #expect(report.statistics[.retainedBytes] > 0) + reducer = .init(mode: .caught) + do { _ = try walk.inspectPreparedOwned(reducer: &reducer); Issue.record("Expected preparation error") } + catch PreparationMarker.original { } + var limits = JSONLimits.balanced; limits[.nodes] = 0 + walk = JSONTraversal(document: source, limits: limits) + walk.lifetimeObserver = { root, state in facts.withLock { $0.append((root,state)) } } + reducer = .init() + do { _ = try walk.inspectPreparedOwned(reducer: &reducer); Issue.record("Expected admission error") } + catch let failure as JSONFailure { #expect(failure.resource?.metric == .nodes) } + let observed = facts.withLock { $0 } + #expect(observed.count == 3 && observed.allSatisfy { $0.0 && $0.1 }) +} + +@Test func navigationPreparedD0IndependentConcurrentHealth() async throws { + let source = JSONDocument(root: [0,1,2]) + try await withThrowingTaskGroup(of: UInt64.self) { group in + for _ in 0..<16 { + group.addTask { + var reducer = PreparedProbe(mode: .charged) + return try JSONTraversal(document: source).inspectPreparedOwned(reducer: &reducer).statistics[.workUnits] + } + } + var counts: [UInt64] = [] + for try await count in group { counts.append(count) } + #expect(counts.count == 16 && counts.allSatisfy { $0 == counts[0] }) + } +} From aa8d6c66aa4f6af9fb34e337b8bef4d2039f8bd6 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:16:11 +0200 Subject: [PATCH 17/25] Construct a private exact configured store on one prepared ledger --- .../Architecture/ExtractionManifest.json | 3 +- .../NAV12PrivateStore.md | 73 + .../Qualification/TestDiscovery.json | 28 +- Package.swift | 2 +- Scripts/ci.py | 43 +- Scripts/qualify_store.py | 76 + Scripts/store_acceptance.py | 43 + Scripts/test_qualification_harness.py | 48 +- Scripts/testing_events.py | 1 + Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Store/JSONConfiguredStore.swift | 389 ++ .../Store/JSONStoreAccounting.swift | 104 + .../Store/JSONStorePreparation.swift | 86 + .../Store/JSONStoreProfile.swift | 98 + .../JSONNavigation/JSONTraversalContext.swift | 14 + .../StoreFixtures/PROVENANCE.json | 10 + .../StoreFixtures/VECTORS.json | 5399 +++++++++++++++++ .../StoreFixtures/cases/D001-empty.json | 3 + .../StoreFixtures/cases/D002-one.json | 9 + .../cases/D003-same-id-cross-collections.json | 14 + .../cases/D004-duplicate-id.json | 12 + .../StoreFixtures/cases/D005-missing-id.json | 9 + .../StoreFixtures/cases/D006-null-id.json | 9 + .../StoreFixtures/cases/D007-id-number.json | 9 + .../StoreFixtures/cases/D008-id-boolean.json | 9 + .../StoreFixtures/cases/D009-id-array.json | 9 + .../StoreFixtures/cases/D010-id-object.json | 9 + .../cases/D011-scalar-record.json | 7 + .../cases/D012-null-collection.json | 5 + .../cases/D013-container-array.json | 3 + .../cases/D014-missing-container.json | 3 + .../cases/D015-wrong-ancestor.json | 3 + .../D016-scalar-distinct-collections.json | 14 + .../cases/D017-scalar-distinct-ids.json | 12 + .../cases/D018-equal-payload-two-records.json | 18 + .../cases/D019-configured-id-only.json | 10 + .../cases/D020-unconfigured-ref-inert.json | 13 + .../cases/D021-disabled-with-fields.json | 5 + .../cases/D022-forward-target.json | 20 + .../cases/D023-unknown-collection.json | 13 + .../StoreFixtures/cases/D024-dangling-id.json | 13 + .../cases/D025-required-absent.json | 9 + .../cases/D026-optional-absent.json | 9 + .../cases/D027-required-null-False.json | 10 + .../cases/D028-required-null-True.json | 10 + .../cases/D029-optional-null-False.json | 10 + .../cases/D030-optional-null-True.json | 10 + .../cases/D031-marker-string.json | 10 + .../cases/D032-marker-id-missing.json | 12 + .../cases/D033-marker-id-null.json | 13 + .../cases/D034-marker-extra.json | 14 + .../D035-marker-missing-beats-extra.json | 13 + .../cases/D036-marker-kind-beats-extra.json | 14 + .../cases/D037-raw-duplicate-marker.json | 1 + .../StoreFixtures/cases/D038-self-cycle.json | 13 + .../cases/D039-mutual-cycle.json | 20 + .../StoreFixtures/cases/D040-slash-tilde.json | 12 + .../cases/D041-empty-identities.json | 13 + .../cases/D042-numeric-key-not-index.json | 5 + .../StoreFixtures/cases/D043-numeric-key.json | 5 + .../D044-configured-collection-absent.json | 3 + .../cases/D045-unconfigured-collection.json | 6 + .../cases/D046-duplicate-declaration.json | 5 + .../D047-scalar-distinct-ref-fields.json | 10 + .../cases/D048-id-ref-collision.json | 5 + .../cases/D049-duplicate-ref-field.json | 5 + .../cases/D050-nested-array-refused.json | 5 + .../cases/D051-unknown-version.json | 5 + .../cases/D052-negative-profile-index.json | 5 + .../cases/D053-nullable-must-be-boolean.json | 5 + .../cases/D054-duplicate-beats-dangling.json | 16 + .../D055-late-malformed-beats-dangling.json | 19 + .../D056-all-headers-before-records.json | 8 + .../cases/D057-configured-field-order.json | 10 + .../cases/D058-unknown-before-dangling.json | 13 + .../cases/D059-marker-collection-null.json | 13 + .../cases/D060-lookalike-marker-key.json | 13 + .../cases/D061-explicit-dollar-ref-field.json | 13 + .../cases/D062-physical-root-container.json | 7 + .../D063-absent-null-linked-separate.json | 20 + .../cases/D064-empty-key-container.json | 5 + .../cases/D065-exact-reference-id.json | 19 + .../StoreFixtures/cases/D066-null-record.json | 7 + .../cases/D067-empty-selected-array.json | 5 + .../cases/D068-number-payload-lexemes.json | 1 + .../cases/D069-unknown-profile-field.json | 5 + .../StoreFixtures/profiles/example.json | 31 + .../StoreRuntimeTests.swift | 292 + .../StoreVectorTests.swift | 137 + 89 files changed, 7515 insertions(+), 26 deletions(-) create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV12PrivateStore.md create mode 100644 Scripts/qualify_store.py create mode 100644 Scripts/store_acceptance.py create mode 100644 Sources/JSONInspection/Store/JSONConfiguredStore.swift create mode 100644 Sources/JSONInspection/Store/JSONStoreAccounting.swift create mode 100644 Sources/JSONInspection/Store/JSONStorePreparation.swift create mode 100644 Sources/JSONInspection/Store/JSONStoreProfile.swift create mode 100644 Tests/JSONInspectionTests/StoreFixtures/PROVENANCE.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/VECTORS.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D001-empty.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D002-one.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D003-same-id-cross-collections.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D004-duplicate-id.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D005-missing-id.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D006-null-id.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D007-id-number.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D008-id-boolean.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D009-id-array.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D010-id-object.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D011-scalar-record.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D012-null-collection.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D013-container-array.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D014-missing-container.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D015-wrong-ancestor.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D016-scalar-distinct-collections.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D017-scalar-distinct-ids.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D018-equal-payload-two-records.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D019-configured-id-only.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D020-unconfigured-ref-inert.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D021-disabled-with-fields.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D022-forward-target.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D023-unknown-collection.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D024-dangling-id.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D025-required-absent.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D026-optional-absent.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D027-required-null-False.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D028-required-null-True.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D029-optional-null-False.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D030-optional-null-True.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D031-marker-string.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D032-marker-id-missing.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D033-marker-id-null.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D034-marker-extra.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D035-marker-missing-beats-extra.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D036-marker-kind-beats-extra.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D037-raw-duplicate-marker.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D038-self-cycle.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D039-mutual-cycle.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D040-slash-tilde.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D041-empty-identities.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D042-numeric-key-not-index.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D043-numeric-key.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D044-configured-collection-absent.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D045-unconfigured-collection.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D046-duplicate-declaration.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D047-scalar-distinct-ref-fields.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D048-id-ref-collision.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D049-duplicate-ref-field.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D050-nested-array-refused.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D051-unknown-version.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D052-negative-profile-index.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D053-nullable-must-be-boolean.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D054-duplicate-beats-dangling.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D055-late-malformed-beats-dangling.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D056-all-headers-before-records.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D057-configured-field-order.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D058-unknown-before-dangling.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D059-marker-collection-null.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D060-lookalike-marker-key.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D061-explicit-dollar-ref-field.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D062-physical-root-container.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D063-absent-null-linked-separate.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D064-empty-key-container.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D065-exact-reference-id.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D066-null-record.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D067-empty-selected-array.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D068-number-payload-lexemes.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/cases/D069-unknown-profile-field.json create mode 100644 Tests/JSONInspectionTests/StoreFixtures/profiles/example.json create mode 100644 Tests/JSONInspectionTests/StoreRuntimeTests.swift create mode 100644 Tests/JSONInspectionTests/StoreVectorTests.swift diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 4a191c4..8c9b6e6 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -302,7 +302,8 @@ "NAV5 public array-evidence packaging, external consumer and DocC are original Apache-2.0 code. Existing exact shape evidence, exclusive reducer, weights and I01-I17 semantics are unchanged; no upstream engine or dependency is adapted. Promotion is a draft candidate, not a released product.", "NAV5 package-only complete structural snapshot, parent-linked metadata, original source-ownership transfer and S01-S24 synthetic tests are original Apache-2.0 code. The existing read scope admits once; its default public read/traversal/inspection completion behavior is preserved. No persistent identity, upstream algorithm or new dependency is introduced.", "NAV6 snapshot access-level promotion, external consumer, independent 28-record maintainer oracle, terminal-transfer/concurrent lookup proofs and compiled DocC are original Apache-2.0 contributions. No upstream engine, new semantic authority, dependency or additional product is adapted.", - "NAV8 current-main N3/P1 reconciliation, minimal public immutable projection API, independent 16-case consumer and DocC/access-control proofs are original Apache-2.0 contributions. NAV7 donor db991c5/bda8aef and packet SHA-256 57ca72cb817272ed466a7cb8810043d56a079190d830ea7b995abd308070f98d retain provenance. No external engine, dependency or product is added." + "NAV8 current-main N3/P1 reconciliation, minimal public immutable projection API, independent 16-case consumer and DocC/access-control proofs are original Apache-2.0 contributions. NAV7 donor db991c5/bda8aef and packet SHA-256 57ca72cb817272ed466a7cb8810043d56a079190d830ea7b995abd308070f98d retain provenance. No external engine, dependency or product is added.", + "NAV12 D0/D1 private configured-store preparation, immutable source-local metadata, one-ledger integration and native semantic/compiler/runtime tests are original Apache-2.0 code. JSONTraversalContext.equalStrings reuses the same original delegate as sibling NAV11 PR #67 at 81485930f859131d0802691ca6370f482ba62911; no sibling implementation is merged. Frozen maintainer-authored synthetic S0 inputs are copied unchanged, with per-file and archive hashes in Tests/JSONInspectionTests/StoreFixtures/PROVENANCE.json and VECTORS.json. No store public declaration, follow engine, parser/serialization authority, upstream runtime dependency or new product is introduced." ], "jsonhtml_export_original_contributions": { "license": "Apache-2.0", diff --git a/Documentation/Architecture/StructuralIntelligence/NAV12PrivateStore.md b/Documentation/Architecture/StructuralIntelligence/NAV12PrivateStore.md new file mode 100644 index 0000000..372c5c3 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV12PrivateStore.md @@ -0,0 +1,73 @@ +# Private configured-store construction + +This D1 experiment belongs to JSONInspection at package access. It adds no public +product or declaration. Ordinary Navigation, shape, snapshot and projection entry +points retain their admission, accounting and completion contracts. This is +complete construction only: no follow, query, graph, persistence or resolver API. + +The closed native profile has version `nav12.store-profile/1`, an absolute typed +`collectionsAt`, explicit collection declarations and disabled/strict references. +Each collection selects exactly its own direct array member, declares its exact +ID key, and orders reference fields with required/optional presence and nullable +policy. The strict `collection-id/1` marker has exactly two string members, +`collection` and `id`. There is no wire-profile parser; unsupported versions, +unknown configuration arguments and non-Boolean nullable values are compiler +refusals. Decoded-scalar JSONKey/text comparison remains the identity authority. + +Construction is one invocation using the original read scope and meter: + +1. Validate fixed finite limits; prepare the complete profile on the exclusive + profile-only capability, before source admission. +2. Admit the entire canonical root, source label/bytes and source map once. +3. Select the exact typed collections object. +4. Validate every physical collection header, then missing declarations. +5. Validate every record and ID; report the second duplicate and first ID path. +6. Validate every configured field's presence, null policy and marker shape. +7. Resolve all staged target names and IDs, preserving link order. +8. Check metadata, transfer the original document once and complete atomically. + +Each phase completes before the next begins. Source collections/rows use physical +insertion/index order; fields use configured order. Thus a later malformed marker +precedes an earlier dangling link. Operational errors and cancellation throw and +cannot publish a partial store. Cycles remain links between source-local records; +construction never follows them. Position tokens contain a private instance owner +and checked collection/row ordinals. They are neither semantic IDs nor persistent +identities. Empty collections, absent slots, null slots and links remain distinct. + +Source errors retain exact typed physical/requested paths. Missing-member spans +refer to the nearest existing parent; duplicates retain the first ID path. +Selection preserves the core access cause/location. Replacing a root does not +invent source bytes or spans. + +The twelve inclusive prototype caps are: 256 collections; 10,000 records per +collection and in total; 16 reference declarations per collection and 4,096 total; +100,000 configured slots (including absent/null) and 100,000 links; 4,096 UTF-8 +bytes per identity; 1,048,576 profile-text bytes; 16,777,216 ID/marker alias bytes; +128 path components; 33,554,432 logical overlay bytes. Zero is finite and accepted; +negative values and Int.max are invalid. The original 49 native caps still apply. + +Reservations precede growth: 512 fixed bytes, 128 per collection declaration, +64 per field/record/reference slot, 32 per text alias/staged link, text bytes, +8 per temporary header/row slot and the existing 32-byte typed path component +weight plus key text. Original native helpers charge scans, exact comparisons, +member lookup, path building and source spans to the same ledger. Marker text +reservations are released only after all targets resolve and staging is cleared; +work is never refunded. Path and scratch reservations conservatively remain in +the invocation's overlay accounting; the final overlay is not a precise live heap +measurement. Fixed immutable record access does not start a hidden operation. + +The caps are policy, not jointly achievable capacity guarantees. The S0 estimate +35,163,280 bytes already exceeds the overlay cap before paths/scratch. Linear +exact duplicate/target scans deliberately retain the correct bounded baseline; +large configurations can exhaust native work before reaching count maxima. +Logical retention differs from heap/RSS, and observer/caller work is not owned +instruction work. No timing, throughput or performance improvement is claimed. + +`Scripts/ci.py --lane store` requires twelve exact Swift Testing identities, +65 native vectors plus one strict-parser case, R01–R10, Debug/Release, ASan, +targeted TSan, strict memory compilation and the private consumer. Three remaining +frozen vectors use real compiler diagnostics in both configurations. Premise-only +Python observations and future F01–F03 follow cases are not engine successes. +`--lane store-preparation` retains D0 lifecycle/codegen gates. Full native/Linux +lanes retain the independent suites, all 49 cap witnesses and 126 maintenance IDs. +Receipts establish actual execution; this document does not certify a platform. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 5aaf8b5..9b47faa 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,9 +12,9 @@ "JSONCLITests": 88, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 88 }, - "parameterized_invocations": 325, + "parameterized_invocations": 390, "composition_external_functions": 5, "qa_functions": { "JSONTests": 82, @@ -35,7 +35,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 5 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -76,7 +76,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 88 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,9 +89,9 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 88 }, - "wasi_parameterized_invocations": 288, + "wasi_parameterized_invocations": 353, "portable_shared_state_functions": { "JSONTests": 4, "JSONSchemaTests": 3, @@ -103,7 +103,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 5 }, "export_functions": { "JSONSchemaTests": 1, @@ -120,5 +120,19 @@ "JSONNavigationTests.navigationPreparedD0OneShotAdmissionAndTransfer()", "JSONNavigationTests.navigationPreparedD0SourceTransferAndInvalidationAllExits()", "JSONNavigationTests.navigationPreparedD0IndependentConcurrentHealth()" + ], + "store_function_ids": [ + "JSONInspectionTests.storeD1NativeVector(id:)", + "JSONInspectionTests.storeD1StrictParserD037RejectsDecodedDuplicateMarker()", + "JSONInspectionTests.storeR01ExactPathsSpansOwnerAndReplacement()", + "JSONInspectionTests.storeR02PreparationPrecedesOneFullSourceAdmission()", + "JSONInspectionTests.storeR03AllInclusiveCapsCheckedArithmeticAndLateRefusal()", + "JSONInspectionTests.storeR04PresenceSlotsRetentionAndScaleCalibration()", + "JSONInspectionTests.storeR05InnerCancellationAcrossPhasesAndAfterTransfer()", + "JSONInspectionTests.storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth()", + "JSONInspectionTests.storeR07SourceAndStateInvalidatedOnEveryFailureAndSuccess()", + "JSONInspectionTests.storeR08DeclarationAndPhysicalPermutationMetamorphisms()", + "JSONInspectionTests.storeR09CompleteRowsDeepSourcesAndZeroCapacity()", + "JSONInspectionTests.storeR10NoInferenceOrModelMutationAndExactPayloads()" ] } diff --git a/Package.swift b/Package.swift index dcdd32c..2aca21f 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( // Opt-in N1 product candidate; promotion does not change core navigation semantics. .target(name: "JSONNavigation", dependencies: ["JSON"]), .target(name: "JSONInspection", dependencies: ["JSON", "JSONNavigation"]), - .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"], resources: [.copy("ProjectionFixtures")]), + .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"], resources: [.copy("ProjectionFixtures"), .copy("StoreFixtures")]), .testTarget(name: "JSONNavigationTests", dependencies: ["JSONNavigation", "JSON"], exclude: ["Fixtures"]), .target(name: "JSONBridges", dependencies: ["JSON"]), .testTarget(name: "JSONIntegrationTests", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch"]), diff --git a/Scripts/ci.py b/Scripts/ci.py index 9127b86..5c9c178 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -5,6 +5,7 @@ from archive_candidate import files from preflight import collect, collect_linux from testing_events import read_events, ROOT_PARAMETER_CASES +from store_acceptance import check_store_tests, check_store_consumer from projection_acceptance import check_projection_cases, check_public_projection_receipt from test_discovery import DISCOVERY, ROOT_COUNTS, SHARED_COUNTS, PARAMETER_INVOCATIONS, check_restored @@ -17,6 +18,7 @@ SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics' +SHARED_TESTS += '|storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -25,6 +27,17 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + if lane == 'store': + return [ + ['python3', '-B', 'Scripts/public_check.py'], + ['python3', '-B', 'Scripts/test_qualification_harness.py'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '--filter', 'store'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--filter', 'store'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/store-address', '--filter', 'store'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--sanitize', 'thread', '--scratch-path', '.build/store-thread', '--filter', 'storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth'], + ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/store-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], + ['python3', '-B', 'Scripts/qualify_store.py', '--output', str(output / 'private-consumer')], + ] if lane == 'store-preparation': return [ ['python3', '-B', 'Scripts/public_check.py'], @@ -38,13 +51,14 @@ def commands(lane, output): ] if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], - ['swift', 'test', '--filter', 'inspection|snapshot|projectionP'], - ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP'], - ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP'], + ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|store'], + ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP|store'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP|store'], ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/inspection-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], ['python3', '-B', 'Scripts/qualify_store_preparation.py', '--output', str(output / 'prepared-codegen')], + ['python3', '-B', 'Scripts/qualify_store.py', '--output', str(output / 'private-store-consumer')], ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')]] return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] @@ -187,6 +201,7 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], ['python3', '-B', 'Scripts/qualify_store_preparation.py', '--output', str(output / 'prepared-codegen')], + ['python3', '-B', 'Scripts/qualify_store.py', '--output', str(output / 'private-store-consumer')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -234,6 +249,7 @@ def check_result(command, code, log, events=None): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if events is not None: counts = events['bundle_counts'] + store_test = any(x == 'store' or x.startswith('storeR06') for x in command) prepared_test = any('navigationPreparedD0' in x for x in command) if command[:2] == ['swift', 'test']: expected = [DISCOVERY['composition_external_functions']] if 'Tools/CompositionQualification' in command else [12] if 'composition|composed|caughtOperationalFailureCannotComplete' in command else [79] if 'JSONSchemaTests' in command else [4 if 'thread' in command else 98] if 'JSONSchemaExamplesTests' in command else [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()) @@ -246,10 +262,10 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 - if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP']): + if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|store']): expected = [DISCOVERY['root_functions']['JSONInspectionTests']] if events is not None: - passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 and check_projection_cases(events) + passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameterized_functions'] == {'storeD1NativeVector(id:)': 65} and check_projection_cases(events) and check_store_tests(events, log) if prepared_test: expected_ids = set(DISCOVERY['prepared_entry_function_ids']) if 'navigationPreparedD0IndependentConcurrentHealth' in command: @@ -257,16 +273,20 @@ def check_result(command, code, log, events=None): expected = [len(expected_ids)] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': len(expected_ids)} and set(events['function_names']) == expected_ids and events['parameter_case_invocations'] == 0 + if store_test: + shared_store = any(x.startswith('storeR06') for x in command) + expected = [1 if shared_store else 12] + passed = passed and events is not None and check_store_tests(events, log, shared=shared_store, scoped=True) passed = passed and sorted(counts) == expected if 'Tools/CompositionQualification' in command and events is not None: passed = passed and events['module_function_counts'] == {'CompositionScaleTests': DISCOVERY['composition_external_functions']} and events['parameter_case_invocations'] == 0 if 'qa' in command and events is not None: passed = passed and events['module_function_counts'] == DISCOVERY['qa_functions'] and check_restored(events) - if 'thread' in command and 'JSONSchemaExamplesTests' not in command and not prepared_test and events is not None: - passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) + if 'thread' in command and 'JSONSchemaExamplesTests' not in command and not prepared_test and not store_test and events is not None: + passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) and check_store_tests(events, log, shared=True) if '--filter' not in command and '--test-product' not in command and not any(x.startswith('Tools/') for x in command): if events is not None: - passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) + passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) and check_store_tests(events, log) passed = passed and (events['parameterized_functions'] == ROOT_PARAMETER_CASES if events is not None else sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log)) == PARAMETER_INVOCATIONS) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) @@ -325,6 +345,9 @@ 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_store.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_store_consumer(data) if command[:3] == ['python3', '-B', 'Scripts/qualify_store_preparation.py']: data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) probes = [x for x in data['commands'] if x.get('expected') is not None] @@ -354,12 +377,12 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=18' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=19' in log.splitlines() return passed, counts def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['store-preparation', 'composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) + parser.add_argument('--lane', choices=['store', 'store-preparation', 'composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) diff --git a/Scripts/qualify_store.py b/Scripts/qualify_store.py new file mode 100644 index 0000000..950b137 --- /dev/null +++ b/Scripts/qualify_store.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Actual package-private store consumer and ordinary full-codegen refusals.""" +import argparse,hashlib,json,pathlib,re,subprocess +from archive_candidate import files +ROOT=pathlib.Path(__file__).resolve().parents[1] +CONSUMER=r'''import Foundation +import JSON +import JSONInspection +@main struct Main { + static func main() throws { + let doc=try JSONParser().parseDocument(Data(#"{"collections":{"users":[{"uuid":"é","friend":null},{"uuid":"e\u0301","friend":{"collection":"users","id":"é"}}]}}"#.utf8)) + let profile=JSONStoreProfile(version:.v1,collectionsAt:[.key("collections")],collections:[ + .init(name:"users",recordsAt:[.key("users")],idKey:"uuid",referenceFields:[.init(key:"friend",presence:.required,nullable:true)])],references:.strict(marker:.v1,validateTargets:true)) + let report=try JSONStoreBuilder(document:doc,profile:profile).build(),store=report.result + precondition(store.recordCount==2 && store.references.count==2) + if case .null=store.references[0].state {} else { preconditionFailure() } + if case .link(let target)=store.references[1].state { precondition(target.row==0); let record=try store.record(at:target); precondition(record.id=="é") } else { preconditionFailure() } + let other=try JSONStoreBuilder(document:doc,profile:profile).build().result + do { _=try other.record(at:store.record(collection:0,row:0).position); preconditionFailure() } + catch let e as JSONStoreFailure { precondition(e.code == .invalidPosition) } + precondition(store.document.sourceBytes==doc.sourceBytes) + precondition(report.statistics[.nodes]>0 && report.statistics[.workUnits]>0) + print("STORE_PRIVATE_CONSUMER passed cases=6 work=\(report.statistics[.workUnits]) retained=\(report.statistics[.retainedBytes])") + } +} +''' +PROBES={ + 'D051':('func illegal() { _ = JSONStoreProfileVersion.v2 }',"has no member 'v2'"), + 'D053':('func illegal() { _ = JSONStoreReferenceField(key:"f",presence:.required,nullable:"yes") }',"cannot convert value of type 'String' to expected argument type 'Bool'"), + 'D069':('func illegal() { _ = JSONStoreProfile(version:.v1,collectionsAt:[],collections:[],references:.disabled,guessID:true) }',"extra argument 'guessID' in call"), + 'store-initializer':('func illegal() { _ = JSONConfiguredStore() }','initializer is inaccessible'), + 'position-forge':('func illegal() { _ = JSONStorePosition() }','initializer is inaccessible'), + 'external-store':('func illegal(_ x: JSONStoreProfile) {}',"cannot find type 'JSONStoreProfile' in scope"), + 'bound-member':('import JSONNavigation\nfunc illegal(_ c: inout JSONTraversalContext) { _ = c.member }',"cannot reference 'mutating' method as function value"), + 'member-escape':('import JSONNavigation\nfunc illegal(_ c: inout JSONTraversalContext) -> () throws -> JSON? { { try c.member(.null,key:"x",at:[]) } }',"escaping closure captures 'inout' parameter"), +} +def main(): + ap=argparse.ArgumentParser();ap.add_argument('--output',required=True);args=ap.parse_args();out=ROOT/args.output;out.mkdir(parents=True,exist_ok=True) + receipt={'status':'running','source_inputs':[{'path':p.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()],'commands':[],'vector_compiler_results':[]} + def save(): (out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + def run(name,cmd,expected=None): + p=subprocess.run(cmd,cwd=ROOT,capture_output=True,text=True,timeout=900);data=p.stdout+p.stderr + crash=p.returncode<0 or any(x in data for x in ['Stack dump:','Assertion failed','Found ownership error','fatal error encountered during compilation']) + diagnostics=re.findall(r': error: ([^\n]+)',data) + passed=not crash and (p.returncode==0 if expected is None else p.returncode>0 and any(expected.lower() in d.lower() for d in diagnostics)) + log=out/(name+'.log');log.write_text(data) + receipt['commands'].append({'name':name,'command':cmd,'exit':p.returncode,'passed':passed,'compiler_crash':crash,'expected':expected,'primary_diagnostics':diagnostics,'log':str(log),'log_sha256':hashlib.sha256(log.read_bytes()).hexdigest()});save() + print(name,p.returncode,'PASS' if passed else 'FAIL',flush=True) + if not passed:raise ValueError(name) + return p.stdout + try: + identity=re.sub('[^a-zA-Z0-9_]','_',ROOT.name.lower()) + if identity[0].isdigit():identity='_'+identity + for config in ['debug','release']: + run(config+'-build',['swift','build','-c',config,'--target','JSONInspection','--jobs','2']) + bp=pathlib.Path(run(config+'-binpath',['swift','build','-c',config,'--show-bin-path']).strip());mods=bp/'Modules' if (bp/'Modules').exists() else bp + common=['swiftc','-swift-version','6','-warnings-as-errors','-I',str(mods),'-Onone' if config=='debug' else '-O','-parse-as-library'] + source=out/'consumer.swift';source.write_text(CONSUMER) + run(config+'-positive',common+['-package-name',identity,'-c',str(source),'-o',str(out/(config+'-positive.o'))]) + objs=[] + for module in ['JSON','JSONNavigation','JSONInspection']: + aggregate=bp/(module+'.o');objs += [str(aggregate)] if aggregate.exists() else [str(p) for p in sorted((bp/(module+'.build')).glob('*.o'))] + executable=out/('consumer-'+config) + run(config+'-link',common+['-package-name',identity,str(source),*objs,'-o',str(executable)]) + text=run(config+'-run',[str(executable)]) + if len(re.findall(r'^STORE_PRIVATE_CONSUMER passed cases=6 work=[1-9]\d* retained=[1-9]\d*$',text,re.M))!=1:raise ValueError('Missing actual consumer outcomes') + for name,(body,expected) in PROBES.items(): + source=out/(name+'.swift');source.write_text('import JSON\nimport JSONInspection\n'+body+'\n') + cmd=common+['-package-name','outside_consumer' if name.startswith('external') else identity,'-c',str(source),'-o',str(out/(config+'-'+name+'.o'))] + run(config+'-'+name,cmd,expected) + if name.startswith('D'):receipt['vector_compiler_results'].append({'id':name,'configuration':config,'driver':'profile-schema/compiler-negative','passed':True,'log':str(out/(config+'-'+name+'.log'))}) + receipt['status']='passed' + except (ValueError,OSError,subprocess.TimeoutExpired) as error:receipt.update(status='failed',error=str(error)) + save();return 0 if receipt['status']=='passed' else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/store_acceptance.py b/Scripts/store_acceptance.py new file mode 100644 index 0000000..e94d8e8 --- /dev/null +++ b/Scripts/store_acceptance.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Exact native store identities; premise checks and compiler cases stay separate.""" +import json +import pathlib +import re +from test_discovery import DISCOVERY + +ROOT = pathlib.Path(__file__).resolve().parents[1] +VECTORS = json.loads((ROOT / 'Tests/JSONInspectionTests/StoreFixtures/VECTORS.json').read_text())['cases'] +FUNCTIONS = frozenset(DISCOVERY['store_function_ids']) +SHARED = frozenset({'JSONInspectionTests.storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth()'}) + +def check_store_tests(events, log, shared=False, scoped=False): + expected = SHARED if shared else FUNCTIONS + names = [x for x in events['function_names'] if x.startswith('JSONInspectionTests.store')] + if len(names) != len(set(names)) or set(names) != expected: + return False + if scoped and (events['module_function_counts'] != {'JSONInspectionTests': len(expected)} or + events['parameterized_functions'] != ({} if shared else {'storeD1NativeVector(id:)': 65})): + return False + observed = re.findall(r'^STORE_VECTOR (D\d+) passed driver=(\S+) phase=(\S+) kind=(\S+)', log, re.M) + expected_vectors = [] if shared else [(x['id'], x['execution_kind'], str(x['expected']['phase']), x['expected']['kind']) + for x in VECTORS if x['execution_kind'] in ('native-build', 'strict-parser')] + runtime = re.findall(r'^STORE_RUNTIME (R\d+) passed$', log, re.M) + return sorted(observed) == sorted(expected_vectors) and sorted(runtime) == (['R06'] if shared else ['R%02d' % n for n in range(1,11)]) + +def check_store_consumer(data): + commands = data.get('commands', []) + if data.get('status') != 'passed' or len(commands) != 26 or len({c['name'] for c in commands}) != 26: + return False + for c in commands: + if not c.get('passed') or c.get('compiler_crash') is not False: + return False + expected = c.get('expected') + if expected: + if c.get('exit', 0) <= 0 or not any(expected.lower() in d.lower() for d in c.get('primary_diagnostics', [])): + return False + elif c.get('exit') != 0: + return False + results = data.get('vector_compiler_results', []) + expected = {(i, c) for i in ('D051','D053','D069') for c in ('debug','release')} + return len(results) == 6 and {(x['id'], x['configuration']) for x in results} == expected and all( + x.get('passed') and x.get('driver') == 'profile-schema/compiler-negative' for x in results) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 1f47be1..ab65656 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -12,9 +12,42 @@ from coverage import executable_objects, shared_objects, validate_export, without_schema_objects from test_discovery import DISCOVERY, ROOT_COUNTS, PARAMETER_INVOCATIONS, check_root, check_restored, RESTORED_FUNCTIONS from ci import check_result, commands +from store_acceptance import FUNCTIONS as STORE_FUNCTIONS, VECTORS, check_store_tests, check_store_consumer from projection_acceptance import EXPECTED as PROJECTION_FUNCTIONS, PUBLIC_CHECKS, check_projection_cases, check_public_projection_receipt + +def store_markers(): + return '\n'.join(['STORE_VECTOR %s passed driver=%s phase=%s kind=%s' % + (x['id'],x['execution_kind'],x['expected']['phase'],x['expected']['kind']) + for x in VECTORS if x['execution_kind'] in ('native-build','strict-parser')]+[ + 'STORE_RUNTIME R%02d passed' % n for n in range(1,11)]) + class HarnessTests(unittest.TestCase): + def test_store_requires_exact_engine_vectors_runtime_and_compiler_primary_diagnostics(self): + events = dict(function_names=sorted(STORE_FUNCTIONS), module_function_counts={'JSONInspectionTests':12}, + parameterized_functions={'storeD1NativeVector(id:)':65}) + log = store_markers() + self.assertTrue(check_store_tests(events, log, scoped=True)) + for broken in [log.replace('D001','D999'), log+'\n'+log.splitlines()[0], + log.replace('driver=native-build','driver=python-premise'), log.replace('R10 passed','R10 skipped')]: + self.assertFalse(check_store_tests(events, broken, scoped=True)) + self.assertFalse(check_store_tests(dict(events,function_names=sorted(STORE_FUNCTIONS)[:-1]),log)) + self.assertFalse(check_store_tests(dict(events,parameterized_functions={'storeD1NativeVector(id:)':64}),log,scoped=True)) + valid = dict(status='passed',commands=[dict(name=str(i),passed=True,compiler_crash=False, + expected='inaccessible' if i<16 else None,exit=1 if i<16 else 0, + primary_diagnostics=['inaccessible'] if i<16 else []) for i in range(26)], + vector_compiler_results=[dict(id=i,configuration=c,driver='profile-schema/compiler-negative',passed=True) + for i in ['D051','D053','D069'] for c in ['debug','release']]) + self.assertTrue(check_store_consumer(valid)) + for mode in ['missing','incidental','crash','zero','vector']: + bad=copy.deepcopy(valid) + if mode=='missing': bad['commands'].pop() + elif mode=='incidental': bad['commands'][0]['primary_diagnostics']=['unrelated error'] + elif mode=='crash': bad['commands'][0]['compiler_crash']=True + elif mode=='zero': bad['commands'][0]['exit']=0 + else: bad['vector_compiler_results'].pop() + self.assertFalse(check_store_consumer(bad)) + def test_public_projection_receipt_rejects_incomplete_and_incidental_successes(self): valid = dict(status='passed', commands=[dict(name=str(i), status='passed', compiler_crash=False, expected_diagnostic='inaccessible' if i < 18 else None, @@ -74,12 +107,15 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel # Combined-source checks must retain both independently owned optional products. for module, count, command in [ ('JSONNavigationTests', 57, ['swift', 'test', '--filter', 'navigation']), - ('JSONInspectionTests', 76, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP']), + ('JSONInspectionTests', 88, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|store']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, - parameter_case_invocations=0, function_names=sorted(PROJECTION_FUNCTIONS) if module == 'JSONInspectionTests' else []) - self.assertTrue(check_result(command, 0, '', events)[0]) + parameter_case_invocations=0, function_names=sorted(PROJECTION_FUNCTIONS | STORE_FUNCTIONS) if module == 'JSONInspectionTests' else [], + parameterized_functions={'storeD1NativeVector(id:)': 65} if module == 'JSONInspectionTests' else {}) + if module == 'JSONInspectionTests': events['parameter_case_invocations'] = 65 + evidence = store_markers() if module == 'JSONInspectionTests' else '' + self.assertTrue(check_result(command, 0, evidence, events)[0]) for broken in [dict(events, bundle_counts=[count-1]), dict(events, module_function_counts={'WrongModule': count}), dict(events, parameter_case_invocations=1)]: @@ -88,12 +124,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 57) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONInspectionTests'], 76) + self.assertEqual(policy[key]['JSONInspectionTests'], 88) self.assertEqual(policy[key]['JSONIntegrationTests'], 32) for key in ('shared_state_functions', 'portable_shared_state_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 2) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 4) - self.assertEqual(policy[key]['JSONInspectionTests'], 4) + self.assertEqual(policy[key]['JSONInspectionTests'], 5) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): @@ -199,7 +235,7 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): identity['function_names'][0] = 'JSONTests.unrelatedReplacement()' self.assertFalse(check_restored(identity)) harness = ['python3', '-B', 'Scripts/test_qualification_harness.py'] - self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=18')[0]) + self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=19')[0]) for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=16', '']: self.assertFalse(check_result(harness, 0, stale)[0]) lifetime = ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py'] diff --git a/Scripts/testing_events.py b/Scripts/testing_events.py index 0baadd8..e552a99 100644 --- a/Scripts/testing_events.py +++ b/Scripts/testing_events.py @@ -4,6 +4,7 @@ import json ROOT_PARAMETER_CASES = { + 'storeD1NativeVector(id:)': 65, 'structuredCanonicalBase64MatchesPadBitGold(_:)': 16, 'structuredInlineMatchesIndependentDialectCases(_:)': 32, 'structuredMiniMathMatchesIndependentGold(_:)': 45, diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 2863bf4..d3565a5 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:10f4665817c36cabc08fd7b1e98c64076851e175e0c332f4abeb313d89c4e609" + package static let revision = "sha256:40669ae92e9827800f3faa15d90e9753ccc8000d20f83db0ae4d608793b7cb49" package static let kind = "source-sha256" } diff --git a/Sources/JSONInspection/Store/JSONConfiguredStore.swift b/Sources/JSONInspection/Store/JSONConfiguredStore.swift new file mode 100644 index 0000000..604badc --- /dev/null +++ b/Sources/JSONInspection/Store/JSONConfiguredStore.swift @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +private final class JSONStoreOwner: Sendable {} +package struct JSONStorePosition: Sendable { + fileprivate let owner: JSONStoreOwner + package let collection: Int + package let row: Int +} +package enum JSONStoreReferenceState: Sendable { + case absent + case null + case link(JSONStorePosition) +} +package struct JSONStoreRecordMetadata: Sendable { + package let position: JSONStorePosition + package let id: String + package let path: [JSONLocationComponent] + package let idPath: [JSONLocationComponent] + package let referenceRange: Range +} +package struct JSONStoreReferenceMetadata: Sendable { + package let field: Int + package let path: [JSONLocationComponent] + package let state: JSONStoreReferenceState +} +internal struct StoreCollectionMetadata: Sendable { + let profile: Int + let path: [JSONLocationComponent] + let start: Int + let count: Int +} +/// Package-private immutable complete view. No independent resolver or follow API. +package struct JSONConfiguredStore: Sendable { + package let document: JSONDocument + package let profile: JSONStoreProfile + private let owner: JSONStoreOwner + private let collections: [StoreCollectionMetadata] + private let records: [JSONStoreRecordMetadata] + package let references: [JSONStoreReferenceMetadata] + package let overlayLogicalBytes: UInt64 + package let identityAliasBytes: UInt64 + package let profileTextBytes: UInt64 + package var collectionCount: Int { collections.count } + package var recordCount: Int { records.count } + package func recordCount(in collection: Int) throws -> Int { + guard collection >= 0, collection < collections.count else { + throw JSONStoreFailure(.invalidPosition, phase: 0, domain: .operation) + } + return collections[collection].count + } + fileprivate init(document: JSONDocument, profile: JSONStoreProfile, owner: JSONStoreOwner, + collections: [StoreCollectionMetadata], records: [JSONStoreRecordMetadata], + references: [JSONStoreReferenceMetadata], overlay: UInt64, aliases: UInt64, + profileText: UInt64) { + self.document = document; self.profile = profile; self.owner = owner + self.collections = collections; self.records = records; self.references = references + overlayLogicalBytes = overlay; identityAliasBytes = aliases + profileTextBytes = profileText + } + /// Checked fixed metadata access; positions are bound to this exact owner instance. + package func record(collection: Int, row: Int) throws -> JSONStoreRecordMetadata { + guard collection >= 0, collection < collections.count, row >= 0, + row < collections[collection].count else { + throw JSONStoreFailure(.invalidPosition, phase: 0, domain: .operation) + } + return records[collections[collection].start + row] + } + package func record(at position: JSONStorePosition) throws -> JSONStoreRecordMetadata { + guard position.owner === owner else { + throw JSONStoreFailure(.invalidPosition, phase: 0, domain: .operation) + } + return try record(collection: position.collection, row: position.row) + } +} + +package struct JSONStoreBuilder: Sendable { + package let document: JSONDocument + package let profile: JSONStoreProfile + package let limits: JSONLimits + package let caps: JSONStoreCaps + package var checkpointObserver: (@Sendable (UInt64) -> Void)? + package var lifetimeObserver: (@Sendable (Bool, Bool) -> Void)? + /// Qualification-only phase rendezvous; arbitrary observer work is not library work. + package var phaseObserver: (@Sendable (Int) -> Void)? + package init(document: JSONDocument, profile: JSONStoreProfile, + limits: JSONLimits = .balanced, caps: JSONStoreCaps = .init()) { + self.document = document; self.profile = profile; self.limits = limits; self.caps = caps + } + package func build() throws -> JSONOperationReport { + try Task.checkCancellation() + try limits.validate() + try caps.validate() + var reducer = StoreReducer(profile: profile, accounting: .init(caps: caps)) + reducer.phaseObserver = phaseObserver + var traversal = JSONTraversal(document: document, limits: limits) + traversal.checkpointObserver = checkpointObserver + traversal.lifetimeObserver = lifetimeObserver + do { + return try traversal.inspectPreparedOwned(scope: .subtree(at: profile.collectionsAt), + options: .init(depthLimit: 0), reducer: &reducer) + } catch let error as JSONAccessFailure { + // Selection happens before the reducer: preserve the real core cause/path/location. + throw JSONStoreFailure(error.cause.category == .missingValue ? .selectionMissing : .selectionWrongKind, + phase: 3, domain: .source, path: error.path, coreCause: error.cause) + } + } +} + +private struct StorePendingReference { + let metadata: Int + let collection: String + let id: String + let aliasBytes: UInt64 +} +private struct StoreReducer: JSONReadPreparation, JSONTraversalOwnedReducer { + let profile: JSONStoreProfile + var accounting: JSONStoreAccounting + private var owner: JSONStoreOwner? + private var collections: [StoreCollectionMetadata] = [] + private var records: [JSONStoreRecordMetadata] = [] + private var references: [JSONStoreReferenceMetadata] = [] + private var pending: [StorePendingReference] = [] + private var observed = false + private var stagedLinks = 0 + private var resolvedLinks = 0 + var phaseObserver: (@Sendable (Int) -> Void)? + init(profile: JSONStoreProfile, accounting: JSONStoreAccounting) { + self.profile = profile; self.accounting = accounting + } + mutating func prepare(context: inout JSONReadPreparationContext) throws { + phaseObserver?(1) + try profile.prepare(accounting: &accounting, context: &context) + owner = JSONStoreOwner() // fixed reservation was admitted before allocation + phaseObserver?(2) + } + mutating func refuse(_ code: JSONStoreFailureCode, phase: Int, at path: [JSONLocationComponent], + missing: Bool = false, first: [JSONLocationComponent]? = nil, + context: inout JSONTraversalContext) throws -> Never { + var anchor = path + if missing { + anchor = [] + for component in path.dropLast() { + anchor = try accounting.path(component, after: anchor, context: &context) + } + } + let span = try context.sourceSpan(at: anchor) + throw JSONStoreFailure(code, phase: phase, domain: .source, path: path, + firstPath: first, sourceSpan: span) + } + mutating func path(_ component: JSONLocationComponent, after prefix: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> [JSONLocationComponent] { + try accounting.path(component, after: prefix, context: &context) + } + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work() + guard !observed, hit.relativeDepth == 0 else { throw JSONFailure(.configuration, code: .invalidOperation) } + observed = true + phaseObserver?(3) + guard case .object(let object) = value else { + try refuse(.collectionsNotObject, phase: 3, at: hit.absoluteLocation, context: &context) + } + try headers(object, root: hit.absoluteLocation, context: &context) + try identifiers(object, context: &context) + try markers(object, context: &context) + try targets(context: &context) + return .skipDescendants + } + mutating func headers(_ object: JSONObject, root: [JSONLocationComponent], + context: inout JSONTraversalContext) throws { + accounting.phase = 4 + phaseObserver?(4) + try accounting.reserve(try JSONStoreAccounting.product(UInt64(profile.collections.count) + 1, 8), context: &context) + var seen = Array(repeating: false, count: profile.collections.count) + var total = 0 + for index in 0.. JSONConfiguredStore { + accounting.phase = 8 + phaseObserver?(8) + try context.work() + guard let owner, observed, pending.isEmpty, stagedLinks == resolvedLinks, + summary.visitedCount == 1, summary.emittedCount == 1 else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + for record in records { + try context.work() + guard record.referenceRange.lowerBound >= 0, record.referenceRange.upperBound <= references.count else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + } + for reference in references { + try context.work() + if case .link(let target) = reference.state { + guard target.owner === owner, target.collection >= 0, target.collection < collections.count, + target.row >= 0, target.row < collections[target.collection].count else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + } + } + try context.checkpoint() + let source = try context.transferDocumentOwnership() + phaseObserver?(8) // completion checkpoint after the one-shot transfer + let result = JSONConfiguredStore(document: source, profile: profile, owner: owner, + collections: collections, records: records, references: references, + overlay: accounting.overlay, aliases: accounting.aliases, profileText: accounting.profileText) + collections = []; records = []; references = [] + return result + } +} diff --git a/Sources/JSONInspection/Store/JSONStoreAccounting.swift b/Sources/JSONInspection/Store/JSONStoreAccounting.swift new file mode 100644 index 0000000..5018c50 --- /dev/null +++ b/Sources/JSONInspection/Store/JSONStoreAccounting.swift @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Additional capacity bookkeeping, never a work meter or independent health. +internal struct JSONStoreAccounting { + let caps: JSONStoreCaps + var overlay: UInt64 = 0 + var profileText: UInt64 = 0 + var aliases: UInt64 = 0 + var phase = 1 + static func sum(_ lhs: UInt64, _ rhs: UInt64) throws -> UInt64 { + let (next, overflow) = lhs.addingReportingOverflow(rhs) + guard !overflow else { throw JSONStoreFailure(.counterOverflow, phase: 0, domain: .operation) } + return next + } + static func product(_ lhs: UInt64, _ rhs: UInt64) throws -> UInt64 { + let (next, overflow) = lhs.multipliedReportingOverflow(by: rhs) + guard !overflow else { throw JSONStoreFailure(.counterOverflow, phase: 0, domain: .operation) } + return next + } + static func check(_ count: Int, _ limit: Int, name: String, phase: Int, + path: [JSONLocationComponent] = []) throws { + guard count <= limit else { + throw JSONStoreFailure(.capacityExceeded, phase: phase, domain: .operation, + path: path, capacity: name, limit: limit) + } + } + mutating func track(_ bytes: UInt64) throws { + let next = try Self.sum(overlay, bytes) + guard next <= UInt64(caps.overlayBytes) else { + throw JSONStoreFailure(.capacityExceeded, phase: phase, domain: .operation, + capacity: "overlayBytes", limit: caps.overlayBytes) + } + overlay = next + } + mutating func reserve(_ bytes: UInt64, context: inout JSONReadPreparationContext) throws { + try track(bytes); try context.retain(bytes) + } + mutating func reserve(_ bytes: UInt64, context: inout JSONTraversalContext) throws { + try track(bytes); try context.retain(bytes) + } + mutating func profile(_ text: String, key: Bool, identity: Bool = false, + context: inout JSONReadPreparationContext) throws { + var size: UInt64 = 0 + for _ in text.utf8 { + try context.work(); size = try Self.sum(size, 1) + let next = try Self.sum(profileText, 1) + guard next <= UInt64(caps.profileTextBytes), + !identity || size <= UInt64(caps.identityBytes) else { + throw JSONStoreFailure(.capacityExceeded, phase: 1, domain: .operation, + capacity: identity && size > UInt64(caps.identityBytes) ? "identityBytes" : "profileTextBytes", + limit: identity && size > UInt64(caps.identityBytes) ? caps.identityBytes : caps.profileTextBytes) + } + profileText = next + } + // Native admission owns the text reservation. Track, do not reserve twice. + try track(size); try context.admitText(text, key: key) + } + mutating func alias(_ text: String, phase: Int, path: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> UInt64 { + try reserve(32, context: &context) + var size: UInt64 = 0 + for _ in text.utf8 { + try context.work(); size = try Self.sum(size, 1) + let next = try Self.sum(aliases, 1) + guard size <= UInt64(caps.identityBytes), next <= UInt64(caps.aliasBytes) else { + throw JSONStoreFailure(.capacityExceeded, phase: phase, domain: .operation, path: path, + capacity: size > UInt64(caps.identityBytes) ? "identityBytes" : "aliasBytes", + limit: size > UInt64(caps.identityBytes) ? caps.identityBytes : caps.aliasBytes) + } + aliases = next; try reserve(1, context: &context) + } + return size + } + mutating func releaseAliases(_ textBytes: UInt64, count: UInt64, + context: inout JSONTraversalContext) throws { + let bytes = try Self.sum(textBytes, Self.product(count, 32)) + guard aliases >= textBytes, overlay >= bytes else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try context.release(bytes); aliases -= textBytes; overlay -= bytes + } + mutating func path(_ component: JSONLocationComponent, after prefix: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> [JSONLocationComponent] { + let count = try Self.sum(UInt64(prefix.count), 1) + guard count <= UInt64(caps.pathComponents) else { + throw JSONStoreFailure(.capacityExceeded, phase: phase, domain: .operation, + capacity: "pathComponents", limit: caps.pathComponents) + } + var bytes = try Self.product(count, JSONLogicalWeights.indexSlot) + for part in prefix { + try context.work() + if case .key(let key) = part { + for _ in key.string.utf8 { try context.work(); bytes = try Self.sum(bytes, 1) } + } + } + if case .key(let key) = component { + for _ in key.string.utf8 { try context.work(); bytes = try Self.sum(bytes, 1) } + } + try track(bytes) + return try context.appending(component, to: prefix) + } +} diff --git a/Sources/JSONInspection/Store/JSONStorePreparation.swift b/Sources/JSONInspection/Store/JSONStorePreparation.swift new file mode 100644 index 0000000..9ed4df0 --- /dev/null +++ b/Sources/JSONInspection/Store/JSONStorePreparation.swift @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +internal extension JSONStoreProfile { + func prepare(accounting a: inout JSONStoreAccounting, + context: inout JSONReadPreparationContext) throws { + try a.reserve(512, context: &context) + try a.profile(version.rawValue, key: false, context: &context) + try JSONStoreAccounting.check(collectionsAt.count, a.caps.pathComponents, + name: "pathComponents", phase: 1) + for (index, part) in collectionsAt.enumerated() { + try context.work(); try a.reserve(JSONLogicalWeights.indexSlot, context: &context) + switch part { + case .key(let key): try a.profile(key.string, key: true, context: &context) + case .index(let indexValue): + guard indexValue >= 0 else { + throw JSONStoreFailure(.invalidTypedComponent, phase: 1, domain: .profile, + path: [.key("collectionsAt"), .index(index), .key("index")]) + } + } + } + switch references { + case .disabled: try a.profile("disabled", key: false, context: &context) + case .strict(let marker, let validateTargets): + try a.profile("strict", key: false, context: &context) + try a.profile(marker.rawValue, key: false, context: &context) + guard validateTargets else { + throw JSONStoreFailure(.invalidReferenceMode, phase: 1, domain: .profile, + path: [.key("references"), .key("validateTargets")]) + } + try a.profile("collection", key: true, context: &context) + try a.profile("id", key: true, context: &context) + } + var fields = 0 + for (i, collection) in collections.enumerated() { + try context.work() + try JSONStoreAccounting.check(i + 1, a.caps.collections, name: "collections", phase: 1) + try a.reserve(128, context: &context) + try a.profile(collection.name.string, key: true, identity: true, context: &context) + for prior in 0..= 0, value < Int.max else { throw JSONFailure(.configuration, code: .invalidLimits) } + } + } +} + +package enum JSONStoreFailureCode: String, Sendable { + case invalidTypedComponent, duplicateCollectionDeclaration, invalidRecordsAt + case duplicateReferenceField, idReferenceFieldCollision, disabledReferencesConflict + case invalidReferenceMode, collectionsNotObject, unconfiguredCollection, collectionNotArray + case missingConfiguredCollection, recordWrongKind, recordIDMissing, recordIDNull, recordIDWrongKind + case duplicateRecordID, referenceMissing, referenceNull, referenceMarkerWrongKind + case referenceMarkerMissing, referenceMarkerMemberWrongKind, referenceMarkerExtra + case referenceUnknownCollection, referenceDanglingID, selectionMissing, selectionWrongKind + case capacityExceeded, counterOverflow, invalidPosition +} +package enum JSONStoreFailureDomain: String, Sendable { case profile, source, operation } +/// Source path and core cause are separate; missing targets have no invented span. +package struct JSONStoreFailure: Error, Sendable { + package let code: JSONStoreFailureCode + package let phase: Int + package let domain: JSONStoreFailureDomain + package let path: [JSONLocationComponent] + package let firstPath: [JSONLocationComponent]? + package let sourceSpan: JSONSourceSpan? + package let coreCause: JSONFailure? + package let capacity: String? + package let limit: Int? + internal init(_ code: JSONStoreFailureCode, phase: Int, domain: JSONStoreFailureDomain, + path: [JSONLocationComponent] = [], firstPath: [JSONLocationComponent]? = nil, + sourceSpan: JSONSourceSpan? = nil, coreCause: JSONFailure? = nil, + capacity: String? = nil, limit: Int? = nil) { + self.code = code; self.phase = phase; self.domain = domain; self.path = path + self.firstPath = firstPath; self.sourceSpan = sourceSpan; self.coreCause = coreCause + self.capacity = capacity; self.limit = limit + } +} diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift index a08e58e..9fe1049 100644 --- a/Sources/JSONNavigation/JSONTraversalContext.swift +++ b/Sources/JSONNavigation/JSONTraversalContext.swift @@ -34,6 +34,20 @@ package struct JSONTraversalContext: ~Copyable { package mutating func equalKeys(_ lhs: JSONKey, _ rhs: JSONKey) throws -> Bool { try perform { try JSONComparison.textEquals(lhs.string, rhs.string, meter: &$0.meter) } } + /// Scalar-exact comparison charged to the original admitted scope. + package mutating func equalStrings(_ lhs: String, _ rhs: String) throws -> Bool { + try perform { try JSONComparison.textEquals(lhs, rhs, meter: &$0.meter) } + } + /// Ephemeral canonical member in the owned reducer. The requested key was admitted + /// by preparation; any retained string/output alias needs its own reservation. + package mutating func member(_ value: JSON, key: JSONKey, + at path: [JSONLocationComponent]) throws -> JSON? { + try perform(at: path) { state in + try state.meter.work() + guard case .object(let object) = value else { throw JSONFailure(.typeMismatch, code: .wrongKind) } + return try object.lookup(key.string, meter: &state.meter) + } + } package mutating func arrayCount(_ value: JSON, at path: [JSONLocationComponent]) throws -> Int { try perform(at: path) { state in try state.meter.work() diff --git a/Tests/JSONInspectionTests/StoreFixtures/PROVENANCE.json b/Tests/JSONInspectionTests/StoreFixtures/PROVENANCE.json new file mode 100644 index 0000000..46a97a0 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/PROVENANCE.json @@ -0,0 +1,10 @@ +{ + "source": "Owner-supplied NAV12 S0 design corpus", + "s0_zip_sha256": "2510aaa42b66505e568c5d61dca73a6bb4bf7050bfc697f2d5a334082f49de17", + "manifest_sha256": "82189b8349afdcbe79e71d23245a892cfcbced5e7535e9f49ee6a7a8190ce038", + "cases": 69, + "native_build_cases": 65, + "parser_cases": 1, + "compiler_cases": 3, + "policy": "Independent expected data, not evaluated successes or production JSON store wire format" +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/VECTORS.json b/Tests/JSONInspectionTests/StoreFixtures/VECTORS.json new file mode 100644 index 0000000..2825b44 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/VECTORS.json @@ -0,0 +1,5399 @@ +{ + "version": 1, + "status": "Declarative D1 expectations; premise checks are NOT Swift conformance", + "cases": [ + { + "id": "D001", + "name": "empty", + "file": "cases/D001-empty.json", + "sha256": "08e6ca68120665df8617898e6a58f4f299b380a51adec2afc5ca6330ba748bd0", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "collectionCount": 0, + "recordCount": 0, + "referenceSlots": 0, + "links": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + } + ], + "kind": "object" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D002", + "name": "one", + "file": "cases/D002-one.json", + "sha256": "861f2a13b0bb6d8ae5e480cd1291dec040b7259d4eede956d6f394934e795794", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 1, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "u1" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D003", + "name": "same-id-cross-collections", + "file": "cases/D003-same-id-cross-collections.json", + "sha256": "0dfe6bfbb850954e8ab6ed96c290383f06572655a36baf7c826645de41adabda", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + }, + { + "name": "posts", + "recordsAt": [ + { + "key": "posts" + } + ], + "idKey": "pid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 2, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "u1" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "posts" + }, + { + "index": 0 + }, + { + "key": "pid" + } + ], + "value": "u1" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D004", + "name": "duplicate-id", + "file": "cases/D004-duplicate-id.json", + "sha256": "65ee7f7f145d83efb1ad78ca68fb02e9ebc4d366a13bc716eb7443a4331fc000", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "duplicateRecordID", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ], + "firstAt": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "u1" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ], + "value": "u1" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D005", + "name": "missing-id", + "file": "cases/D005-missing-id.json", + "sha256": "4a3150a2bf1b5e074958e45049130002a835a1776203195189b3bfb2ef390884", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D006", + "name": "null-id", + "file": "cases/D006-null-id.json", + "sha256": "6e7cbd87dcc20c272e6a6f4782c3d3e668c78a6adec1cc039969f642fd0ac677", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDNull", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D007", + "name": "id-number", + "file": "cases/D007-id-number.json", + "sha256": "b4cd489627602ffbbfd4a51b63e8c338997078da7c5324145f1c6d958c96f1d1", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "number" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D008", + "name": "id-boolean", + "file": "cases/D008-id-boolean.json", + "sha256": "26c5827bfa8ec84f7833bca17aac787788df1b3ad78673bfdce125298fa65645", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "boolean" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D009", + "name": "id-array", + "file": "cases/D009-id-array.json", + "sha256": "7beb327faa3c6ade20ac44542b1b26a9aa4195c9f46a86141c882d6448ac2194", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D010", + "name": "id-object", + "file": "cases/D010-id-object.json", + "sha256": "c695856e014c021fbfc3a5470d2be7e6aec6d1c003888a9db0413c36d6950300", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "object" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D011", + "name": "scalar-record", + "file": "cases/D011-scalar-record.json", + "sha256": "d083d4a17645009409d996ab309cc30a2241d38e8689ea25ad848805add802ac", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + } + ], + "kind": "string" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D012", + "name": "null-collection", + "file": "cases/D012-null-collection.json", + "sha256": "22d8e61258c38c870ac954fc93991246d4fa308eaa24189839edd9d8456b61ff", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 4, + "kind": "collectionNotArray", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D013", + "name": "container-array", + "file": "cases/D013-container-array.json", + "sha256": "ef8d0a99763e822fe6036ec48ec94665bbc14bfbd2191a0ea2738339848c8669", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 3, + "kind": "collectionsNotObject", + "at": [ + { + "key": "collections" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D014", + "name": "missing-container", + "file": "cases/D014-missing-container.json", + "sha256": "3554e4987cd73891fe8cc2822b44cc9c2764393218e8463065176690d51c996e", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 3, + "kind": "selectionMissing", + "at": [ + { + "key": "collections" + } + ], + "coreCause": "missingMember", + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D015", + "name": "wrong-ancestor", + "file": "cases/D015-wrong-ancestor.json", + "sha256": "fba16fc16d67a625a464d059d0161bfac9bed0c9ad3c6b310835c27ed9b8bd43", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "outer" + }, + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 3, + "kind": "selectionWrongKind", + "at": [ + { + "key": "outer" + }, + { + "key": "collections" + } + ], + "coreCause": "wrongKind", + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "outer" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D016", + "name": "scalar-distinct-collections", + "file": "cases/D016-scalar-distinct-collections.json", + "sha256": "83ce5acac1507bb229b28682d5dfa38bf6c706f3348a35cd8fb822066ee7479e", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "é", + "recordsAt": [ + { + "key": "é" + } + ], + "idKey": "uuid", + "referenceFields": [] + }, + { + "name": "é", + "recordsAt": [ + { + "key": "é" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "collectionCount": 2, + "domain": "source" + }, + "premises": [ + { + "assert": "exactKeys", + "path": [ + { + "key": "collections" + } + ], + "keys": [ + "é", + "é" + ] + } + ], + "execution_kind": "native-build" + }, + { + "id": "D017", + "name": "scalar-distinct-ids", + "file": "cases/D017-scalar-distinct-ids.json", + "sha256": "fb3a0d0da5fed99401b7f07057e05b41ff7c8051870705daae28868d42bcb196", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 2, + "domain": "source" + }, + "premises": [ + { + "assert": "scalarDifferent", + "domain": "source", + "left": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "right": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ] + } + ], + "execution_kind": "native-build" + }, + { + "id": "D018", + "name": "equal-payload-two-records", + "file": "cases/D018-equal-payload-two-records.json", + "sha256": "1100172eae0a9b0ef780afc3ea4e0eecb18886efb1fd3d140b692f3794a7480a", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 2, + "domain": "source" + }, + "premises": [ + { + "assert": "scalarDifferent", + "domain": "source", + "left": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "right": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ] + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "payload" + } + ], + "value": { + "x": 1 + } + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "payload" + } + ], + "value": { + "x": 1 + } + } + ], + "execution_kind": "native-build" + }, + { + "id": "D019", + "name": "configured-id-only", + "file": "cases/D019-configured-id-only.json", + "sha256": "fbd38b0652f6229a198f30f1ce209b69ba682f5e988bcb163b729e4daa121f55", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordIDMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "missing" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "id" + } + ], + "value": "a" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "_id" + } + ], + "value": "b" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D020", + "name": "unconfigured-ref-inert", + "file": "cases/D020-unconfigured-ref-inert.json", + "sha256": "b3ed3c8073897b25e4f19fd0f25ff49f91f417b08a1bb933c6aa4b538ed717a6", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "referenceSlots": 0, + "links": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "$ref" + }, + { + "key": "collection" + } + ], + "value": "bad" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D021", + "name": "disabled-with-fields", + "file": "cases/D021-disabled-with-fields.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "disabledReferencesConflict", + "at": [ + { + "key": "references" + }, + { + "key": "mode" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "references" + }, + { + "key": "mode" + } + ], + "value": "disabled" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D022", + "name": "forward-target", + "file": "cases/D022-forward-target.json", + "sha256": "aef1e26ae78d45d5af213d632179afec471f1a7eb06d75c7c47c76c654fec3f8", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "links": 2, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "b" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ], + "value": "b" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D023", + "name": "unknown-collection", + "file": "cases/D023-unknown-collection.json", + "sha256": "d0657caf40a9f5020f3351fdbcfadbbeb25990c11ac1bd83e8fdbf27dd07d4c2", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 7, + "kind": "referenceUnknownCollection", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "value": "posts" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D024", + "name": "dangling-id", + "file": "cases/D024-dangling-id.json", + "sha256": "104224445baed4327b782141e5a5144dfc7c79ed7efc2cddb8d4f829355d785d", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 7, + "kind": "referenceDanglingID", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "bad" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "a" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D025", + "name": "required-absent", + "file": "cases/D025-required-absent.json", + "sha256": "3b90aa42daef3f06894c1d3e75cf7306be0dba9a080ef6d3f3dea4f3d9ab2613", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D026", + "name": "optional-absent", + "file": "cases/D026-optional-absent.json", + "sha256": "3b90aa42daef3f06894c1d3e75cf7306be0dba9a080ef6d3f3dea4f3d9ab2613", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "optional", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "states": [ + "absent" + ], + "referenceSlots": 1, + "links": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D027", + "name": "required-null-False", + "file": "cases/D027-required-null-False.json", + "sha256": "585bc3a98b4f4e74f01574cfd860d0bb3c37612f795cf30aad39191596ee22b2", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceNull", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D028", + "name": "required-null-True", + "file": "cases/D028-required-null-True.json", + "sha256": "585bc3a98b4f4e74f01574cfd860d0bb3c37612f795cf30aad39191596ee22b2", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": true + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "states": [ + "null" + ], + "referenceSlots": 1, + "links": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D029", + "name": "optional-null-False", + "file": "cases/D029-optional-null-False.json", + "sha256": "585bc3a98b4f4e74f01574cfd860d0bb3c37612f795cf30aad39191596ee22b2", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "optional", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceNull", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D030", + "name": "optional-null-True", + "file": "cases/D030-optional-null-True.json", + "sha256": "585bc3a98b4f4e74f01574cfd860d0bb3c37612f795cf30aad39191596ee22b2", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "optional", + "nullable": true + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "states": [ + "null" + ], + "referenceSlots": 1, + "links": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D031", + "name": "marker-string", + "file": "cases/D031-marker-string.json", + "sha256": "5dccc805daed6aec2c02675c10a9f037e955e7ad49175d1cc28088a03557771b", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "string" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D032", + "name": "marker-id-missing", + "file": "cases/D032-marker-id-missing.json", + "sha256": "2a54f01a3d1ec197df8bbcae30f14eda192332ee094e2b361a5dbaaf031d40f4", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D033", + "name": "marker-id-null", + "file": "cases/D033-marker-id-null.json", + "sha256": "c81d5128eaf06d55de6babcea7d1dd405f2c6f8bf1247132869beb1a7850764b", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMemberWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D034", + "name": "marker-extra", + "file": "cases/D034-marker-extra.json", + "sha256": "5f91412213ef43381bfe08804fb601863938239c65a6def15a8059fc0c6e6276", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerExtra", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "other" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "other" + } + ], + "value": 1 + } + ], + "execution_kind": "native-build" + }, + { + "id": "D035", + "name": "marker-missing-beats-extra", + "file": "cases/D035-marker-missing-beats-extra.json", + "sha256": "42945b733fcd1500398573f10a3d3ea9811202b90fa1b2a1b9ef7fc4ee976bb2", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "kind": "missing" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "extra" + } + ], + "value": 1 + } + ], + "execution_kind": "native-build" + }, + { + "id": "D036", + "name": "marker-kind-beats-extra", + "file": "cases/D036-marker-kind-beats-extra.json", + "sha256": "114b15de6b1b1142f2b0e856ad06e147a4324a14f64a2959103c4efb0fdf9c00", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMemberWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "kind": "number" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "extra" + } + ], + "value": 1 + } + ], + "execution_kind": "native-build" + }, + { + "id": "D037", + "name": "raw-duplicate-marker", + "file": "cases/D037-raw-duplicate-marker.json", + "sha256": "af9711082da00dba32644e0d2255b5d5b55bf7273286bb93d13c45e3d57bae3e", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "parse", + "expected": { + "phase": "parse", + "kind": "duplicateDecodedKey", + "at": [], + "domain": "syntax" + }, + "premises": [ + { + "assert": "rawDuplicate", + "key": "id" + } + ], + "execution_kind": "strict-parser" + }, + { + "id": "D038", + "name": "self-cycle", + "file": "cases/D038-self-cycle.json", + "sha256": "69e752c46db3f2ed4d114cd6a7bc895a94f160a37d576688a92d0b89e1477157", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "links": 1, + "followExpected": [ + [ + "users", + 0 + ] + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "a" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "a" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D039", + "name": "mutual-cycle", + "file": "cases/D039-mutual-cycle.json", + "sha256": "aef1e26ae78d45d5af213d632179afec471f1a7eb06d75c7c47c76c654fec3f8", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "links": 2, + "followExpected": [ + [ + "users", + 0 + ], + [ + "users", + 1 + ] + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "b" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "a" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D040", + "name": "slash-tilde", + "file": "cases/D040-slash-tilde.json", + "sha256": "5f975b10a98fa222268c36b04ee894057120c5f8d25c784d1fcf78fe8de4f7c7", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "a/b~", + "recordsAt": [ + { + "key": "a/b~" + } + ], + "idKey": "u/id~", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 2, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "a/b~" + }, + { + "index": 0 + }, + { + "key": "u/id~" + } + ], + "value": "0" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D041", + "name": "empty-identities", + "file": "cases/D041-empty-identities.json", + "sha256": "c195973392fbedd25eaaa0b0f9f317df242e023fff0ae6c09adf7f59a96ede2d", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "", + "recordsAt": [ + { + "key": "" + } + ], + "idKey": "", + "referenceFields": [ + { + "key": "r", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "links": 1, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "" + }, + { + "index": 0 + }, + { + "key": "" + } + ], + "value": "" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D042", + "name": "numeric-key-not-index", + "file": "cases/D042-numeric-key-not-index.json", + "sha256": "b31be933d56d90870caacb58beb9b2526bc432d051554e058145358617145e10", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "index": 0 + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 3, + "kind": "selectionWrongKind", + "at": [ + { + "index": 0 + } + ], + "coreCause": "wrongKind", + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [], + "kind": "object" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D043", + "name": "numeric-key", + "file": "cases/D043-numeric-key.json", + "sha256": "b31be933d56d90870caacb58beb9b2526bc432d051554e058145358617145e10", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "0" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "0" + }, + { + "key": "users" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D044", + "name": "configured-collection-absent", + "file": "cases/D044-configured-collection-absent.json", + "sha256": "08e6ca68120665df8617898e6a58f4f299b380a51adec2afc5ca6330ba748bd0", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 4, + "kind": "missingConfiguredCollection", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D045", + "name": "unconfigured-collection", + "file": "cases/D045-unconfigured-collection.json", + "sha256": "f048ff30e948c1c1e7a5498f336f0ae8f6f85220036fb82c9c805d7cab575843", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 4, + "kind": "unconfiguredCollection", + "at": [ + { + "key": "collections" + }, + { + "key": "other" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "other" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D046", + "name": "duplicate-declaration", + "file": "cases/D046-duplicate-declaration.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + }, + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "duplicateCollectionDeclaration", + "at": [ + { + "key": "collections" + }, + { + "index": 1 + }, + { + "key": "name" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "name" + } + ], + "value": "users" + }, + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 1 + }, + { + "key": "name" + } + ], + "value": "users" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D047", + "name": "scalar-distinct-ref-fields", + "file": "cases/D047-scalar-distinct-ref-fields.json", + "sha256": "ba8294e86a4ba4f566b699537918041682c4b680240cbc2383ce34b5aa553a07", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "é", + "presence": "optional", + "nullable": false + }, + { + "key": "é", + "presence": "optional", + "nullable": true + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "states": [ + "absent", + "null" + ], + "referenceSlots": 2, + "links": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "é" + } + ], + "kind": "missing" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "é" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D048", + "name": "id-ref-collision", + "file": "cases/D048-id-ref-collision.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "uuid", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "idReferenceFieldCollision", + "at": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "referenceFields" + }, + { + "index": 0 + }, + { + "key": "key" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "idKey" + } + ], + "value": "uuid" + }, + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "referenceFields" + }, + { + "index": 0 + }, + { + "key": "key" + } + ], + "value": "uuid" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D049", + "name": "duplicate-ref-field", + "file": "cases/D049-duplicate-ref-field.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + }, + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "duplicateReferenceField", + "at": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "referenceFields" + }, + { + "index": 1 + }, + { + "key": "key" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "referenceFields" + }, + { + "index": 1 + }, + { + "key": "key" + } + ], + "value": "friend" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D050", + "name": "nested-array-refused", + "file": "cases/D050-nested-array-refused.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "nested" + }, + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "invalidRecordsAt", + "at": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "recordsAt" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "recordsAt" + }, + { + "index": 0 + }, + { + "key": "key" + } + ], + "value": "nested" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D051", + "name": "unknown-version", + "file": "cases/D051-unknown-version.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/2", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "unsupportedProfileVersion", + "at": [ + { + "key": "version" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "version" + } + ], + "value": "nav12.store-profile/2" + } + ], + "execution_kind": "profile-schema/compiler-negative" + }, + { + "id": "D052", + "name": "negative-profile-index", + "file": "cases/D052-negative-profile-index.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "index": -1 + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "invalidTypedComponent", + "at": [ + { + "key": "collectionsAt" + }, + { + "index": 0 + }, + { + "key": "index" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "collectionsAt" + }, + { + "index": 0 + }, + { + "key": "index" + } + ], + "value": -1 + } + ], + "execution_kind": "native-build" + }, + { + "id": "D053", + "name": "nullable-must-be-boolean", + "file": "cases/D053-nullable-must-be-boolean.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": "yes" + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "invalidNullablePolicy", + "at": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "referenceFields" + }, + { + "index": 0 + }, + { + "key": "nullable" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "profile", + "path": [ + { + "key": "collections" + }, + { + "index": 0 + }, + { + "key": "referenceFields" + }, + { + "index": 0 + }, + { + "key": "nullable" + } + ], + "kind": "string" + } + ], + "execution_kind": "profile-schema/compiler-negative" + }, + { + "id": "D054", + "name": "duplicate-beats-dangling", + "file": "cases/D054-duplicate-beats-dangling.json", + "sha256": "6db9fb7ceb44f3a4573183199a5e1281a9793ef506d58676549215e0aeb329df", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "duplicateRecordID", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ], + "firstAt": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "a" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ], + "value": "a" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "bad" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D055", + "name": "late-malformed-beats-dangling", + "file": "cases/D055-late-malformed-beats-dangling.json", + "sha256": "947de8c3e61b154f63e3f69ba66f29081ea25d140c92980ebece0a514ca3d688", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "bad" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "kind": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D056", + "name": "all-headers-before-records", + "file": "cases/D056-all-headers-before-records.json", + "sha256": "48ef5bf0e68d5e42def5afe51e351a7a90b1e6c237da075fcbe256c7f2ddc8bb", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + }, + { + "name": "posts", + "recordsAt": [ + { + "key": "posts" + } + ], + "idKey": "pid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 4, + "kind": "collectionNotArray", + "at": [ + { + "key": "collections" + }, + { + "key": "posts" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "kind": "missing" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "posts" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D057", + "name": "configured-field-order", + "file": "cases/D057-configured-field-order.json", + "sha256": "6273bd4f23a6919e62b51288264ec9a86b27a9da45fb2e75c9f037c60a0bbe49", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "first", + "presence": "required", + "nullable": false + }, + { + "key": "second", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "first" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "first" + } + ], + "kind": "missing" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "second" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D058", + "name": "unknown-before-dangling", + "file": "cases/D058-unknown-before-dangling.json", + "sha256": "41ea5546361389413f8d111e810111b945aac1d89ce04797733bfe9dfde90419", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 7, + "kind": "referenceUnknownCollection", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "value": "bad" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "missing" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D059", + "name": "marker-collection-null", + "file": "cases/D059-marker-collection-null.json", + "sha256": "2bede9cbd0932509bd434825177c045e008a0a529510b94460d8c421f3bc9598", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMemberWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D060", + "name": "lookalike-marker-key", + "file": "cases/D060-lookalike-marker-key.json", + "sha256": "ca35c419e298704e010174cac26b54344351ec7a60752e0f833dbf2f059a7047", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 6, + "kind": "referenceMarkerMissing", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collection" + } + ], + "kind": "missing" + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + }, + { + "key": "collectión" + } + ], + "value": "users" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D061", + "name": "explicit-dollar-ref-field", + "file": "cases/D061-explicit-dollar-ref-field.json", + "sha256": "81b67061a45a19ecaf259fe02e5b9a933d9620736d514aea4e1af92ab1c4de88", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "$ref", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "links": 1, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "$ref" + }, + { + "key": "id" + } + ], + "value": "a" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D062", + "name": "physical-root-container", + "file": "cases/D062-physical-root-container.json", + "sha256": "22119f3ed20c291c757cd7683b4eb9cdee0c17c68882713c6713c48d5ccb184d", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 1, + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "a" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D063", + "name": "absent-null-linked-separate", + "file": "cases/D063-absent-null-linked-separate.json", + "sha256": "71bf894dfb4c3762b09fbe4dcd19ba54c3f48e68262cfbe3f9171470b188fe6e", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "optional", + "nullable": true + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "states": [ + "absent", + "null", + "link" + ], + "referenceSlots": 3, + "links": 1, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "friend" + } + ], + "kind": "missing" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "friend" + } + ], + "kind": "null" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 2 + }, + { + "key": "friend" + } + ], + "kind": "object" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D064", + "name": "empty-key-container", + "file": "cases/D064-empty-key-container.json", + "sha256": "bfbdb740452fff857dd13335f29add0a959dc1d198ed3a5a847e9f062a0afbe4", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "" + }, + { + "key": "users" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D065", + "name": "exact-reference-id", + "file": "cases/D065-exact-reference-id.json", + "sha256": "083529843664ad250c805923a7203855ba911d32dcffd166bc7026018072d81c", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "optional", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "targetRow": 1, + "domain": "source" + }, + "premises": [ + { + "assert": "scalarDifferent", + "domain": "source", + "left": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "right": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 1 + }, + { + "key": "uuid" + } + ] + }, + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 2 + }, + { + "key": "friend" + }, + { + "key": "id" + } + ], + "value": "é" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D066", + "name": "null-record", + "file": "cases/D066-null-record.json", + "sha256": "126f6eab6a4ff0a7b0e7bd17cbdb392de61f9859e4496ab11109d1883881a36c", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 5, + "kind": "recordWrongKind", + "at": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + } + ], + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + } + ], + "kind": "null" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D067", + "name": "empty-selected-array", + "file": "cases/D067-empty-selected-array.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "recordCount": 0, + "domain": "source" + }, + "premises": [ + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + } + ], + "kind": "array" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D068", + "name": "number-payload-lexemes", + "file": "cases/D068-number-payload-lexemes.json", + "sha256": "8d3fbb3a46831dfa15c4be84e3a5339d78fc3c86e1abb49fa0a76732fe6982ca", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + } + }, + "operation": "build", + "expected": { + "phase": 8, + "kind": "success", + "at": [], + "lexemes": [ + "9007199254740993", + "1e0" + ], + "domain": "source" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "uuid" + } + ], + "value": "a" + }, + { + "assert": "pathKind", + "domain": "source", + "path": [ + { + "key": "collections" + }, + { + "key": "users" + }, + { + "index": 0 + }, + { + "key": "n" + } + ], + "kind": "number" + } + ], + "execution_kind": "native-build" + }, + { + "id": "D069", + "name": "unknown-profile-field", + "file": "cases/D069-unknown-profile-field.json", + "sha256": "b294e2a06cc551ccc5c2e01803d1e094ee207a16b33342244cd30fb956340957", + "profile": { + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [] + } + ], + "references": { + "mode": "disabled" + }, + "guessID": true + }, + "operation": "build", + "expected": { + "phase": 1, + "kind": "unknownProfileField", + "at": [ + { + "key": "guessID" + } + ], + "domain": "profile" + }, + "premises": [ + { + "assert": "exactValue", + "domain": "profile", + "path": [ + { + "key": "guessID" + } + ], + "value": true + } + ], + "execution_kind": "profile-schema/compiler-negative" + } + ] +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D001-empty.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D001-empty.json new file mode 100644 index 0000000..2cb1460 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D001-empty.json @@ -0,0 +1,3 @@ +{ + "collections": {} +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D002-one.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D002-one.json new file mode 100644 index 0000000..02167c1 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D002-one.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": "u1" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D003-same-id-cross-collections.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D003-same-id-cross-collections.json new file mode 100644 index 0000000..5ff8cd0 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D003-same-id-cross-collections.json @@ -0,0 +1,14 @@ +{ + "collections": { + "users": [ + { + "uuid": "u1" + } + ], + "posts": [ + { + "pid": "u1" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D004-duplicate-id.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D004-duplicate-id.json new file mode 100644 index 0000000..34ccfd7 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D004-duplicate-id.json @@ -0,0 +1,12 @@ +{ + "collections": { + "users": [ + { + "uuid": "u1" + }, + { + "uuid": "u1" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D005-missing-id.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D005-missing-id.json new file mode 100644 index 0000000..3113baa --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D005-missing-id.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "id": "u1" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D006-null-id.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D006-null-id.json new file mode 100644 index 0000000..a076d27 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D006-null-id.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D007-id-number.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D007-id-number.json new file mode 100644 index 0000000..58018b6 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D007-id-number.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": 17 + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D008-id-boolean.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D008-id-boolean.json new file mode 100644 index 0000000..02d249d --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D008-id-boolean.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": true + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D009-id-array.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D009-id-array.json new file mode 100644 index 0000000..a5b68fa --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D009-id-array.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": [] + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D010-id-object.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D010-id-object.json new file mode 100644 index 0000000..38e7a75 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D010-id-object.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": {} + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D011-scalar-record.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D011-scalar-record.json new file mode 100644 index 0000000..ab600c7 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D011-scalar-record.json @@ -0,0 +1,7 @@ +{ + "collections": { + "users": [ + "u1" + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D012-null-collection.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D012-null-collection.json new file mode 100644 index 0000000..f4f9a05 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D012-null-collection.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": null + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D013-container-array.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D013-container-array.json new file mode 100644 index 0000000..2a99a02 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D013-container-array.json @@ -0,0 +1,3 @@ +{ + "collections": [] +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D014-missing-container.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D014-missing-container.json new file mode 100644 index 0000000..79802cb --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D014-missing-container.json @@ -0,0 +1,3 @@ +{ + "records": {} +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D015-wrong-ancestor.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D015-wrong-ancestor.json new file mode 100644 index 0000000..5ae477a --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D015-wrong-ancestor.json @@ -0,0 +1,3 @@ +{ + "outer": [] +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D016-scalar-distinct-collections.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D016-scalar-distinct-collections.json new file mode 100644 index 0000000..8f6392b --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D016-scalar-distinct-collections.json @@ -0,0 +1,14 @@ +{ + "collections": { + "é": [ + { + "uuid": "x" + } + ], + "é": [ + { + "uuid": "x" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D017-scalar-distinct-ids.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D017-scalar-distinct-ids.json new file mode 100644 index 0000000..e053837 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D017-scalar-distinct-ids.json @@ -0,0 +1,12 @@ +{ + "collections": { + "users": [ + { + "uuid": "é" + }, + { + "uuid": "é" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D018-equal-payload-two-records.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D018-equal-payload-two-records.json new file mode 100644 index 0000000..88d1b6b --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D018-equal-payload-two-records.json @@ -0,0 +1,18 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "payload": { + "x": 1 + } + }, + { + "uuid": "b", + "payload": { + "x": 1 + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D019-configured-id-only.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D019-configured-id-only.json new file mode 100644 index 0000000..b7cce40 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D019-configured-id-only.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "id": "a", + "_id": "b" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D020-unconfigured-ref-inert.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D020-unconfigured-ref-inert.json new file mode 100644 index 0000000..8848ea3 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D020-unconfigured-ref-inert.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "$ref": { + "collection": "bad", + "id": "bad" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D021-disabled-with-fields.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D021-disabled-with-fields.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D021-disabled-with-fields.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D022-forward-target.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D022-forward-target.json new file mode 100644 index 0000000..543d98d --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D022-forward-target.json @@ -0,0 +1,20 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "b" + } + }, + { + "uuid": "b", + "friend": { + "collection": "users", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D023-unknown-collection.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D023-unknown-collection.json new file mode 100644 index 0000000..5110594 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D023-unknown-collection.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "posts", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D024-dangling-id.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D024-dangling-id.json new file mode 100644 index 0000000..b2ba5cc --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D024-dangling-id.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "bad" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D025-required-absent.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D025-required-absent.json new file mode 100644 index 0000000..d48f7b9 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D025-required-absent.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": "a" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D026-optional-absent.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D026-optional-absent.json new file mode 100644 index 0000000..d48f7b9 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D026-optional-absent.json @@ -0,0 +1,9 @@ +{ + "collections": { + "users": [ + { + "uuid": "a" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D027-required-null-False.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D027-required-null-False.json new file mode 100644 index 0000000..fe5b0ed --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D027-required-null-False.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D028-required-null-True.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D028-required-null-True.json new file mode 100644 index 0000000..fe5b0ed --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D028-required-null-True.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D029-optional-null-False.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D029-optional-null-False.json new file mode 100644 index 0000000..fe5b0ed --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D029-optional-null-False.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D030-optional-null-True.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D030-optional-null-True.json new file mode 100644 index 0000000..fe5b0ed --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D030-optional-null-True.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D031-marker-string.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D031-marker-string.json new file mode 100644 index 0000000..16a06ae --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D031-marker-string.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": "users/a" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D032-marker-id-missing.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D032-marker-id-missing.json new file mode 100644 index 0000000..6dc5ced --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D032-marker-id-missing.json @@ -0,0 +1,12 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D033-marker-id-null.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D033-marker-id-null.json new file mode 100644 index 0000000..c191cf6 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D033-marker-id-null.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": null + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D034-marker-extra.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D034-marker-extra.json new file mode 100644 index 0000000..3952413 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D034-marker-extra.json @@ -0,0 +1,14 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "a", + "other": 1 + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D035-marker-missing-beats-extra.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D035-marker-missing-beats-extra.json new file mode 100644 index 0000000..2b9bbaf --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D035-marker-missing-beats-extra.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "extra": 1 + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D036-marker-kind-beats-extra.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D036-marker-kind-beats-extra.json new file mode 100644 index 0000000..729ed0f --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D036-marker-kind-beats-extra.json @@ -0,0 +1,14 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": 17, + "id": "a", + "extra": 1 + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D037-raw-duplicate-marker.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D037-raw-duplicate-marker.json new file mode 100644 index 0000000..a26476c --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D037-raw-duplicate-marker.json @@ -0,0 +1 @@ +{"collections":{"users":[{"uuid":"a","friend":{"collection":"users","id":"a","\u0069d":"b"}}]}} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D038-self-cycle.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D038-self-cycle.json new file mode 100644 index 0000000..7f3e950 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D038-self-cycle.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D039-mutual-cycle.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D039-mutual-cycle.json new file mode 100644 index 0000000..543d98d --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D039-mutual-cycle.json @@ -0,0 +1,20 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "b" + } + }, + { + "uuid": "b", + "friend": { + "collection": "users", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D040-slash-tilde.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D040-slash-tilde.json new file mode 100644 index 0000000..123ebc4 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D040-slash-tilde.json @@ -0,0 +1,12 @@ +{ + "collections": { + "a/b~": [ + { + "u/id~": "0" + }, + { + "u/id~": "a/b" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D041-empty-identities.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D041-empty-identities.json new file mode 100644 index 0000000..19b081c --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D041-empty-identities.json @@ -0,0 +1,13 @@ +{ + "collections": { + "": [ + { + "": "", + "r": { + "collection": "", + "id": "" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D042-numeric-key-not-index.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D042-numeric-key-not-index.json new file mode 100644 index 0000000..26b7f40 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D042-numeric-key-not-index.json @@ -0,0 +1,5 @@ +{ + "0": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D043-numeric-key.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D043-numeric-key.json new file mode 100644 index 0000000..26b7f40 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D043-numeric-key.json @@ -0,0 +1,5 @@ +{ + "0": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D044-configured-collection-absent.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D044-configured-collection-absent.json new file mode 100644 index 0000000..2cb1460 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D044-configured-collection-absent.json @@ -0,0 +1,3 @@ +{ + "collections": {} +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D045-unconfigured-collection.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D045-unconfigured-collection.json new file mode 100644 index 0000000..55f7c92 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D045-unconfigured-collection.json @@ -0,0 +1,6 @@ +{ + "collections": { + "users": [], + "other": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D046-duplicate-declaration.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D046-duplicate-declaration.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D046-duplicate-declaration.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D047-scalar-distinct-ref-fields.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D047-scalar-distinct-ref-fields.json new file mode 100644 index 0000000..93a1e68 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D047-scalar-distinct-ref-fields.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "é": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D048-id-ref-collision.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D048-id-ref-collision.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D048-id-ref-collision.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D049-duplicate-ref-field.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D049-duplicate-ref-field.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D049-duplicate-ref-field.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D050-nested-array-refused.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D050-nested-array-refused.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D050-nested-array-refused.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D051-unknown-version.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D051-unknown-version.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D051-unknown-version.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D052-negative-profile-index.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D052-negative-profile-index.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D052-negative-profile-index.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D053-nullable-must-be-boolean.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D053-nullable-must-be-boolean.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D053-nullable-must-be-boolean.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D054-duplicate-beats-dangling.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D054-duplicate-beats-dangling.json new file mode 100644 index 0000000..eb140df --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D054-duplicate-beats-dangling.json @@ -0,0 +1,16 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "bad" + } + }, + { + "uuid": "a" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D055-late-malformed-beats-dangling.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D055-late-malformed-beats-dangling.json new file mode 100644 index 0000000..c65b698 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D055-late-malformed-beats-dangling.json @@ -0,0 +1,19 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "users", + "id": "bad" + } + }, + { + "uuid": "b", + "friend": { + "collection": "users" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D056-all-headers-before-records.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D056-all-headers-before-records.json new file mode 100644 index 0000000..414b019 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D056-all-headers-before-records.json @@ -0,0 +1,8 @@ +{ + "collections": { + "users": [ + {} + ], + "posts": null + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D057-configured-field-order.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D057-configured-field-order.json new file mode 100644 index 0000000..b52729a --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D057-configured-field-order.json @@ -0,0 +1,10 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "second": null + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D058-unknown-before-dangling.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D058-unknown-before-dangling.json new file mode 100644 index 0000000..7cce031 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D058-unknown-before-dangling.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": "bad", + "id": "missing" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D059-marker-collection-null.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D059-marker-collection-null.json new file mode 100644 index 0000000..252a89d --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D059-marker-collection-null.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collection": null, + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D060-lookalike-marker-key.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D060-lookalike-marker-key.json new file mode 100644 index 0000000..f73040f --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D060-lookalike-marker-key.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "friend": { + "collectión": "users", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D061-explicit-dollar-ref-field.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D061-explicit-dollar-ref-field.json new file mode 100644 index 0000000..3248d22 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D061-explicit-dollar-ref-field.json @@ -0,0 +1,13 @@ +{ + "collections": { + "users": [ + { + "uuid": "a", + "$ref": { + "collection": "users", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D062-physical-root-container.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D062-physical-root-container.json new file mode 100644 index 0000000..aeef8a7 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D062-physical-root-container.json @@ -0,0 +1,7 @@ +{ + "users": [ + { + "uuid": "a" + } + ] +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D063-absent-null-linked-separate.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D063-absent-null-linked-separate.json new file mode 100644 index 0000000..a91520a --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D063-absent-null-linked-separate.json @@ -0,0 +1,20 @@ +{ + "collections": { + "users": [ + { + "uuid": "a" + }, + { + "uuid": "b", + "friend": null + }, + { + "uuid": "c", + "friend": { + "collection": "users", + "id": "a" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D064-empty-key-container.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D064-empty-key-container.json new file mode 100644 index 0000000..0b81d35 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D064-empty-key-container.json @@ -0,0 +1,5 @@ +{ + "": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D065-exact-reference-id.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D065-exact-reference-id.json new file mode 100644 index 0000000..341e2f1 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D065-exact-reference-id.json @@ -0,0 +1,19 @@ +{ + "collections": { + "users": [ + { + "uuid": "é" + }, + { + "uuid": "é" + }, + { + "uuid": "z", + "friend": { + "collection": "users", + "id": "é" + } + } + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D066-null-record.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D066-null-record.json new file mode 100644 index 0000000..4494cc9 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D066-null-record.json @@ -0,0 +1,7 @@ +{ + "collections": { + "users": [ + null + ] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D067-empty-selected-array.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D067-empty-selected-array.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D067-empty-selected-array.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D068-number-payload-lexemes.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D068-number-payload-lexemes.json new file mode 100644 index 0000000..119bddb --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D068-number-payload-lexemes.json @@ -0,0 +1 @@ +{"collections":{"users":[{"uuid":"a","n":9007199254740993,"m":1e0}]}} diff --git a/Tests/JSONInspectionTests/StoreFixtures/cases/D069-unknown-profile-field.json b/Tests/JSONInspectionTests/StoreFixtures/cases/D069-unknown-profile-field.json new file mode 100644 index 0000000..6f1c432 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/cases/D069-unknown-profile-field.json @@ -0,0 +1,5 @@ +{ + "collections": { + "users": [] + } +} diff --git a/Tests/JSONInspectionTests/StoreFixtures/profiles/example.json b/Tests/JSONInspectionTests/StoreFixtures/profiles/example.json new file mode 100644 index 0000000..18ede52 --- /dev/null +++ b/Tests/JSONInspectionTests/StoreFixtures/profiles/example.json @@ -0,0 +1,31 @@ +{ + "version": "nav12.store-profile/1", + "collectionsAt": [ + { + "key": "collections" + } + ], + "collections": [ + { + "name": "users", + "recordsAt": [ + { + "key": "users" + } + ], + "idKey": "uuid", + "referenceFields": [ + { + "key": "friend", + "presence": "required", + "nullable": false + } + ] + } + ], + "references": { + "mode": "strict", + "marker": "collection-id/1", + "validateTargets": true + } +} diff --git a/Tests/JSONInspectionTests/StoreRuntimeTests.swift b/Tests/JSONInspectionTests/StoreRuntimeTests.swift new file mode 100644 index 0000000..6d6a34f --- /dev/null +++ b/Tests/JSONInspectionTests/StoreRuntimeTests.swift @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +@testable import JSONInspection + +private enum StoreTestError: Error { case expectedFailure } +private func storeBuild(_ id: String = "D002", caps: JSONStoreCaps = .init(), + limits: JSONLimits = .balanced) throws -> JSONOperationReport { + let fixture = try storeCase(id) + return try JSONStoreBuilder(document: storeInput(fixture), profile: storeProfile(storeMember(fixture,"profile")), + limits: limits, caps: caps).build() +} +private func storeRefusal(_ body: () throws -> Void) throws -> JSONStoreFailure { + do { try body(); Issue.record("Expected typed store refusal"); throw StoreTestError.expectedFailure } + catch let error as JSONStoreFailure { return error } +} +private func storeCoreRefusal(_ body: () throws -> Void) throws -> JSONFailure { + do { try body(); Issue.record("Expected original core resource refusal"); throw StoreTestError.expectedFailure } + catch let error as JSONFailure { return error } +} +private func storeSimpleProfile(name: JSONKey = "users", id: JSONKey = "uuid", + path: [JSONLocationComponent] = [.key("collections")], + fields: [JSONStoreReferenceField] = []) -> JSONStoreProfile { + .init(version: .v1, collectionsAt: path, collections: [ + .init(name: name, recordsAt: [.key(name)], idKey: id, referenceFields: fields)], + references: fields.isEmpty ? .disabled : .strict(marker: .v1, validateTargets: true)) +} +private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSONDocument { + var rows: [JSON] = [] + for i in 0.. base.statistics[.workUnits]) + var limits = JSONLimits.balanced; limits[.inputBytes] = 0 + #expect(try storeCoreRefusal { _ = try storeBuild(limits: limits) }.resource?.metric == .inputBytes) + let outside = try JSONParser().parseDocument(Data(#"{"collections":{"users":[]},"outside":[1,2,3]}"#.utf8)) + limits = .balanced; limits[.nodes] = 4 + #expect(try storeCoreRefusal { _ = try JSONStoreBuilder(document: outside, profile: storeSimpleProfile(), limits: limits).build() }.resource?.metric == .nodes) + print("STORE_RUNTIME R02 passed") +} + +@Test func storeR03AllInclusiveCapsCheckedArithmeticAndLateRefusal() throws { + let ordinary = try storeBuild(), strict = try storeBuild("D038") + let rows: [(String, WritableKeyPath, Int, String)] = [ + ("collections",\JSONStoreCaps.collections,1,"D002"), + ("recordsPerCollection",\JSONStoreCaps.recordsPerCollection,1,"D002"), + ("records",\JSONStoreCaps.records,1,"D002"), + ("referenceFieldsPerCollection",\JSONStoreCaps.referenceFieldsPerCollection,1,"D038"), + ("referenceFields",\JSONStoreCaps.referenceFields,1,"D038"), + ("referenceSlots",\JSONStoreCaps.referenceSlots,1,"D038"), + ("links",\JSONStoreCaps.links,1,"D038"), + ("identityBytes",\JSONStoreCaps.identityBytes,5,"D002"), + ("profileTextBytes",\JSONStoreCaps.profileTextBytes,Int(ordinary.result.profileTextBytes),"D002"), + ("aliasBytes",\JSONStoreCaps.aliasBytes,Int(ordinary.result.identityAliasBytes),"D002"), + ("pathComponents",\JSONStoreCaps.pathComponents,4,"D002"), + ("overlayBytes",\JSONStoreCaps.overlayBytes,Int(ordinary.result.overlayLogicalBytes),"D002")] + for (name,key,needed,id) in rows { + var caps = JSONStoreCaps(); caps[keyPath:key] = needed + _ = try storeBuild(id,caps:caps) + caps[keyPath:key] = needed - 1 + let failure = try storeRefusal { _ = try storeBuild(id,caps:caps) } + #expect(failure.code == .capacityExceeded && failure.capacity == name) + caps[keyPath:key] = -1 + #expect(try storeCoreRefusal { _ = try storeBuild(id,caps:caps) }.code == .invalidLimits) + caps[keyPath:key] = Int.max + #expect(try storeCoreRefusal { _ = try storeBuild(id,caps:caps) }.code == .invalidLimits) + print("STORE_CAP \(name) exact=\(needed) below=\(needed-1) passed") + } + for metric in [JSONResourceMetric.workUnits,.retainedBytes] { + var limits = JSONLimits.balanced + let needed = metric == .workUnits ? ordinary.statistics[metric] : ordinary.statistics.peakLogicalRetainedBytes + limits[metric] = needed; _ = try storeBuild(limits:limits) + limits[metric] -= 1 + #expect(try storeCoreRefusal { _ = try storeBuild(limits:limits) }.resource?.metric == metric) + } + var aliasCaps = JSONStoreCaps(); aliasCaps.aliasBytes = 7 + _ = try storeBuild("D038",caps:aliasCaps) + aliasCaps.aliasBytes = 6 + #expect(try storeRefusal { _ = try storeBuild("D038",caps:aliasCaps) }.capacity == "aliasBytes") + #expect(strict.result.identityAliasBytes == 1) // staged marker text was released + for operation in [0,1] { + #expect(try storeRefusal { + if operation == 0 { _ = try JSONStoreAccounting.sum(UInt64.max,1) } + else { _ = try JSONStoreAccounting.product(UInt64.max,2) } + }.code == .counterOverflow) + } + var capped = JSONStoreCaps(); capped.referenceSlots = 0 + #expect(try storeRefusal { _ = try storeBuild("D024",caps:capped) }.capacity == "referenceSlots") + capped.referenceSlots = 1 + #expect(try storeRefusal { _ = try storeBuild("D024",caps:capped) }.code == .referenceDanglingID) + let short = try JSONParser().parseDocument(Data(#"{"collections":{"x":[{"id":"éx"}]}}"#.utf8)) + capped = .init(); capped.identityBytes = 3 + _ = try JSONStoreBuilder(document: short,profile:storeSimpleProfile(name:"x",id:"id"),caps:capped).build() + capped.identityBytes = 2 + #expect(try storeRefusal { _ = try JSONStoreBuilder(document:short,profile:storeSimpleProfile(name:"x",id:"id"),caps:capped).build() }.capacity == "identityBytes") + print("STORE_RUNTIME R03 passed") +} + +@Test func storeR04PresenceSlotsRetentionAndScaleCalibration() throws { + let empty = try storeBuild("D001").result + #expect(empty.recordCount == 0 && empty.references.isEmpty) + let states = try storeBuild("D063").result + #expect(states.references.count == 3) + if case .absent = states.references[0].state {} else { Issue.record("Missing state lost") } + if case .null = states.references[1].state {} else { Issue.record("Null state lost") } + if case .link = states.references[2].state {} else { Issue.record("Link state lost") } + var caps = JSONStoreCaps(); caps.referenceSlots = 2 + #expect(try storeRefusal { _ = try storeBuild("D063",caps:caps) }.capacity == "referenceSlots") + caps.referenceSlots = 3; _ = try storeBuild("D063",caps:caps) + for size in [0,1,16,1024] { + let document = try storeRows(size) + let report = try JSONStoreBuilder(document:document,profile:storeSimpleProfile()).build() + #expect(report.result.recordCount == size) + #expect(report.statistics[.retainedBytes] >= report.result.overlayLogicalBytes) + #expect(report.statistics.peakLogicalRetainedBytes >= report.statistics[.retainedBytes]) + print("STORE_RESOURCE rows=\(size) work=\(report.statistics[.workUnits]) retained=\(report.statistics[.retainedBytes]) peak=\(report.statistics.peakLogicalRetainedBytes) overlay=\(report.result.overlayLogicalBytes)") + } + #expect(35_163_280 > JSONStoreCaps().overlayBytes) // declared maxima are not jointly feasible + print("STORE_RUNTIME R04 passed") +} + +@Test func storeR05InnerCancellationAcrossPhasesAndAfterTransfer() async throws { + let long = String(repeating:"x",count:2048)+"a" + let text = "{\"collections\":{\"users\":[{\"uuid\":\"\(long)\",\"friend\":{\"collection\":\"users\",\"id\":\"\(long)\"}}]}}" + let document = try JSONParser().parseDocument(Data(text.utf8)) + let profile = storeSimpleProfile(fields:[.init(key:"friend",presence:.required,nullable:false)]) + for targetPhase in [1,2,4,5,6,7,8] { + let task = Task { + let state = Mutex<(phase:Int,work:UInt64,start:UInt64?)>((0,0,nil)) + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + var builder = JSONStoreBuilder(document:document,profile:profile,limits:limits) + builder.phaseObserver = { phase in state.withLock { + $0.phase = phase + if phase == targetPhase && $0.start == nil { $0.start = $0.work } + } } + builder.checkpointObserver = { work in state.withLock { + $0.work = work + if $0.phase == targetPhase, let start = $0.start, + work >= start + (targetPhase == 8 ? 0 : targetPhase == 4 ? 4 : targetPhase == 1 ? 8 : 128) { + withUnsafeCurrentTask { $0?.cancel() } + } + } } + return try builder.build() + } + do { _ = try await task.value; Issue.record("Expected phase \(targetPhase) cancellation") } + catch is CancellationError { } + } + let after = Task { + let finish = Mutex(0) + var builder = JSONStoreBuilder(document:document,profile:profile) + builder.phaseObserver = { phase in + if phase == 8 { finish.withLock { $0 += 1; if $0 == 2 { withUnsafeCurrentTask { $0?.cancel() } } } } + } + return try builder.build() + } + do { _ = try await after.value; Issue.record("Transfer cannot certify completion after cancellation") } + catch is CancellationError { } + print("STORE_RUNTIME R05 passed") +} + +@Test func storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth() async throws { + let fixture = try storeCase("D038"), source = try storeInput(fixture), profile = try storeProfile(storeMember(fixture,"profile")) + let report = try JSONStoreBuilder(document:source,profile:profile).build() + var short = JSONLimits.balanced; short[.workUnits] = report.statistics[.workUnits] - 1 + #expect(try storeCoreRefusal { _ = try JSONStoreBuilder(document:source,profile:profile,limits:short).build() }.resource?.metric == .workUnits) + let values = try await withThrowingTaskGroup(of: UInt64.self) { group in + for _ in 0..<16 { group.addTask { try JSONStoreBuilder(document:source,profile:profile).build().statistics[.workUnits] } } + var results: [UInt64] = [] + for try await value in group { results.append(value) } + return results + } + #expect(values.count == 16 && values.allSatisfy { $0 == report.statistics[.workUnits] }) + // D0 tests exercise caught native failures and replacement attempts inside the only capability. + print("STORE_RUNTIME R06 passed") +} + +@Test func storeR07SourceAndStateInvalidatedOnEveryFailureAndSuccess() throws { + let facts = Mutex<[(Bool,Bool)]>([]) + for id in ["D002","D046","D004","D055","D024"] { + let fixture = try storeCase(id) + var builder = try JSONStoreBuilder(document:storeInput(fixture),profile:storeProfile(storeMember(fixture,"profile"))) + builder.lifetimeObserver = { root,state in facts.withLock { $0.append((root,state)) } } + if id == "D002" { #expect(try builder.build().result.document.sourceMap != nil) } + else { _ = try storeRefusal { _ = try builder.build() } } + } + let result = facts.withLock { $0 } + #expect(result.count == 5 && result.allSatisfy { $0.0 && $0.1 }) + print("STORE_RUNTIME R07 passed") +} + +@Test func storeR08DeclarationAndPhysicalPermutationMetamorphisms() throws { + let fixture = try storeCase("D003"), document = try storeInput(fixture) + let original = try storeProfile(storeMember(fixture,"profile")) + let reversed = JSONStoreProfile(version:.v1,collectionsAt:original.collectionsAt, + collections:Array(original.collections.reversed()),references:original.references) + let a = try JSONStoreBuilder(document:document,profile:original).build().result + let b = try JSONStoreBuilder(document:document,profile:reversed).build().result + #expect(try a.record(collection:0,row:0).path == b.record(collection:0,row:0).path) + #expect(try a.record(collection:1,row:0).path == b.record(collection:1,row:0).path) + let rows = try storeRows(2) + let array = try #require(rows.root.objectValue?["collections"]?.objectValue?["users"]?.arrayValue) + let reordered = JSONDocument(root:.object(try JSONObject(members:[.init("collections",value:.object( + try JSONObject(members:[.init("users",value:.array(Array(array.reversed())))])))]))) + let first = try JSONStoreBuilder(document:rows,profile:storeSimpleProfile()).build().result + let second = try JSONStoreBuilder(document:reordered,profile:storeSimpleProfile()).build().result + #expect(try first.record(collection:0,row:0).id == second.record(collection:0,row:1).id) + #expect(try first.record(collection:0,row:0).path != second.record(collection:0,row:1).path) + let exact = try storeBuild("D065").result + #expect(try JSONComparison.equals(.string(exact.record(collection:0,row:0).id),.string(exact.record(collection:0,row:1).id)) == false) + print("STORE_RUNTIME R08 passed") +} + +@Test func storeR09CompleteRowsDeepSourcesAndZeroCapacity() throws { + for depth in [16,64,256] { + var root = try storeRows(1).root + for _ in 0.. Data { + let root = try #require(Bundle.module.resourceURL) + return try Data(contentsOf: URL(fileURLWithPath: root.path + "/StoreFixtures/" + path)) +} +func storeMember(_ value: JSON, _ key: String) throws -> JSON { + let object = try #require(value.objectValue) + return try #require(object[key]) +} +func storeText(_ value: JSON) throws -> String { try #require(value.stringValue) } +func storeInt(_ value: JSON) throws -> Int { try #require(value.numberValue).integer(as: Int.self) } +func storePath(_ value: JSON) throws -> [JSONLocationComponent] { + try #require(value.arrayValue).map { part in + let object = try #require(part.objectValue) + if let key = object["key"] { return .key(JSONKey(try storeText(key))) } + return .index(try storeInt(try storeMember(part,"index"))) + } +} +func storeProfile(_ value: JSON) throws -> JSONStoreProfile { + #expect(try storeText(storeMember(value,"version")) == JSONStoreProfileVersion.v1.rawValue) + let declarations = try #require(try storeMember(value,"collections").arrayValue) + let collections = try declarations.map { declaration in + let fields = try #require(try storeMember(declaration,"referenceFields").arrayValue).map { field in + JSONStoreReferenceField(key: JSONKey(try storeText(storeMember(field,"key"))), + presence: try #require(JSONStoreFieldPresence(rawValue: storeText(storeMember(field,"presence")))), + nullable: try #require(try storeMember(field,"nullable").booleanValue)) + } + return JSONStoreCollectionProfile(name: JSONKey(try storeText(storeMember(declaration,"name"))), + recordsAt: try storePath(storeMember(declaration,"recordsAt")), + idKey: JSONKey(try storeText(storeMember(declaration,"idKey"))), referenceFields: fields) + } + let refs = try storeMember(value,"references") + let mode = try storeText(storeMember(refs,"mode")) + #expect(mode == "disabled" || mode == "strict") + if mode == "strict" { #expect(try storeText(storeMember(refs,"marker")) == JSONStoreMarkerVersion.v1.rawValue) } + let references: JSONStoreReferences = mode == "disabled" ? .disabled + : .strict(marker: .v1, validateTargets: try #require(try storeMember(refs,"validateTargets").booleanValue)) + return JSONStoreProfile(version: .v1, collectionsAt: try storePath(storeMember(value,"collectionsAt")), + collections: collections, references: references) +} +// Immutable independent input oracle, shared only by the test driver. +private let frozenStoreCases: [JSON] = { + do { + let manifest = try JSONParser(options: .init(limits: .offline)).parse(storeFixture("VECTORS.json")) + return try #require(try storeMember(manifest,"cases").arrayValue) + } catch { fatalError("Required store fixture manifest failed: \(error)") } +}() +func storeManifestCases() throws -> [JSON] { frozenStoreCases } +func storeCase(_ id: String) throws -> JSON { + let cases = try storeManifestCases() + return try #require(cases.first { try storeText(storeMember($0,"id")) == id }) +} +func storeInput(_ fixture: JSON) throws -> JSONDocument { + let bytes = try storeFixture(storeText(storeMember(fixture,"file"))) + // This separate fixture-integrity operation is caller work, not the build ledger. + var hashMeter = try JSONWorkMeter(limits: .offline, domain: .core) + let hash = try JSONSHA256Core.hexadecimal(bytes, accounting: &hashMeter) + #expect(hash == (try storeText(storeMember(fixture,"sha256")))) + return try JSONParser().parseDocument(bytes) +} +func storeTokens(_ path: [JSONLocationComponent]) -> [String] { + path.map { switch $0 { case .key(let key): key.string; case .index(let index): String(index) } } +} + +private let storeBuildVectorIDs = ["D001", "D002", "D003", "D004", "D005", "D006", "D007", "D008", "D009", "D010", "D011", "D012", "D013", "D014", "D015", "D016", "D017", "D018", "D019", "D020", "D021", "D022", "D023", "D024", "D025", "D026", "D027", "D028", "D029", "D030", "D031", "D032", "D033", "D034", "D035", "D036", "D038", "D039", "D040", "D041", "D042", "D043", "D044", "D045", "D046", "D047", "D048", "D049", "D050", "D052", "D054", "D055", "D056", "D057", "D058", "D059", "D060", "D061", "D062", "D063", "D064", "D065", "D066", "D067", "D068"] + +@Test(arguments: storeBuildVectorIDs) +func storeD1NativeVector(id: String) throws { + let fixture = try storeCase(id), expected = try storeMember(fixture,"expected") + let document = try storeInput(fixture), profile = try storeProfile(storeMember(fixture,"profile")) + let kind = try storeText(storeMember(expected,"kind")) + do { + let report = try JSONStoreBuilder(document: document, profile: profile).build() + #expect(kind == "success") + let store = report.result + if let count = expected.objectValue?["collectionCount"] { #expect(store.collectionCount == (try storeInt(count))) } + if let count = expected.objectValue?["recordCount"] { #expect(store.recordCount == (try storeInt(count))) } + if let count = expected.objectValue?["referenceSlots"] { #expect(store.references.count == (try storeInt(count))) } + var states: [String] = [], links = 0 + for reference in store.references { + switch reference.state { + case .absent: states.append("absent") + case .null: states.append("null") + case .link(let target): + states.append("link"); links += 1 + _ = try store.record(at: target) + if let row = expected.objectValue?["targetRow"] { #expect(target.row == (try storeInt(row))) } + } + } + if let count = expected.objectValue?["links"] { #expect(links == (try storeInt(count))) } + if let expectedStates = expected.objectValue?["states"] { + #expect(states == (try #require(expectedStates.arrayValue).map(storeText))) + } + // Every positive record's exact original ID path resolves through core authority. + for ci in 0.. 0 && report.statistics[.retainedBytes] > 0) + #expect(store.document.sourceBytes == document.sourceBytes) + print("STORE_VECTOR \(id) passed driver=native-build phase=8 kind=success work=\(report.statistics[.workUnits]) nodes=\(report.statistics[.nodes]) retained=\(report.statistics[.retainedBytes])") + } catch let error as JSONStoreFailure { + #expect(error.code.rawValue == kind) + #expect(error.phase == (try storeInt(storeMember(expected,"phase")))) + #expect(error.domain.rawValue == (try storeText(storeMember(expected,"domain")))) + #expect(error.path == (try storePath(storeMember(expected,"at")))) + if let first = expected.objectValue?["firstAt"] { #expect(error.firstPath == (try storePath(first))) } + if let cause = expected.objectValue?["coreCause"] { + #expect(error.coreCause?.code.rawValue == (try storeText(cause))) + } + print("STORE_VECTOR \(id) passed driver=native-build phase=\(error.phase) kind=\(error.code.rawValue)") + } +} + +@Test func storeD1StrictParserD037RejectsDecodedDuplicateMarker() throws { + let fixture = try storeCase("D037") + let data = try storeFixture(storeText(storeMember(fixture,"file"))) + do { _ = try JSONParser().parseDocument(data); Issue.record("Duplicate decoded marker keys must fail native parsing") } + catch let failure as JSONFailure { + #expect(failure.category == .syntax) + #expect(failure.code == .duplicateKey) + } + print("STORE_VECTOR D037 passed driver=strict-parser phase=parse kind=duplicateDecodedKey") +} From 208af0171573e2367d3c02783daec061c15a8e2e Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:18:28 +0200 Subject: [PATCH 18/25] Disambiguate optional Boolean fixture assertions --- Tests/JSONInspectionTests/StoreVectorTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/JSONInspectionTests/StoreVectorTests.swift b/Tests/JSONInspectionTests/StoreVectorTests.swift index e4f7f8b..254dcb9 100644 --- a/Tests/JSONInspectionTests/StoreVectorTests.swift +++ b/Tests/JSONInspectionTests/StoreVectorTests.swift @@ -30,7 +30,7 @@ func storeProfile(_ value: JSON) throws -> JSONStoreProfile { let fields = try #require(try storeMember(declaration,"referenceFields").arrayValue).map { field in JSONStoreReferenceField(key: JSONKey(try storeText(storeMember(field,"key"))), presence: try #require(JSONStoreFieldPresence(rawValue: storeText(storeMember(field,"presence")))), - nullable: try #require(try storeMember(field,"nullable").booleanValue)) + nullable: try #require(try storeMember(field,"nullable").booleanValue as Bool?)) } return JSONStoreCollectionProfile(name: JSONKey(try storeText(storeMember(declaration,"name"))), recordsAt: try storePath(storeMember(declaration,"recordsAt")), @@ -41,7 +41,7 @@ func storeProfile(_ value: JSON) throws -> JSONStoreProfile { #expect(mode == "disabled" || mode == "strict") if mode == "strict" { #expect(try storeText(storeMember(refs,"marker")) == JSONStoreMarkerVersion.v1.rawValue) } let references: JSONStoreReferences = mode == "disabled" ? .disabled - : .strict(marker: .v1, validateTargets: try #require(try storeMember(refs,"validateTargets").booleanValue)) + : .strict(marker: .v1, validateTargets: try #require(try storeMember(refs,"validateTargets").booleanValue as Bool?)) return JSONStoreProfile(version: .v1, collectionsAt: try storePath(storeMember(value,"collectionsAt")), collections: collections, references: references) } From 0135d600bc5fdfdbeddc431aca3a434d4562c369 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:57:04 +0200 Subject: [PATCH 19/25] Tighten preparation visibility and complete store acceptance witnesses --- .../NAV12Preparation.md | 6 +- Scripts/ci.py | 4 +- Scripts/qualify_store_preparation.py | 1 + Scripts/store_acceptance.py | 3 +- Scripts/test_qualification_harness.py | 4 +- Sources/JSON/Reading/JSONReadScope.swift | 2 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Store/JSONConfiguredStore.swift | 2 +- .../Store/JSONStoreAccounting.swift | 6 ++ .../StoreRuntimeTests.swift | 94 ++++++++++++++++++- .../StoreVectorTests.swift | 35 +++++++ 11 files changed, 148 insertions(+), 11 deletions(-) diff --git a/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md b/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md index 0d4ef01..0720a13 100644 --- a/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md +++ b/Documentation/Architecture/StructuralIntelligence/NAV12Preparation.md @@ -17,7 +17,7 @@ admission only when the result does not own it; profile output reservations are not incorrectly included in the input refund. Full Debug/Release code generation supplements the unchanged original 2-positive/ -20-negative matrix with 2 positive and 36 ordinary-negative prepared-capability +20-negative matrix with 2 positive and 38 ordinary-negative prepared-capability outcomes. A compiler crash, permitted escape or incidental diagnostic blocks this experiment. Runtime tests require every-counter no-op parity, one root admission, cumulative work/exact/one-below quotas, pre-admission refusal, caught-error and @@ -31,3 +31,7 @@ the original gates and add the prepared matrix; all 49 core-cap witnesses and 126 maintenance identities remain mandatory. Actual receipts, not this contract, establish execution. No public declaration/product, store-follow engine, merge or release is introduced by this preparation increment. + +The generic preparation-state closure is internal to JSON. Navigation receives +only the strategy entry; a separate full-codegen negative rejects using that +closure to return raw state from another target in the same package. diff --git a/Scripts/ci.py b/Scripts/ci.py index 5c9c178..c6e7487 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -265,7 +265,7 @@ def check_result(command, code, log, events=None): if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|store']): expected = [DISCOVERY['root_functions']['JSONInspectionTests']] if events is not None: - passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameterized_functions'] == {'storeD1NativeVector(id:)': 65} and check_projection_cases(events) and check_store_tests(events, log) + passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 65 and events['parameterized_functions'] == {'storeD1NativeVector(id:)': 65} and check_projection_cases(events) and check_store_tests(events, log) if prepared_test: expected_ids = set(DISCOVERY['prepared_entry_function_ids']) if 'navigationPreparedD0IndependentConcurrentHealth' in command: @@ -351,7 +351,7 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_store_preparation.py']: data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) probes = [x for x in data['commands'] if x.get('expected') is not None] - passed = passed and data['status'] == 'passed' and len(data['commands']) == 42 and len(probes) == 36 + passed = passed and data['status'] == 'passed' and len(data['commands']) == 44 and len(probes) == 38 passed = passed and all(x['passed'] and not x['compiler_crash'] for x in data['commands']) if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() diff --git a/Scripts/qualify_store_preparation.py b/Scripts/qualify_store_preparation.py index e1ff1eb..872498f 100644 --- a/Scripts/qualify_store_preparation.py +++ b/Scripts/qualify_store_preparation.py @@ -20,6 +20,7 @@ """ PROBES={ 'positive':(POSITIVE,None), + 'raw-preparation-state':('func illegal() throws { let scope = try JSONReadScope(preparingWith: .balanced); _ = try scope.runPreparation { $0 } }', "'runPreparation' is inaccessible"), 'return':('func illegal(_ c: inout JSONReadPreparationContext) -> JSONReadPreparationContext { consume c }','missing reinitialization of inout parameter'), 'escape':('func illegal(_ c: inout JSONReadPreparationContext) -> () throws -> Void { { try c.checkpoint() } }',"escaping closure captures 'inout' parameter"), 'explicit-capture':('func illegal(_ c: inout JSONReadPreparationContext) -> () -> Void { { [c = consume c] in _ = consume c } }', 'missing reinitialization of inout parameter'), diff --git a/Scripts/store_acceptance.py b/Scripts/store_acceptance.py index e94d8e8..343d771 100644 --- a/Scripts/store_acceptance.py +++ b/Scripts/store_acceptance.py @@ -16,7 +16,8 @@ def check_store_tests(events, log, shared=False, scoped=False): if len(names) != len(set(names)) or set(names) != expected: return False if scoped and (events['module_function_counts'] != {'JSONInspectionTests': len(expected)} or - events['parameterized_functions'] != ({} if shared else {'storeD1NativeVector(id:)': 65})): + events['parameterized_functions'] != ({} if shared else {'storeD1NativeVector(id:)': 65}) or + events['parameter_case_invocations'] != (0 if shared else 65)): return False observed = re.findall(r'^STORE_VECTOR (D\d+) passed driver=(\S+) phase=(\S+) kind=(\S+)', log, re.M) expected_vectors = [] if shared else [(x['id'], x['execution_kind'], str(x['expected']['phase']), x['expected']['kind']) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index ab65656..aaf5956 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -25,7 +25,7 @@ def store_markers(): class HarnessTests(unittest.TestCase): def test_store_requires_exact_engine_vectors_runtime_and_compiler_primary_diagnostics(self): events = dict(function_names=sorted(STORE_FUNCTIONS), module_function_counts={'JSONInspectionTests':12}, - parameterized_functions={'storeD1NativeVector(id:)':65}) + parameterized_functions={'storeD1NativeVector(id:)':65}, parameter_case_invocations=65) log = store_markers() self.assertTrue(check_store_tests(events, log, scoped=True)) for broken in [log.replace('D001','D999'), log+'\n'+log.splitlines()[0], @@ -119,7 +119,7 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for broken in [dict(events, bundle_counts=[count-1]), dict(events, module_function_counts={'WrongModule': count}), dict(events, parameter_case_invocations=1)]: - self.assertFalse(check_result(command, 0, '', broken)[0]) + self.assertFalse(check_result(command, 0, evidence, broken)[0]) policy = DISCOVERY for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 57) diff --git a/Sources/JSON/Reading/JSONReadScope.swift b/Sources/JSON/Reading/JSONReadScope.swift index 3fff894..430106b 100644 --- a/Sources/JSON/Reading/JSONReadScope.swift +++ b/Sources/JSON/Reading/JSONReadScope.swift @@ -40,7 +40,7 @@ package final class JSONReadScope { limits: limits, domain: .core, trackAcceptedCapacities: true)) phase = .preparing } - package func runPreparation(_ body: (JSONCodingState) throws -> T) throws -> T { + internal func runPreparation(_ body: (JSONCodingState) throws -> T) throws -> T { do { guard ObjectIdentifier(Thread.current) == owner, phase == .preparing, document == nil, let state else { diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index d3565a5..4347e6a 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:40669ae92e9827800f3faa15d90e9753ccc8000d20f83db0ae4d608793b7cb49" + package static let revision = "sha256:49f19e4c720f2d2c56215634d6e40146d3a67d7d60f67f70e96e9f338a4b4487" package static let kind = "source-sha256" } diff --git a/Sources/JSONInspection/Store/JSONConfiguredStore.swift b/Sources/JSONInspection/Store/JSONConfiguredStore.swift index 604badc..b58a220 100644 --- a/Sources/JSONInspection/Store/JSONConfiguredStore.swift +++ b/Sources/JSONInspection/Store/JSONConfiguredStore.swift @@ -192,7 +192,7 @@ private struct StoreReducer: JSONReadPreparation, JSONTraversalOwnedReducer { try JSONStoreAccounting.check(array.count, accounting.caps.recordsPerCollection, name: "recordsPerCollection", phase: 4, path: location) let sum = try JSONStoreAccounting.sum(UInt64(total), UInt64(array.count)) - guard let next = Int(exactly: sum) else { throw JSONStoreFailure(.counterOverflow, phase: 4, domain: .operation) } + let next = try JSONStoreAccounting.checkedCount(sum, phase: 4) try JSONStoreAccounting.check(next, accounting.caps.records, name: "records", phase: 4, path: location) collections.append(.init(profile: declaration, path: location, start: total, count: array.count)) seen[declaration] = true; total = next diff --git a/Sources/JSONInspection/Store/JSONStoreAccounting.swift b/Sources/JSONInspection/Store/JSONStoreAccounting.swift index 5018c50..afc14fd 100644 --- a/Sources/JSONInspection/Store/JSONStoreAccounting.swift +++ b/Sources/JSONInspection/Store/JSONStoreAccounting.swift @@ -19,6 +19,12 @@ internal struct JSONStoreAccounting { guard !overflow else { throw JSONStoreFailure(.counterOverflow, phase: 0, domain: .operation) } return next } + static func checkedCount(_ value: UInt64, phase: Int) throws -> Int { + guard let result = Int(exactly: value) else { + throw JSONStoreFailure(.counterOverflow, phase: phase, domain: .operation) + } + return result + } static func check(_ count: Int, _ limit: Int, name: String, phase: Int, path: [JSONLocationComponent] = []) throws { guard count <= limit else { diff --git a/Tests/JSONInspectionTests/StoreRuntimeTests.swift b/Tests/JSONInspectionTests/StoreRuntimeTests.swift index 6d6a34f..eebab81 100644 --- a/Tests/JSONInspectionTests/StoreRuntimeTests.swift +++ b/Tests/JSONInspectionTests/StoreRuntimeTests.swift @@ -67,6 +67,15 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON let replaced = try JSONStoreBuilder(document: replacement, profile: store.profile).build().result #expect(replaced.document.sourceMap == nil) #expect(store.document.sourceMap != nil) + let changed = store.document.replacingRoot(try storeRows(0).root) + let changedStore = try JSONStoreBuilder(document:changed,profile:store.profile).build().result + #expect(changedStore.recordCount == 0 && changedStore.document.sourceMap == nil) + #expect(store.recordCount == 1 && store.document.sourceMap != nil) + #expect(try storeRefusal { _ = try changedStore.record(at:record.position) }.code == .invalidPosition) + let emptyKeyStore = try storeBuild("D064").result + let rootPointer = try JSONPointer(parsing:"").resolve(in:emptyKeyStore.document.root) + let emptyKeyPointer = try JSONPointer(parsing:"/").resolve(in:emptyKeyStore.document.root) + #expect(rootPointer == emptyKeyStore.document.root && rootPointer != emptyKeyPointer) print("STORE_RUNTIME R01 passed") } @@ -111,6 +120,8 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON caps[keyPath:key] = needed - 1 let failure = try storeRefusal { _ = try storeBuild(id,caps:caps) } #expect(failure.code == .capacityExceeded && failure.capacity == name) + caps[keyPath:key] = 0 + #expect(try storeRefusal { _ = try storeBuild(id,caps:caps) }.capacity == name) caps[keyPath:key] = -1 #expect(try storeCoreRefusal { _ = try storeBuild(id,caps:caps) }.code == .invalidLimits) caps[keyPath:key] = Int.max @@ -135,6 +146,8 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON else { _ = try JSONStoreAccounting.product(UInt64.max,2) } }.code == .counterOverflow) } + #expect(try JSONStoreAccounting.checkedCount(UInt64(Int.max), phase: 4) == Int.max) + #expect(try storeRefusal { _ = try JSONStoreAccounting.checkedCount(UInt64.max, phase: 4) }.code == .counterOverflow) var capped = JSONStoreCaps(); capped.referenceSlots = 0 #expect(try storeRefusal { _ = try storeBuild("D024",caps:capped) }.capacity == "referenceSlots") capped.referenceSlots = 1 @@ -176,10 +189,12 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON let document = try JSONParser().parseDocument(Data(text.utf8)) let profile = storeSimpleProfile(fields:[.init(key:"friend",presence:.required,nullable:false)]) for targetPhase in [1,2,4,5,6,7,8] { + let lifetime = Mutex(false) let task = Task { let state = Mutex<(phase:Int,work:UInt64,start:UInt64?)>((0,0,nil)) var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 var builder = JSONStoreBuilder(document:document,profile:profile,limits:limits) + builder.lifetimeObserver = { root,state in lifetime.withLock { $0 = root && state } } builder.phaseObserver = { phase in state.withLock { $0.phase = phase if phase == targetPhase && $0.start == nil { $0.start = $0.work } @@ -195,7 +210,32 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON } do { _ = try await task.value; Issue.record("Expected phase \(targetPhase) cancellation") } catch is CancellationError { } + #expect(lifetime.withLock { $0 }) + print("STORE_CANCELLATION phase=\(targetPhase) passed invalidated=1") } + // Root-only source: the final phase-5 work is the long duplicate comparison, + // with no later source-map scan. Calibrate exact work, never elapsed time. + let sameID = JSON.object(try JSONObject(members:[.init("uuid",value:.string(long))])) + let duplicateDocument = JSONDocument(root:.object(try JSONObject(members:[.init("collections",value:.object( + try JSONObject(members:[.init("users",value:.array([sameID,sameID]))])))]))) + let progress = Mutex<(phase:Int,last:UInt64)>((0,0)) + var duplicateLimits = JSONLimits.balanced; duplicateLimits[.cancellationQuantum] = 1 + var calibration = JSONStoreBuilder(document:duplicateDocument,profile:storeSimpleProfile(),limits:duplicateLimits) + calibration.phaseObserver = { phase in progress.withLock { $0.phase = phase } } + calibration.checkpointObserver = { work in progress.withLock { if $0.phase == 5 { $0.last = work } } } + #expect(try storeRefusal { _ = try calibration.build() }.code == .duplicateRecordID) + let lastComparison = progress.withLock { $0.last } + #expect(lastComparison > 2) + let duplicateCancellation = Task { + var builder = JSONStoreBuilder(document:duplicateDocument,profile:storeSimpleProfile(),limits:duplicateLimits) + builder.checkpointObserver = { work in + if work == lastComparison - 1 { withUnsafeCurrentTask { $0?.cancel() } } + } + return try builder.build() + } + do { _ = try await duplicateCancellation.value; Issue.record("Expected cancellation inside exact duplicate comparison") } + catch is CancellationError { } + print("STORE_CANCELLATION duplicate_comparison_at=\(lastComparison-1) passed") let after = Task { let finish = Mutex(0) var builder = JSONStoreBuilder(document:document,profile:profile) @@ -227,7 +267,7 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON @Test func storeR07SourceAndStateInvalidatedOnEveryFailureAndSuccess() throws { let facts = Mutex<[(Bool,Bool)]>([]) - for id in ["D002","D046","D004","D055","D024"] { + for id in ["D002","D046","D012","D014","D004","D055","D024"] { let fixture = try storeCase(id) var builder = try JSONStoreBuilder(document:storeInput(fixture),profile:storeProfile(storeMember(fixture,"profile"))) builder.lifetimeObserver = { root,state in facts.withLock { $0.append((root,state)) } } @@ -235,7 +275,22 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON else { _ = try storeRefusal { _ = try builder.build() } } } let result = facts.withLock { $0 } - #expect(result.count == 5 && result.allSatisfy { $0.0 && $0.1 }) + #expect(result.count == 7 && result.allSatisfy { $0.0 && $0.1 }) + let fixture = try storeCase("D038"), source = try storeInput(fixture) + let profile = try storeProfile(storeMember(fixture,"profile")) + let ample = try JSONStoreBuilder(document:source,profile:profile).build() + var short = JSONLimits.balanced; short[.workUnits] = ample.statistics[.workUnits] - 1 + var refusing = JSONStoreBuilder(document:source,profile:profile,limits:short) + refusing.lifetimeObserver = { root,state in facts.withLock { $0.append((root,state)) } } + #expect(try storeCoreRefusal { _ = try refusing.build() }.resource?.metric == .workUnits) + #expect(facts.withLock { $0.count == 8 && $0.allSatisfy { $0.0 && $0.1 } }) + // Exercise report/aliases leaving lexical scope under ASan/LSan; keep only facts. + for _ in 0..<16 { + var scoped = JSONStoreBuilder(document:source,profile:profile) + scoped.lifetimeObserver = { root,state in facts.withLock { $0.append((root,state)) } } + do { let report = try scoped.build(); #expect(report.result.recordCount == 1) } + } + #expect(facts.withLock { $0.count == 24 && $0.allSatisfy { $0.0 && $0.1 } }) print("STORE_RUNTIME R07 passed") } @@ -248,6 +303,14 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON let b = try JSONStoreBuilder(document:document,profile:reversed).build().result #expect(try a.record(collection:0,row:0).path == b.record(collection:0,row:0).path) #expect(try a.record(collection:1,row:0).path == b.record(collection:1,row:0).path) + let container = try #require(document.root.objectValue?["collections"]?.objectValue) + let swapped = JSONDocument(root:.object(try JSONObject(members:[.init("collections",value:.object( + try JSONObject(members:Array(container.reversed()))))]))) + let physical = try JSONStoreBuilder(document:swapped,profile:original).build().result + #expect(try a.record(collection:0,row:0).id == physical.record(collection:1,row:0).id) + #expect(try a.record(collection:0,row:0).path == physical.record(collection:1,row:0).path) + #expect(try a.record(collection:0,row:0).position.collection == 0) + #expect(try physical.record(collection:1,row:0).position.collection == 1) let rows = try storeRows(2) let array = try #require(rows.root.objectValue?["collections"]?.objectValue?["users"]?.arrayValue) let reordered = JSONDocument(root:.object(try JSONObject(members:[.init("collections",value:.object( @@ -258,6 +321,24 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON #expect(try first.record(collection:0,row:0).path != second.record(collection:0,row:1).path) let exact = try storeBuild("D065").result #expect(try JSONComparison.equals(.string(exact.record(collection:0,row:0).id),.string(exact.record(collection:0,row:1).id)) == false) + let linkedProfile = storeSimpleProfile(fields:[.init(key:"friend",presence:.required,nullable:false)]) + let permutations = [ + #"{"collections":{"users":[{"uuid":"u","ordinary":17,"friend":{"collection":"users","id":"u"}}]}}"#, + #"{"collections":{"users":[{"friend":{"id":"u","collection":"users"},"ordinary":17,"uuid":"u"}]}}"#] + for text in permutations { + let view = try JSONStoreBuilder(document:JSONParser().parseDocument(Data(text.utf8)),profile:linkedProfile).build().result + #expect(try view.record(collection:0,row:0).id == "u") + if case .link(let target) = view.references[0].state { #expect(target.collection == 0 && target.row == 0) } + else { Issue.record("Payload/marker permutation changed link") } + } + let orderedFixture = try storeCase("D057"), orderedSource = try storeInput(orderedFixture) + let forward = try storeProfile(storeMember(orderedFixture,"profile")) + let declaration = forward.collections[0] + let reverseFields = JSONStoreProfile(version:.v1,collectionsAt:forward.collectionsAt,collections:[ + .init(name:declaration.name,recordsAt:declaration.recordsAt,idKey:declaration.idKey, + referenceFields:Array(declaration.referenceFields.reversed()))],references:forward.references) + #expect(try storeRefusal { _ = try JSONStoreBuilder(document:orderedSource,profile:forward).build() }.code == .referenceMissing) + #expect(try storeRefusal { _ = try JSONStoreBuilder(document:orderedSource,profile:reverseFields).build() }.code == .referenceNull) print("STORE_RUNTIME R08 passed") } @@ -276,6 +357,15 @@ private func storeRows(_ count: Int, nullReference: Bool = false) throws -> JSON #expect(empty.result.recordCount == 0) _ = try storeRefusal { _ = try JSONStoreBuilder(document:storeRows(1),profile:storeSimpleProfile(),caps:zero).build() } #expect(try storeRefusal { _ = try storeBuild("D055") }.phase == 6) + let valid = JSON.object(try JSONObject(members:[.init("uuid",value:.string("first"))])) + for bad in [JSON.object(try JSONObject(members:[])), + .object(try JSONObject(members:[.init("uuid",value:.null)])), + .object(try JSONObject(members:[.init("uuid",value:.boolean(false))]))] { + let document = JSONDocument(root:.object(try JSONObject(members:[.init("collections",value:.object( + try JSONObject(members:[.init("users",value:.array([valid,bad]))])))]))) + let error = try storeRefusal { _ = try JSONStoreBuilder(document:document,profile:storeSimpleProfile()).build() } + #expect(error.phase == 5 && error.path == [.key("collections"),.key("users"),.index(1),.key("uuid")]) + } print("STORE_RUNTIME R09 passed") } diff --git a/Tests/JSONInspectionTests/StoreVectorTests.swift b/Tests/JSONInspectionTests/StoreVectorTests.swift index 254dcb9..e23f600 100644 --- a/Tests/JSONInspectionTests/StoreVectorTests.swift +++ b/Tests/JSONInspectionTests/StoreVectorTests.swift @@ -69,6 +69,17 @@ func storeTokens(_ path: [JSONLocationComponent]) -> [String] { path.map { switch $0 { case .key(let key): key.string; case .index(let index): String(index) } } } +// Independent public typed-read delegate; never used inside the engine operation. +func storeTypedValue(_ path: [JSONLocationComponent], in document: JSONDocument) throws -> JSON { + try document.read { cursor in + var selected = cursor + for component in path { + switch component { case .key(let key): selected = try selected[key]; case .index(let i): selected = try selected[i] } + } + return try selected.requiredValue() + } +} + private let storeBuildVectorIDs = ["D001", "D002", "D003", "D004", "D005", "D006", "D007", "D008", "D009", "D010", "D011", "D012", "D013", "D014", "D015", "D016", "D017", "D018", "D019", "D020", "D021", "D022", "D023", "D024", "D025", "D026", "D027", "D028", "D029", "D030", "D031", "D032", "D033", "D034", "D035", "D036", "D038", "D039", "D040", "D041", "D042", "D043", "D044", "D045", "D046", "D047", "D048", "D049", "D050", "D052", "D054", "D055", "D056", "D057", "D058", "D059", "D060", "D061", "D062", "D063", "D064", "D065", "D066", "D067", "D068"] @Test(arguments: storeBuildVectorIDs) @@ -104,6 +115,30 @@ func storeD1NativeVector(id: String) throws { let record = try store.record(collection: ci, row: row) let pointer = try JSONPointer(tokens: storeTokens(record.idPath)) #expect(try pointer.resolve(in: document.root) == .string(record.id)) + #expect(try storeTypedValue(record.idPath,in:document) == .string(record.id)) + #expect(try JSONPointer(tokens:storeTokens(record.path)).resolve(in:document.root) == storeTypedValue(record.path,in:document)) + for reference in store.references[record.referenceRange] { + let key = try #require(reference.path.last) + guard case .key(let field) = key else { Issue.record("Reference path must end in an exact key"); continue } + let parent = try storeTypedValue(record.path,in:document) + let presence = try parent.presence(of:field.string) + switch reference.state { + case .absent: + #expect(presence == .absent) + do { _ = try JSONPointer(tokens:storeTokens(reference.path)).resolve(in:document.root); Issue.record("Absent reference resolved") } + catch let error as JSONFailure { #expect(error.category == .pointer && error.code == .missingTarget) } + do { _ = try storeTypedValue(reference.path,in:document); Issue.record("Absent reference read as a value") } + catch let error as JSONAccessFailure { #expect(error.cause.category == .missingValue) } + case .null: + #expect(presence == .null) + #expect(try storeTypedValue(reference.path,in:document) == .null) + #expect(try JSONPointer(tokens:storeTokens(reference.path)).resolve(in:document.root) == .null) + case .link: + let original = try JSONPointer(tokens:storeTokens(reference.path)).resolve(in:document.root) + #expect(presence == .value(original)) + #expect(try storeTypedValue(reference.path,in:document) == original) + } + } } } let admission = try JSONTraversal(document: document).visit { _,_ in .skipDescendants } From d8a854abd811fb59b95e0e52271db57d79771d19 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:07:27 +0200 Subject: [PATCH 20/25] Classify exact scoped filters without hiding full safety inventory --- Scripts/ci.py | 9 +++++---- Scripts/test_qualification_harness.py | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Scripts/ci.py b/Scripts/ci.py index c6e7487..2cd5005 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -249,8 +249,9 @@ def check_result(command, code, log, events=None): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if events is not None: counts = events['bundle_counts'] - store_test = any(x == 'store' or x.startswith('storeR06') for x in command) - prepared_test = any('navigationPreparedD0' in x for x in command) + selected_filter = command[command.index('--filter') + 1] if '--filter' in command else None + store_test = selected_filter in ('store', 'storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth') + prepared_test = selected_filter in ('navigationPreparedD0', 'navigationPreparedD0IndependentConcurrentHealth') if command[:2] == ['swift', 'test']: expected = [DISCOVERY['composition_external_functions']] if 'Tools/CompositionQualification' in command else [12] if 'composition|composed|caughtOperationalFailureCannotComplete' in command else [79] if 'JSONSchemaTests' in command else [4 if 'thread' in command else 98] if 'JSONSchemaExamplesTests' in command else [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 'JSONSchemaExamplesTests' in command and events is not None: @@ -283,7 +284,7 @@ def check_result(command, code, log, events=None): if 'qa' in command and events is not None: passed = passed and events['module_function_counts'] == DISCOVERY['qa_functions'] and check_restored(events) if 'thread' in command and 'JSONSchemaExamplesTests' not in command and not prepared_test and not store_test and events is not None: - passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) and check_store_tests(events, log, shared=True) + passed = passed and events['module_function_counts'] == SHARED_COUNTS and events['parameter_case_invocations'] == 0 and check_projection_cases(events, shared=True) and check_store_tests(events, log, shared=True) if '--filter' not in command and '--test-product' not in command and not any(x.startswith('Tools/') for x in command): if events is not None: passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) and check_store_tests(events, log) @@ -377,7 +378,7 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=19' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=20' in log.splitlines() return passed, counts def main(): diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index aaf5956..dbbe7a5 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -11,7 +11,7 @@ import unittest from coverage import executable_objects, shared_objects, validate_export, without_schema_objects from test_discovery import DISCOVERY, ROOT_COUNTS, PARAMETER_INVOCATIONS, check_root, check_restored, RESTORED_FUNCTIONS -from ci import check_result, commands +from ci import check_result, commands, SHARED_TESTS, SHARED_COUNTS from store_acceptance import FUNCTIONS as STORE_FUNCTIONS, VECTORS, check_store_tests, check_store_consumer from projection_acceptance import EXPECTED as PROJECTION_FUNCTIONS, PUBLIC_CHECKS, check_projection_cases, check_public_projection_receipt @@ -73,6 +73,23 @@ def test_public_projection_receipt_rejects_incomplete_and_incidental_successes(s else: row['work_units'] = 0 self.assertFalse(check_public_projection_receipt(broken)) + def test_full_thread_filter_is_not_misclassified_as_preparation_subset(self): + names = [module+'.fixture'+str(i)+'()' for module,count in SHARED_COUNTS.items() for i in range(count)] + names = [n for n in names if not n.startswith('JSONInspectionTests.')]+[ + 'JSONInspectionTests.inspectionI17CoverageSpansReplacementAndSharedInvocations()', + 'JSONInspectionTests.snapshotS19NavigationParityAndIndependentInvocations()', + 'JSONInspectionTests.snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth()', + 'JSONInspectionTests.projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics()', + 'JSONInspectionTests.storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth()'] + events=dict(function_names=names,module_function_counts=SHARED_COUNTS, + bundle_counts=sorted(SHARED_COUNTS.values()),parameter_case_invocations=0,parameterized_functions={}) + command=['swift','test','--sanitize','thread','--filter',SHARED_TESTS] + self.assertTrue(check_result(command,0,'STORE_RUNTIME R06 passed',events)[0]) + for bad in [dict(events,parameter_case_invocations=1),dict(events,function_names=names[:-1]), + dict(events,module_function_counts=dict(SHARED_COUNTS,JSONNavigationTests=1)), + dict(events,bundle_counts=sorted(SHARED_COUNTS.values())[:-1])]: + self.assertFalse(check_result(command,0,'STORE_RUNTIME R06 passed',bad)[0]) + def test_prepared_entry_rejects_missing_renamed_and_wrong_module_cases(self): command = ['swift', 'test', '--filter', 'navigationPreparedD0'] names = list(DISCOVERY['prepared_entry_function_ids']) @@ -235,7 +252,7 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): identity['function_names'][0] = 'JSONTests.unrelatedReplacement()' self.assertFalse(check_restored(identity)) harness = ['python3', '-B', 'Scripts/test_qualification_harness.py'] - self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=19')[0]) + self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=20')[0]) for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=16', '']: self.assertFalse(check_result(harness, 0, stale)[0]) lifetime = ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py'] From b4874c01342734a877973f5b2b6333699698c1d9 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:29:59 +0200 Subject: [PATCH 21/25] test: scope store fixture ownership for Linux leak checks --- .../StoreVectorTests.swift | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Tests/JSONInspectionTests/StoreVectorTests.swift b/Tests/JSONInspectionTests/StoreVectorTests.swift index e23f600..6c88e21 100644 --- a/Tests/JSONInspectionTests/StoreVectorTests.swift +++ b/Tests/JSONInspectionTests/StoreVectorTests.swift @@ -8,7 +8,9 @@ import JSONNavigation // Test-only typed fixture adapter: no production wire decoder and no store verdict logic. func storeFixture(_ path: String) throws -> Data { let root = try #require(Bundle.module.resourceURL) - return try Data(contentsOf: URL(fileURLWithPath: root.path + "/StoreFixtures/" + path)) + let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: root.path + "/StoreFixtures/" + path)) + defer { try? handle.close() } + return try handle.readToEnd() ?? Data() } func storeMember(_ value: JSON, _ key: String) throws -> JSON { let object = try #require(value.objectValue) @@ -16,6 +18,10 @@ func storeMember(_ value: JSON, _ key: String) throws -> JSON { } func storeText(_ value: JSON) throws -> String { try #require(value.stringValue) } func storeInt(_ value: JSON) throws -> Int { try #require(value.numberValue).integer(as: Int.self) } +func storeBool(_ value: JSON) throws -> Bool { + guard case .boolean(let boolean) = value else { throw JSONFailure(.typeMismatch, code: .wrongKind) } + return boolean +} func storePath(_ value: JSON) throws -> [JSONLocationComponent] { try #require(value.arrayValue).map { part in let object = try #require(part.objectValue) @@ -30,7 +36,7 @@ func storeProfile(_ value: JSON) throws -> JSONStoreProfile { let fields = try #require(try storeMember(declaration,"referenceFields").arrayValue).map { field in JSONStoreReferenceField(key: JSONKey(try storeText(storeMember(field,"key"))), presence: try #require(JSONStoreFieldPresence(rawValue: storeText(storeMember(field,"presence")))), - nullable: try #require(try storeMember(field,"nullable").booleanValue as Bool?)) + nullable: try storeBool(storeMember(field,"nullable"))) } return JSONStoreCollectionProfile(name: JSONKey(try storeText(storeMember(declaration,"name"))), recordsAt: try storePath(storeMember(declaration,"recordsAt")), @@ -41,18 +47,16 @@ func storeProfile(_ value: JSON) throws -> JSONStoreProfile { #expect(mode == "disabled" || mode == "strict") if mode == "strict" { #expect(try storeText(storeMember(refs,"marker")) == JSONStoreMarkerVersion.v1.rawValue) } let references: JSONStoreReferences = mode == "disabled" ? .disabled - : .strict(marker: .v1, validateTargets: try #require(try storeMember(refs,"validateTargets").booleanValue as Bool?)) + : .strict(marker: .v1, validateTargets: try storeBool(storeMember(refs,"validateTargets"))) return JSONStoreProfile(version: .v1, collectionsAt: try storePath(storeMember(value,"collectionsAt")), collections: collections, references: references) } -// Immutable independent input oracle, shared only by the test driver. -private let frozenStoreCases: [JSON] = { - do { - let manifest = try JSONParser(options: .init(limits: .offline)).parse(storeFixture("VECTORS.json")) - return try #require(try storeMember(manifest,"cases").arrayValue) - } catch { fatalError("Required store fixture manifest failed: \(error)") } -}() -func storeManifestCases() throws -> [JSON] { frozenStoreCases } +// Immutable independent input oracle, scoped to each caller. Do not retain the +// parsed JSON in a process-global cache: Linux LSan must observe its teardown. +func storeManifestCases() throws -> [JSON] { + let manifest = try JSONParser(options: .init(limits: .offline)).parse(storeFixture("VECTORS.json")) + return try #require(try storeMember(manifest,"cases").arrayValue) +} func storeCase(_ id: String) throws -> JSON { let cases = try storeManifestCases() return try #require(cases.first { try storeText(storeMember($0,"id")) == id }) From 99dbb457f141ee35c7630fbd81bc7a096dee96d5 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:39:41 +0200 Subject: [PATCH 22/25] Reconcile NAV10 and graph seams for local additive proof only --- AGENTS.md | 5 + .../Architecture/ExtractionManifest.json | 29 +- .../NAV11/APICandidate.md | 48 ++ .../StructuralIntelligence/NAV11/GraphADR.md | 57 ++ .../NAV11/SourceAuthority.md | 22 + Documentation/CI/README.md | 37 + Documentation/Qualification/GraphCases.json | 80 +++ .../Qualification/TestDiscovery.json | 10 +- Package.swift | 2 +- README.md | 25 + Scripts/ci.py | 30 +- Scripts/graph_acceptance.py | 38 + Scripts/qualify_graph.py | 89 +++ Scripts/qualify_graph_context.py | 84 +++ Scripts/qualify_public_graph.py | 111 +++ Scripts/test_qualification_harness.py | 46 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/GraphExperiment.md | 19 + .../Documentation.docc/JSONInspection.md | 11 + .../Documentation.docc/SourceBoundGraphs.md | 62 ++ .../Graph/JSONGraphProfile.swift | 70 ++ .../Graph/JSONGraphProjection.swift | 248 +++++++ .../JSONInspection/Graph/JSONGraphQuery.swift | 89 +++ .../JSONNavigation/JSONTraversalContext.swift | 31 +- .../GraphAcceptanceTests.swift | 382 ++++++++++ .../GraphFixtures/GRAPH-FIXTURE-ORACLE.json | 653 ++++++++++++++++++ .../GraphFixtures/g01-empty.json | 4 + .../GraphFixtures/g02-simple.json | 27 + .../GraphFixtures/g03-unicode-scalars.json | 16 + .../g04-empty-and-special-ids.json | 23 + .../GraphFixtures/g05-duplicate-id.json | 13 + .../g06-equal-payload-distinct-id.json | 17 + .../GraphFixtures/g07-node-id-missing.json | 8 + .../GraphFixtures/g08-node-id-null.json | 8 + .../GraphFixtures/g09-node-id-number.json | 8 + .../g10-node-row-not-object.json | 6 + .../g11-edge-source-missing.json | 12 + .../GraphFixtures/g12-edge-target-null.json | 13 + .../GraphFixtures/g13-edge-target-number.json | 13 + .../g14-edge-row-not-object.json | 10 + .../GraphFixtures/g15-dangling.json | 13 + .../GraphFixtures/g15b-dangling-empty.json | 9 + .../GraphFixtures/g16-duplicate-edge.json | 20 + .../GraphFixtures/g17-reverse-edges.json | 20 + .../GraphFixtures/g18-self-edge.json | 13 + .../GraphFixtures/g19-cycle.json | 27 + .../GraphFixtures/g20-isolate.json | 19 + .../g21-bfs-source-edge-order.json | 30 + .../GraphFixtures/g22-diamond.json | 34 + .../GraphFixtures/g23-numeric-object-key.json | 18 + .../GraphFixtures/g24-overlap.json | 7 + .../GraphFixtures/g25-wrong-nodes-array.json | 4 + .../GraphFixtures/g26-wrong-edges-array.json | 4 + .../GraphFixtures/generate_graph_fixtures.py | 88 +++ .../GraphFixtures/verify_graph_fixtures.py | 16 + .../GraphPublicAcceptanceTests.swift | 158 +++++ Tools/GraphPublicQualification/Package.swift | 4 + .../Sources/GraphPublicConsumer/Main.swift | 137 ++++ 58 files changed, 3056 insertions(+), 23 deletions(-) create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md create mode 100644 Documentation/Qualification/GraphCases.json create mode 100644 Scripts/graph_acceptance.py create mode 100644 Scripts/qualify_graph.py create mode 100644 Scripts/qualify_graph_context.py create mode 100644 Scripts/qualify_public_graph.py create mode 100644 Sources/JSONInspection/Documentation.docc/GraphExperiment.md create mode 100644 Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md create mode 100644 Sources/JSONInspection/Graph/JSONGraphProfile.swift create mode 100644 Sources/JSONInspection/Graph/JSONGraphProjection.swift create mode 100644 Sources/JSONInspection/Graph/JSONGraphQuery.swift create mode 100644 Tests/JSONInspectionTests/GraphAcceptanceTests.swift create mode 100644 Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g01-empty.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g02-simple.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json create mode 100755 Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py create mode 100755 Tests/JSONInspectionTests/GraphFixtures/verify_graph_fixtures.py create mode 100644 Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift create mode 100644 Tools/GraphPublicQualification/Package.swift create mode 100644 Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift diff --git a/AGENTS.md b/AGENTS.md index 9dc1041..830a5e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,3 +46,8 @@ optimizations. Source-attached N3 snapshots and P1 tree/sparse-table projections NAV10 exact structural-set/numeric-matrix views are public candidates in existing JSONInspection, not released. Graph/store engines, retrieval/persistence and JSON text-sequence framing remain deferred. The approved NAV10 promotion adds no product; future merge/release and wider APIs remain human-gated. + +NAV11 directed simple graph has an owner-reviewed public candidate API inside JSONInspection; no new product or automatic merge. +Store/set/matrix/retrieval/persistence and JSON text-sequence framing remain separate deferred trains. + +NAV11 public graph candidate adds only the approved immutable profile, checked source-bound graph and typed failures inside JSONInspection. CSR arrays, reducers and meters remain non-public; no new product or graph serialization/mutation. Future merge/release remain separately human-gated. diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index e8b2f63..4e40d82 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -271,6 +271,18 @@ "attribution": "User-authored packet fixture input; original integration/tests Apache-2.0. Packet source/provenance retained separately.", "validation": "TypedConstructionTests.swift and public G3Acceptance consumer; strict native compiler/evaluator authority", "evidence": "Release1.6/G3 qualification and materialized per-case receipts" + }, + { + "repository": "ThorFuchs/Swift-JSON", + "commit": "1a0306d076a08c8c43281f91412a11cd744ff3f6", + "destination": "Sources/JSONInspection/Graph", + "disposition": "Original Apache-2.0 NAV11 experimental source-backed directed simple graph; no upstream graph code adapted. Canonical Navigation/context and JSON comparison remain authorities. Independent packet fixtures retained with SHA-bound oracle.", + "validation": [ + "Tests/JSONInspectionTests/GraphAcceptanceTests.swift", + "Scripts/qualify_graph_context.py", + "Scripts/qualify_graph.py" + ], + "evidence": "Scripts/ci.py source-bound native/Linux/safety/compiler receipts; private symbols only" } ], "original_implementation_notes": [ @@ -314,5 +326,20 @@ "framework_boundary": "Conditional CoreTransferable/UniformTypeIdentifiers plus a narrow explicit main-actor Apple PDF boundary using WebKit, CoreText, CoreGraphics and AppKit/UIKit paragraph styles. Core JSON imports none of these; no new root dependency.", "rtf": "Original bounded strict-token emitter; AppKit/UIKit native decoding only in tests and qualification consumers, not generation.", "pdf": "Original invocation-owned lifecycle, finite adapter accounting and visible-range pagination using Apple SDK APIs. PDFKit is independent qualification only; no third-party renderer adapted." - } + }, + "original_components": [ + { + "component": "NAV11 owner-reviewed public graph candidate", + "license": "Apache-2.0", + "source": "Original Swift-JSON public access/documentation adapters over qualified NAV11 graph donor PR #67 / 81485930f859131d0802691ca6370f482ba62911", + "paths": [ + "Sources/JSONInspection/Graph", + "Tools/GraphPublicQualification", + "Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift", + "Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md" + ], + "validation": "G01–G32 retained; GP01–GP16 plus Debug/Release/ASan public consumer, 26 codegen negatives and three DocC examples; execution receipts required", + "dependencies": "No external runtime dependencies; existing JSON and JSONNavigation authorities only" + } + ] } diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md new file mode 100644 index 0000000..8055869 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md @@ -0,0 +1,48 @@ +# Graph API proposal for later owner review + +All NAV11 types and operations are **package-only**, not application API. Product +count remains ten, dependencies remain empty, Navigation56/Inspection95 unchanged. +The proposed future public subset, contingent on separate review, is: + +- Immutable `JSONGraphProfile(nodesAt:edgesAt:nodeIDKey:edgeSourceKey:edgeTargetKey:)`. +- `JSONGraphProjection.build(document:profile:limits:maximumNodes:maximumEdges:)` + returning `JSONOperationReport`, defaults 10000/50000. +- Read-only captured document/profile/nodeCount/edgeCount and complete traversal + summary. IDs/CSR buffers are research metadata; promoting their raw arrays is + not proposed. Source-local node/edge ordinals have no cross-view identity. +- `nodeValue(at:limits:)`, `edgeValue(at:limits:)`, `nodeSpan(at:limits:)` and + `outgoingEdges(of:limits:maximumResults:)` with independent finite whole-source + admissions. Returned JSON aliases receive additional owned-value accounting. +- `breadthFirst(fromNode:limits:maximumFrontier:maximumVisited:maximumResults:)` + returning original node ordinals; defaults 10000 each, start included, complete + reachable component. Zero caps refuse a real start; an empty graph has no start. +- Typed semantic errors with original row/field paths; core missing/kind/resource + and cancellation errors stay intact. No partial-success variant exists. + +BFS stores queue history as returned output; frontier means pending entries, while +visited/result caps independently bound total enqueues/output. Graph metadata +aliases, visited flags and queue entries share the query's original traversal +ledger. Scalar-exact linear resolution and O(N²+NE+E²) construction are explicit +costs, not optimized lookup claims. No new JSON identity/path or serialization. + +Actual compiled spelling is in Sources/JSONInspection/Graph. Constructors remain +fileprivate. Compiler probes reject external type access, private initializers, +mutation, internal reducers, and context escape. No public DocC examples imply +these private declarations are available. No merge or promotion is authorized. + +## Public successor review + +The owner approved the compiler-derived 34-declaration public subset for the draft +candidate, subject to qualification. The exact source spelling is authoritative: +public JSONGraphProfile (immutable explicit paths/keys and initializer), +JSONGraphProjection (checked factory, immutable document/profile/traversal/counts +and five independent query operations), and JSONGraphFailure with fourteen typed cases. +No existing public declaration changes. CSR buffers, ID staging, edge records, +reducers and the noncopyable context remain inaccessible externally. The old +public-type-invisibility negatives are replaced by internal builder/query/stage +negatives; initializer, storage, mutation and ownership rejections remain. + +Sixteen public-only scenarios and a distinct-package consumer supplement all +G01–G32 and 27 independently pinned fixtures. Three executable DocC examples and +26 external full-codegen rejections are mandatory. This is signature approval +for a draft, not merge/release approval or a claim of unexecuted platform passes. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md b/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md new file mode 100644 index 0000000..16377f1 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md @@ -0,0 +1,57 @@ +# NAV11 directed simple graph experiment + +Package-only JSONInspection research, based on merged Navigation/Inspection; +no NAV10, new product, public symbol or graph JSON model. One original +`JSONTraversal.inspectOwned` invocation selects `[nodesAt, edgesAt]` with +`.rejectAmbiguous` and depth one. All strict root selections and overlap checks +precede callbacks; nodes are validated before edges, in their physical order. + +Negative capacities are configuration failures before admission. Existing core +precedence is retained: complete source admission precedes strict selection, +overlap and root-kind checks. No separate unmetered profile scan or second ledger +is added to move those checks ahead of admission. Missing roots preserve core +`missingMember`/`missingTarget`; wrong root kinds preserve typed core access errors. +Paths and explicit keys are immutable. Reusing a field spelling across roles is +intentional and valid (an edge's single field may explicitly name both ends of a +self-edge); no field inference or silent repair occurs. + +The exclusive context adds mutating delegates only: direct string member selection, +scalar-exact string comparison and matching alias release. JSONObject.lookup and +JSONComparison.textEquals remain authorities. Missing, null and wrong kind are +reported separately; strings own 32+UTF8 logical bytes until released or handed off. +Profile keys are explicitly key-admitted on that meter even for empty arrays; +the preexisting retainKey delegate gains an opt-in caller-input admission flag, +with its original default behavior/counters preserved. +No cursor, scope, ledger or noncopyable capability leaves the reducer. Original +2-positive/20-negative codegen tests are unchanged; helper escape probes and runtime +parity run separately in Debug and Release. A compiler crash blocks implementation. + +Graph metadata is source-local: first row ordinals, exact ID aliases, endpoint +ordinals and CSR offsets/edge ordinals in original edge order. Original source is +transferred once after complete validation/finalization. Defaults are 10,000 nodes, +50,000 edges; each independently configurable finite cap is inclusive and still +subject to all 49 core metrics. Linear exact scans may cost O(N²+NE+E²). + +Later value/span/catalogue/BFS calls start explicit independent full-source +operations. BFS has a head-index queue, visited-at-enqueue and source-edge order. +Defaults: 10,000 frontier entries, visited nodes and returned nodes. Queue history +remains charged until handoff; frontier capacity means live pending entries. +Results certify the complete reachable component including the start, not global +reachability. Failure/cancellation returns no partial result. + +Logical policy V1: 256 fixed builder bytes, 32+UTF8 per node ID alias, +16 per node record, 24 per edge record, 8 per offset and adjacency reference, +plus exact profile keys/paths and the original source. Queries admit graph metadata +aliases, queue/output slots (8 each), visited flags (1 each) and their source; +returned JSON aliases additionally receive core owned-retention accounting. +These are logical quotas, not allocator/RSS or caller-instruction guarantees. + +No public promotion, merge, release, timing or speed claim. Controlled performance +remains HOLD. Store, persistence, graph editing, weights, algorithms other than BFS, +query/CLI/HTML/network adapters and NAV10 batching remain separate work. + +BFS entry precedence is negative capacities, node bounds, whole source/metadata +admission, then enqueue's frontier/visited/result ceilings. Work/cancellation that +is observed first takes precedence. Edge rows validate source field then target +field before resolving source then target IDs, then compare ordered pairs. All +node rows precede all edge rows; a late duplicate node outranks a dangling edge. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md b/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md new file mode 100644 index 0000000..62504b6 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md @@ -0,0 +1,22 @@ +# NAV11 source authority + +Base main: 1a0306d076a08c8c43281f91412a11cd744ff3f6. NAV10 PR66/d566f28 is +independent and not a source dependency. Stable 1.6.1/e431f15 remains immutable. + +| Graph work | Existing delegate | Invocation boundary | +| --- | --- | --- | +| Complete root/source/map admission and failures | JSONReadScope, JSONAdmission, JSONWorkMeter | Original inspectOwned scope | +| Physical/virtual roots and exact overlap | JSONTraversal.execute, rejectAmbiguous | Same original scope, before callbacks | +| Row locations and bounded paths | JSONTraversalHit, JSONLocationComponent, context.appending | Same scope | +| Direct string member/presence/kind | JSONObject.lookup through mutating context delegate | No reader/new ledger | +| ID equality | JSONComparison.textEquals through context.equalStrings | Scalar-exact charged UTF8 comparison | +| Owned alias accounting, cancellation, terminal health | context.perform + scope.run/check | Original state, no escape | +| Adjacency and BFS | Package-only graph reducers | Checked work/retention in that invocation | +| Completed source handoff | context.transferDocumentOwnership | Once after complete valid construction | +| Later source value/span | JSONReadScope.select and sourceMap.span | Explicit new whole-source operation | +| Returned value alias | JSONAdmission.value(owned:true) | Same later-read ledger, additional output cost | + +No JSON, Pointer, value equality, schema, stream, Patch, HTML or CLI semantic change. +The new context methods are package-only and mutating; original ownership codegen +fixtures are byte-identical. ExtractionManifest records original Apache-2.0 code +and independent fixture provenance. Public symbol graphs must remain unchanged. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 0779ff8..c28010b 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,13 @@ +# Local NAV10 + NAV11 scratch seam (not a merge-qualified graph candidate) + +This task-owned composite retains all NAV10 and graph source/test identities. Expected +union: 178 Inspection, 881 native / 865 Linux / 777 WASI functions; shared scopes +56 native / 55 Linux; unchanged 325/288 parameter counts and 49 caps/126 maintenance +identities. API union would be 1,239 (1,205 NAV10 plus graph34), subject to a fresh +compiler graph audit. The 22 harness tests retain both parents' assertions. +These are scratch expectations, not full platform/CI results. Neither PR #68 nor +store refs have been updated. G4 remains HOLD. + # NAV9 Branch B experimental qualification Adds exactly46 package-only set/matrix/core functions: 825 native /809 Linux / @@ -17,6 +27,19 @@ public graph stays ten products/nine libraries, empty root dependencies and Navigation56/Inspection95 declarations. Graph/store have no runtime qualification. Branch A owns G4; no Branch B timing result or speed claim is inherited. +# NAV11 experimental graph qualification + +The package graph/public API stays unchanged: nine libraries plus CLI; Navigation56 +and Inspection95. Graph symbols are package-only. Exact discovery is 108 Inspection +functions including G01–G32, with five Inspection sharing functions. All previous +identities, 49 core cap probes and 126 maintenance witnesses remain required. Native +and Linux add the graph context and package-boundary codegen/runtime qualifiers; +the original two-positive/twenty-negative ownership matrix is unchanged. Exact +identities and genuine primary diagnostics are checked, including crash rejection. + +The current source-bound receipts establish outcomes; the dated sections below +retain their original source scopes. Controlled performance remains HOLD. + # NAV8 Navigation V1 integration Current required discovery retains 779 native /763 Linux /675 WASI functions, @@ -337,3 +360,17 @@ For an ineligible timing host, `--lane wasm --skip-host-measurements` explicitly records optional host samples as NOT RUN. It preserves every runtime/corpus/cap/ strict/ABI gate and the default lane's timing protocol. This flag grants no G4 PASS and produces no performance evidence. + +## NAV11 public graph candidate + +The owner-reviewed successor keeps all 32 original graph identities and adds 16 +public scenarios (124 Inspection functions). Required roots become 827 native, +811 Linux and 723 WASI, with unchanged 325/288 parameter invocations. Shared-state +expectations become 53 native/52 Linux. The public graph qualifier executes 16 +consumer cases in Debug/Release/ASan, three guide blocks/configuration and 26 +ordinary external full-codegen rejections. The original graph boundary matrix +retains 18 negatives, replacing only obsolete invisible-public-type cases with +internal builder/query/stage access rejections. Reducer 2+/20-, helper 2+/8-, all +49 core caps and 126 maintenance identities remain. Harness cardinality is 19. +These are requirements; actual source-bound receipts establish results. No merge +or release follows automatically. Controlled G4 remains HOLD without speed claims. diff --git a/Documentation/Qualification/GraphCases.json b/Documentation/Qualification/GraphCases.json new file mode 100644 index 0000000..2caae38 --- /dev/null +++ b/Documentation/Qualification/GraphCases.json @@ -0,0 +1,80 @@ +{ + "schema_version": 1, + "status": "public API candidate; owner review and full current-source qualification required", + "functions": [ + "JSONInspectionTests.graphG01EmptySourceAndNoValidStart()", + "JSONInspectionTests.graphG02IndependentOracleAndSourceOrder()", + "JSONInspectionTests.graphG03ScalarDistinctIDs()", + "JSONInspectionTests.graphG04SpecialIDsAreNotPaths()", + "JSONInspectionTests.graphG05DuplicateNodeLocations()", + "JSONInspectionTests.graphG06EqualPayloadsPreserveRows()", + "JSONInspectionTests.graphG07MissingNodeID()", + "JSONInspectionTests.graphG08NullNodeID()", + "JSONInspectionTests.graphG09WrongNodeIDKinds()", + "JSONInspectionTests.graphG10NonobjectNode()", + "JSONInspectionTests.graphG11MissingEdgeSource()", + "JSONInspectionTests.graphG12NullEdgeTarget()", + "JSONInspectionTests.graphG13WrongEndpointKind()", + "JSONInspectionTests.graphG14NonobjectEdge()", + "JSONInspectionTests.graphG15DanglingAndEmpty()", + "JSONInspectionTests.graphG16DuplicateOrderedEdge()", + "JSONInspectionTests.graphG17ReverseEdgesAreDistinct()", + "JSONInspectionTests.graphG18SelfEdgeIsVisitedOnce()", + "JSONInspectionTests.graphG19CycleTerminates()", + "JSONInspectionTests.graphG20IsolatesRemainSourceNodes()", + "JSONInspectionTests.graphG21SourceEdgeNeighborOrder()", + "JSONInspectionTests.graphG22DiamondEnqueuesOnce()", + "JSONInspectionTests.graphG23TypedPathsAndPointerDifferential()", + "JSONInspectionTests.graphG24OverlapAndDuplicateRootsFailBeforeRows()", + "JSONInspectionTests.graphG25WholeSourceAdmissionAndFreshQueries()", + "JSONInspectionTests.graphG26InclusiveWorkAndRetention()", + "JSONInspectionTests.graphG27EveryCapacityAndCheckedOverflow()", + "JSONInspectionTests.graphG28CancellationStopsBuildAndBFS()", + "JSONInspectionTests.graphG29FailurePrecedenceAndTerminalLatch()", + "JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()", + "JSONInspectionTests.graphG31CapturedRootAndSourceMapCoherence()", + "JSONInspectionTests.graphG32CheckedMetadataAndExistingPublicSemantics()" + ], + "fixture_cases": 27, + "context_positive_codegen": 2, + "context_negative_codegen": 8, + "graph_positive_codegen": 2, + "graph_negative_codegen": 18, + "public_functions": [ + "JSONInspectionTests.graphGP01OptInSurface()", + "JSONInspectionTests.graphGP02TypedRootAndEmptyKeys()", + "JSONInspectionTests.graphGP03SourceOrderBFS()", + "JSONInspectionTests.graphGP04SelfLoopAndCycle()", + "JSONInspectionTests.graphGP05DuplicateIDPaths()", + "JSONInspectionTests.graphGP06ScalarDistinctIDs()", + "JSONInspectionTests.graphGP07OrderedPairs()", + "JSONInspectionTests.graphGP08DanglingEndpoint()", + "JSONInspectionTests.graphGP09MissingNullWrongKind()", + "JSONInspectionTests.graphGP10BoundsAndZeroCaps()", + "JSONInspectionTests.graphGP11IndependentWholeSourceBudget()", + "JSONInspectionTests.graphGP12OwnedValueAndSourceHandoff()", + "JSONInspectionTests.graphGP13CancellationAndHealth()", + "JSONInspectionTests.graphGP14ConcurrentQueries()", + "JSONInspectionTests.graphGP15SourceMapCoherence()", + "JSONInspectionTests.graphGP16UnselectedAdmission()" + ], + "public_consumer_checks": [ + "GP01", + "GP02", + "GP03", + "GP04", + "GP05", + "GP06", + "GP07", + "GP08", + "GP09", + "GP10", + "GP11", + "GP12", + "GP13", + "GP14", + "GP15", + "GP16" + ], + "public_negative_codegen": 26 +} diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 05fe805..eeb6c32 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 130 + "JSONInspectionTests": 178 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -35,7 +35,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 7 + "JSONInspectionTests": 9 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -76,7 +76,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 130 + "JSONInspectionTests": 178 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,7 +89,7 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 130 + "JSONInspectionTests": 178 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -103,7 +103,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 7 + "JSONInspectionTests": 9 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/Package.swift b/Package.swift index 6bfad3f..6b14abe 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( // Opt-in N1 product candidate; promotion does not change core navigation semantics. .target(name: "JSONNavigation", dependencies: ["JSON"]), .target(name: "JSONInspection", dependencies: ["JSON", "JSONNavigation"]), - .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"], resources: [.copy("ProjectionFixtures"), .copy("P2Fixtures")]), + .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"], resources: [.copy("ProjectionFixtures"), .copy("P2Fixtures"), .copy("GraphFixtures")]), .testTarget(name: "JSONNavigationTests", dependencies: ["JSONNavigation", "JSON"], exclude: ["Fixtures"]), .target(name: "JSONBridges", dependencies: ["JSON"]), .testTarget(name: "JSONIntegrationTests", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch"]), diff --git a/README.md b/README.md index 9f30161..1b34d7e 100644 --- a/README.md +++ b/README.md @@ -731,3 +731,28 @@ limits and an operation report. These are structural views; no set algebra, matr arithmetic or performance claim is included. See the [compiled public guide](Sources/JSONInspection/Documentation.docc/ExactProjections.md), [independent consumer](Tools/ExactProjectionQualification) and [public boundary](Documentation/Architecture/StructuralIntelligence/NAV10/PublicAPI.md). Batch access remains explicitly deferred to NAV13. Stable 1.6.1 is unchanged. + +NAV11 experiments with a package-only, exact source-backed directed simple graph +inside JSONInspection. Graph construction and BFS are not public API and are not +part of stable 1.6.1. The [experimental contract](Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md) +records finite capacities, source/error precedence and one-ledger ownership. There +is no graph product, performance claim or automatic promotion. + +## Source-bound graph candidate (unreleased) + +This development candidate extends JSONInspection with an explicit directed simple +graph view of two disjoint physical arrays. An immutable `JSONGraphProfile` names +exact typed paths and direct string ID/endpoint keys. Checked construction rejects +duplicate scalar-exact IDs, dangling endpoints and repeated directed pairs; self +edges, cycles, reverse edges and isolates remain valid data. BFS returns the complete +reachable component in source-edge order or throws. Node/edge values and original +node spans stay attached to the captured source. + +Each later read or query has its own explicit finite whole-source admission. +Build defaults to 10,000 nodes/50,000 edges; BFS defaults to 10,000 frontier, +visited and result entries. Storage and reducers stay private. Construction can +require quadratic work, and repeated reads re-admit the source. Logical retained +bytes are not RSS; controlled G4 performance remains HOLD with no speed claim. +See the [compiled graph guide](Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md) +and [standalone public consumer](Tools/GraphPublicQualification). This candidate +adds no product and does not establish a published release or merge approval. diff --git a/Scripts/ci.py b/Scripts/ci.py index 266ad50..511e455 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -6,6 +6,7 @@ from preflight import collect, collect_linux from testing_events import read_events, ROOT_PARAMETER_CASES from p2_acceptance import check_p2_cases, check_borrow_receipt, check_private_receipt, check_exact_cases, check_exact_receipt +from graph_acceptance import check_graph_cases, check_graph_receipt, check_public_graph_receipt from projection_acceptance import check_projection_cases, check_public_projection_receipt from test_discovery import DISCOVERY, ROOT_COUNTS, SHARED_COUNTS, PARAMETER_INVOCATIONS, check_restored @@ -20,6 +21,7 @@ SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics' SHARED_TESTS += '|exactProjectionPUB07SharedPublicViewsKeepIndependentHealth' SHARED_TESTS += '|p2SetSSET18SharedImmutableViewsHaveIndependentLedgers|p2MatrixMAT18SharedTransposesHaveIndependentReads' +SHARED_TESTS += '|graphG30ConcurrentViewsHaveIndependentHealth|graphGP14ConcurrentQueries' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -30,12 +32,15 @@ def commands(lane, output): output = output.relative_to(ROOT) if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], - ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB'], - ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB'], - ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB'], + ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG'], + ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG'], ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/inspection-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], + ['python3', '-B', 'Scripts/qualify_graph_context.py', '--output', str(output / 'graph-context')], + ['python3', '-B', 'Scripts/qualify_graph.py', '--output', str(output / 'graph-compiler')], + ['python3', '-B', 'Scripts/qualify_public_graph.py', '--output', str(output / 'public-graph-consumer')], ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/verify_p2_fixtures.py'], ['python3', '-B', 'Scripts/qualify_p2_borrowing.py', '--output', str(output / 'p2-borrowing')], @@ -185,6 +190,9 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_exact_projections.py', '--output', str(output / 'exact-projection-consumer')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], + ['python3', '-B', 'Scripts/qualify_graph_context.py', '--output', str(output / 'graph-context')], + ['python3', '-B', 'Scripts/qualify_graph.py', '--output', str(output / 'graph-compiler')], + ['python3', '-B', 'Scripts/qualify_public_graph.py', '--output', str(output / 'public-graph-consumer')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -244,20 +252,20 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 - if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|p2|exactProjectionPUB']): + if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG']): expected = [DISCOVERY['root_functions']['JSONInspectionTests']] if events is not None: - passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 and check_projection_cases(events) and check_p2_cases(events) and check_exact_cases(events) + passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 and check_projection_cases(events) and check_p2_cases(events) and check_exact_cases(events) and check_graph_cases(events) passed = passed and sorted(counts) == expected if 'Tools/CompositionQualification' in command and events is not None: passed = passed and events['module_function_counts'] == {'CompositionScaleTests': DISCOVERY['composition_external_functions']} and events['parameter_case_invocations'] == 0 if 'qa' in command and events is not None: passed = passed and events['module_function_counts'] == DISCOVERY['qa_functions'] and check_restored(events) if 'thread' in command and 'JSONSchemaExamplesTests' not in command and events is not None: - passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) and check_p2_cases(events, shared=True) and check_exact_cases(events, shared=True) + passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) and check_p2_cases(events, shared=True) and check_exact_cases(events, shared=True) and check_graph_cases(events, shared=True) if '--filter' not in command and '--test-product' not in command and not any(x.startswith('Tools/') for x in command): if events is not None: - passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) and check_p2_cases(events) and check_exact_cases(events) + passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) and check_p2_cases(events) and check_exact_cases(events) and check_graph_cases(events) passed = passed and (events['parameterized_functions'] == ROOT_PARAMETER_CASES if events is not None else sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log)) == PARAMETER_INVOCATIONS) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) @@ -316,6 +324,12 @@ 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_public_graph.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_public_graph_receipt(data) + if command[:3] in (['python3', '-B', 'Scripts/qualify_graph_context.py'], ['python3', '-B', 'Scripts/qualify_graph.py']): + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_graph_receipt(data, context='qualify_graph_context.py' in command[2]) if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_snapshot.py']: @@ -351,7 +365,7 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=20' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=22' in log.splitlines() return passed, counts def main(): diff --git a/Scripts/graph_acceptance.py b/Scripts/graph_acceptance.py new file mode 100644 index 0000000..42f721f --- /dev/null +++ b/Scripts/graph_acceptance.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Exact graph identities and genuine compiler outcomes; no broad exit-zero gate.""" +import json,pathlib +ROOT=pathlib.Path(__file__).resolve().parents[1] +POLICY=json.loads((ROOT/'Documentation/Qualification/GraphCases.json').read_text()) +EXPECTED=frozenset(POLICY['functions']) | frozenset(POLICY['public_functions']) +SHARED=frozenset({'JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()', 'JSONInspectionTests.graphGP14ConcurrentQueries()'}) +def check_graph_cases(events,shared=False): + names=[n for n in events['function_names'] if n.startswith('JSONInspectionTests.graphG')] + return len(names)==len(set(names)) and set(names)==(SHARED if shared else EXPECTED) +def check_graph_receipt(data,context=False): + commands=data.get('commands',[]);expected_count=18 if context else 28;negative_count=8 if context else 18 + if data.get('status')!='passed' or len(commands)!=expected_count or len({c.get('name') for c in commands})!=expected_count:return False + negatives=[c for c in commands if c.get('expected_diagnostic')] + if len(negatives)!=negative_count:return False + for c in commands: + if c.get('status')!='passed' or c.get('compiler_crash') is not False:return False + if c.get('expected_diagnostic'): + if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'].lower() in d.lower() for d in c.get('primary_diagnostics',[])):return False + elif c.get('exit_code')!=0:return False + return True + +PUBLIC_CHECKS = frozenset(POLICY['public_consumer_checks']) +def check_public_graph_receipt(data): + commands=data.get('commands',[]);results=data.get('consumer_results',{}) + if data.get('status')!='passed' or len(commands)!=45 or len({c.get('name') for c in commands})!=45 or set(results)!={'debug','release','address'}:return False + negatives=[c for c in commands if c.get('expected_diagnostic')] + if len(negatives)!=26:return False + for c in commands: + if c.get('status')!='passed' or c.get('compiler_crash') is not False:return False + if c.get('expected_diagnostic'): + if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'] in d for d in c.get('primary_diagnostics',[])):return False + elif c.get('exit_code')!=0:return False + for row in results.values(): + names=row.get('cases',[]) + if len(names)!=16 or len(set(names))!=16 or set(names)!=PUBLIC_CHECKS:return False + if not all(isinstance(row.get(k),int) and row[k]>0 for k in ['work_units','logical_retained_bytes','peak_logical_retained_bytes','admitted_nodes']):return False + return True diff --git a/Scripts/qualify_graph.py b/Scripts/qualify_graph.py new file mode 100644 index 0000000..f111fa5 --- /dev/null +++ b/Scripts/qualify_graph.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Full-codegen proof of graph invariants and non-public storage/reducer boundaries.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT = pathlib.Path(__file__).resolve().parents[1] +POSITIVE = r'''import JSON +import JSONNavigation +import JSONInspection +@main struct GraphConsumer { + static func check(_ value: Bool) { precondition(value) } + static func main() throws { + let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let document = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"é","n":9007199254740993},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#)) + let built = try JSONGraphProjection.build(document: document, profile: profile) + check(built.result.nodeCount == 2 && built.result.edgeCount == 1) + print("PRIVATE_GRAPH exact passed") + check(try built.result.breadthFirst(fromNode: 0).result == [0,1]) + print("PRIVATE_GRAPH bfs passed") + check(try built.result.outgoingEdges(of: 0).result == [0]) + print("PRIVATE_GRAPH adjacency passed") + check(try built.result.nodeValue(at: 0).result.objectValue?["n"]?.numberValue?.integer(as: UInt64.self) == 9007199254740993) + print("PRIVATE_GRAPH values passed") + let baseline = try JSONTraversal(document: document).visit { _,_ in .continue } + check(built.statistics[.nodes] == baseline.statistics[.nodes] && built.statistics[.retainedBytes] > 0) + print("PRIVATE_GRAPH admission passed") + } +} +''' +PROBES = [ + ('positive', POSITIVE, None, False), + ('external-builder', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find', True), + ('external-query', 'import JSONInspection\nlet x = GraphQuery.self\n', 'cannot find', True), + ('external-stage', 'import JSONInspection\nlet x = GraphWorkStage.self\n', 'cannot find', True), + ('external-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find', True), + ('external-context', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find', True), + ('core-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find', True), + ('immutable-ids', 'import JSONInspection\nfunc f(_ x: inout JSONGraphProjection) { x.nodeIDs.removeAll() }\n', "'nodeIDs' is a 'let' constant", False), + ('forged-graph', 'import JSON\nimport JSONNavigation\nimport JSONInspection\nfunc f(_ d: JSONDocument, _ p: JSONGraphProfile, _ s: JSONTraversalSummary) { _ = JSONGraphProjection(document: d, profile: p, nodeIDs: [], edges: [], offsets: [], adjacency: [], traversal: s) }\n', 'initializer is inaccessible', False), + ('private-reducer', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find', False), +] +CHECKS = {'exact', 'bfs', 'adjacency', 'values', 'admission'} + +def main(): + parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--output', required=True) + out = ROOT / parser.parse_args().output; out.mkdir(parents=True, exist_ok=True) + receipt = dict(status='running', scope='same-package graph proof; graph public surface plus private storage/reducers; same-package invariant proof', + source_inputs=[dict(path=p.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files()], commands=[]) + def save(): (out / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr; diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + crash = result.returncode < 0 or any(s in text for s in ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation', 'Found ownership error']) + passed = not crash and (result.returncode == 0 if expected is None else result.returncode > 0 and any(expected.lower() in d.lower() for d in diagnostics)) + log = out / (name + '.log'); log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], exit_code=result.returncode, + status='passed' if passed else 'failed', compiler_crash=crash, expected_diagnostic=expected, + primary_diagnostics=diagnostics, log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: raise ValueError('Unexpected graph compiler outcome: ' + name) + return result.stdout + try: + identity = re.sub(r'[^a-zA-Z0-9_]', '_', ROOT.name.lower()) + if not identity or identity[0].isdigit(): identity = '_' + identity + for config in ['debug', 'release']: + run(config + '-build', ['swift','build','-c',config,'--target','JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift','build','-c',config,'--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + common = ['swiftc','-swift-version','6','-warnings-as-errors','-I',str(modules),'-Onone' if config == 'debug' else '-O','-parse-as-library'] + for name, text, diagnostic, external in PROBES: + source = out / (name + '.swift'); source.write_text(text) + run(config + '-' + name, common + ['-package-name','graph_external' if external else identity, + '-module-name','GraphCompilerProbe','-c',str(source),'-o',str(out / (config + '-' + name + '.o'))], diagnostic) + objects = [] + for module in ['JSON','JSONNavigation','JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: raise ValueError('Missing objects: ' + module) + objects.extend(str(p) for p in selected) + binary = out / ('private-' + config) + run(config + '-link', common + ['-package-name',identity,str(out/'positive.swift'),*objects,'-o',str(binary)]) + output = run(config + '-run', [str(binary)]) + checks = re.findall(r'^PRIVATE_GRAPH (\S+) passed$', output, re.M) + if len(checks) != 5 or set(checks) != CHECKS: raise ValueError('Private projection consumer identity/count drift') + receipt['status'] = 'passed' + except (ValueError, OSError, subprocess.TimeoutExpired) as error: receipt.update(status='failed', error=str(error)) + save(); print('GRAPH_COMPILER ' + receipt['status'] + ' commands=28 positive_codegen=2 negative_codegen=18 private_cases=5/config') + return 0 if receipt['status'] == 'passed' else 1 +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_graph_context.py b/Scripts/qualify_graph_context.py new file mode 100644 index 0000000..13619df --- /dev/null +++ b/Scripts/qualify_graph_context.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Code-generation and original-meter proof for the graph's exclusive delegates.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT=pathlib.Path(__file__).resolve().parents[1] +POSITIVE='''import JSON +import JSONNavigation +struct Probe: JSONTraversalOwnedReducer { + var strings: [String] = [] + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { + if hit.relativeDepth == 0 { try context.retainKey("id", admittingCallerInput: true); try context.release(2); _ = try context.arrayCount(value, at: hit.absoluteLocation); return .continue } + let field = try context.directStringMember(value, key: "id", at: hit.absoluteLocation) + precondition(field.kind == .string) + if let text = field.text { let equal = try context.equalStrings(text, text); precondition(equal); strings.append(text) } + return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> Int { + for text in strings { try context.releaseStringAlias(text) } + try context.work(7); return strings.count + } +} +@main struct Main { + static func main() throws { + let document = JSONDocument(root: try JSON.parse(#"{"n":[{"id":"é"}],"e":[{"id":"e\\u0301"}]}"#)) + let walk = JSONTraversal(document: document) + let roots: JSONTraversalScope = .virtual(roots: [[.key("n")],[.key("e")]], overlap: .rejectAmbiguous) + let base = try walk.visit(scope: roots, options: .init(depthLimit: 1)) { _,_ in .continue } + var reducer = Probe() + let owned = try walk.inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &reducer) + precondition(owned.result == 2) + precondition(owned.statistics[.nodes] == base.statistics[.nodes]) + precondition(owned.statistics[.workUnits] > base.statistics[.workUnits]) + precondition(owned.statistics[.retainedBytes] == base.statistics[.retainedBytes]) + precondition(reducer.strings.count == 2) + var exact = Probe(); var limits = JSONLimits.balanced + limits[.workUnits] = owned.statistics[.workUnits] + _ = try JSONTraversal(document: document, limits: limits).inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &exact) + limits[.workUnits] -= 1 + do { var short = Probe(); _ = try JSONTraversal(document: document, limits: limits).inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &short); preconditionFailure() } + catch let error as JSONFailure { precondition(error.category == .resource) } + print("GRAPH_CONTEXT parity passed nodes=\\(owned.statistics[.nodes]) work=\\(owned.statistics[.workUnits]) retained=\\(owned.statistics[.retainedBytes])") + } +} +''' +PROBES={ + 'positive':(POSITIVE,None), + 'bound':('func f(_ c: inout JSONTraversalContext) { _ = c.directStringMember }', "cannot reference 'mutating' method as function value"), + 'escape':('func f(_ c: inout JSONTraversalContext) -> () throws -> Bool { { try c.equalStrings("a", "a") } }', "escaping closure captures 'inout' parameter"), + 'task':('func f(_ c: inout JSONTraversalContext) { _ = Task { try c.releaseStringAlias("a") } }', "escaping closure captures 'inout' parameter"), + 'store':('final class Box { var c: JSONTraversalContext? }; func f(_ c: inout JSONTraversalContext) -> Box { let b = Box(); b.c = consume c; return b }', 'missing reinitialization of inout parameter'), +} +def main(): + ap=argparse.ArgumentParser();ap.add_argument('--output',required=True);out=ROOT/ap.parse_args().output;out.mkdir(parents=True,exist_ok=True) + receipt={'status':'running','source_inputs':[{'path':p.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()],'commands':[]} + def save(): (out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + def run(name,args,expected=None): + p=subprocess.run(args,cwd=ROOT,capture_output=True,text=True,timeout=900);data=p.stdout+p.stderr + diagnostics=re.findall(r': error: ([^\n]+)',data);crash=p.returncode<0 or any(x in data for x in ['Stack dump:','Assertion failed','Found ownership error','fatal error encountered during compilation']) + passed=not crash and (p.returncode==0 if expected is None else p.returncode>0 and any(expected.lower() in d.lower() for d in diagnostics)) + log=out/(name+'.log');log.write_text(data) + receipt['commands'].append({'name':name,'command':args,'exit_code':p.returncode,'status':'passed' if passed else 'failed','expected_diagnostic':expected,'primary_diagnostics':diagnostics,'compiler_crash':crash,'log':log.relative_to(ROOT).as_posix(),'log_sha256':hashlib.sha256(log.read_bytes()).hexdigest()});save() + if not passed: raise ValueError(name) + return p.stdout + try: + identity=re.sub('[^a-zA-Z0-9_]','_',ROOT.name.lower()) + if identity[0].isdigit():identity='_'+identity + for config in ['debug','release']: + run(config+'-build',['swift','build','-c',config,'--target','JSONNavigation']) + bp=pathlib.Path(run(config+'-binpath',['swift','build','-c',config,'--show-bin-path']).strip());mods=bp/'Modules' if (bp/'Modules').is_dir() else bp + common=['swiftc','-swift-version','6','-warnings-as-errors','-package-name',identity,'-I',str(mods),'-O' if config=='release' else '-Onone','-parse-as-library'] + for name,(body,expected) in PROBES.items(): + f=out/(name+'.swift');f.write_text(body if name=='positive' else 'import JSON\nimport JSONNavigation\n'+body+'\n') + run(config+'-'+name,common+['-c',str(f),'-o',str(out/(config+'-'+name+'.o'))],expected) + objects=[] + for module in ['JSON','JSONNavigation']: + aggregate=bp/(module+'.o');objects+= [str(aggregate)] if aggregate.exists() else [str(p) for p in sorted((bp/(module+'.build')).glob('*.o'))] + exe=out/('parity-'+config);run(config+'-link',common+[str(out/'positive.swift'),*objects,'-o',str(exe)]) + output=run(config+'-run',[str(exe)]) + if len(re.findall(r'^GRAPH_CONTEXT parity passed nodes=\d+ work=\d+ retained=\d+$',output,re.M))!=1:raise ValueError('Missing parity witness') + receipt['status']='passed' + except (ValueError,OSError,subprocess.TimeoutExpired) as e:receipt.update(status='failed',error=str(e)) + save();print('GRAPH_CONTEXT '+receipt['status']+' positive=2 negative=8 runtime=2');return 0 if receipt['status']=='passed' else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/qualify_public_graph.py b/Scripts/qualify_public_graph.py new file mode 100644 index 0000000..f2abd0f --- /dev/null +++ b/Scripts/qualify_public_graph.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Qualify public graph imports, semantics and three executable DocC examples.""" +import argparse +import hashlib +import json +import pathlib +import re +import subprocess +import sys +from archive_candidate import files + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKS = {'GP%02d' % i for i in range(1,17)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + output = ROOT / parser.parse_args().output + output.mkdir(parents=True, exist_ok=True) + shared = (ROOT / 'Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift').read_text().split('\n@Test')[0].replace('import Testing\n', '') + actual = (ROOT / 'Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift').read_text() + if actual.split('\n@main struct')[0] != shared: + raise ValueError('Public consumer scenario bytes differ from the sixteen pinned root cases') + inputs = [{'path': p.relative_to(ROOT).as_posix(), + 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()] + receipt = dict(status='running', source_inputs=inputs, commands=[], consumer_results={}) + + def save(): + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr + crash = result.returncode < 0 or any(x in text for x in + ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation']) + diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + passed = not crash and (result.returncode == 0 if expected is None else + result.returncode > 0 and any(expected in d for d in diagnostics)) + log = output / (name + '.log') + log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], + exit_code=result.returncode, status='passed' if passed else 'failed', compiler_crash=crash, + expected_diagnostic=expected, primary_diagnostics=diagnostics, + log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: + raise ValueError('Unexpected compiler/consumer outcome: ' + name) + return result.stdout + + def consumer(name, command): + text = run(name, command) + checks = re.findall(r'^GRAPH_PUBLIC_CHECK (\S+) passed$', text, re.M) + if len(checks) != 16 or set(checks) != CHECKS or 'GRAPH_PUBLIC_CONSUMER passed cases=16' not in text: + raise ValueError('Missing/duplicate graph consumer checks: ' + name) + resource = re.search(r'^GRAPH_PUBLIC_RESOURCE work=(\d+) retained=(\d+) peak=(\d+) nodes=(\d+)$', text, re.M) + if resource is None or not all(int(x) > 0 for x in resource.groups()): + raise ValueError('Missing finite cumulative report: ' + name) + receipt['consumer_results'][name] = dict(cases=checks, work_units=int(resource[1]), + logical_retained_bytes=int(resource[2]), peak_logical_retained_bytes=int(resource[3]), + admitted_nodes=int(resource[4])) + save() + + try: + for config in ['debug', 'release']: + run(config + '-build', ['swift', 'build', '-c', config, '--target', 'JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift', 'build', '-c', config, '--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + objects = [] + for module in ['JSON', 'JSONNavigation', 'JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: + raise ValueError('Missing built objects: ' + module) + objects.extend(str(p) for p in selected) + common = ['swiftc', '-swift-version', '6', '-warnings-as-errors', '-package-name', + 'graph_external_consumer', '-I', str(modules), + '-O' if config == 'release' else '-Onone'] + consumer(config, ['swift', 'run', '-c', config, '--package-path', + 'Tools/GraphPublicQualification', 'GraphPublicConsumer']) + guide = ROOT / 'Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md' + fences = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(fences) != 3: + raise ValueError('Changed three-fence guide inventory') + for index, fence in enumerate(fences): + file = output / ('guide-' + str(index) + '.swift') + file.write_text('import Foundation\nimport JSON\nimport JSONNavigation\nimport JSONInspection\n' + '@main struct Guide { static func main() throws {\n' + fence + '\n} }\n') + binary = output / ('guide-' + config + '-' + str(index)) + run(config + '-guide-' + str(index), common + ['-parse-as-library', str(file), *objects, '-o', str(binary)]) + run(config + '-guide-' + str(index) + '-run', [str(binary)]) + probes = [('core-import-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find'), ('scope-isolation', 'import JSON\nlet x = JSONReadScope.self\n', 'cannot find'), ('capability-isolation', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find'), ('forged-graph', 'import JSON\nimport JSONNavigation\nimport JSONInspection\nfunc f(_ d: JSONDocument, _ p: JSONGraphProfile, _ s: JSONTraversalSummary) { _ = JSONGraphProjection(document: d, profile: p, nodeIDs: [], edges: [], offsets: [], adjacency: [], traversal: s) }\n', 'initializer is inaccessible'), ('immutable-profile', 'import JSONInspection\nfunc f(_ p: inout JSONGraphProfile) { p.nodesAt.removeAll() }\n', "'let' constant"), ('immutable-count', 'import JSONInspection\nfunc f(_ p: inout JSONGraphProjection) { p.nodeCount = 0 }\n', 'get-only property'), ('private-ids', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.nodeIDs }\n', 'is inaccessible'), ('private-edges', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.edges }\n', 'is inaccessible'), ('private-offsets', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.offsets }\n', 'is inaccessible'), ('private-adjacency', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.adjacency }\n', 'is inaccessible'), ('private-builder', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find'), ('private-query', 'import JSONInspection\nlet x = GraphQuery.self\n', 'cannot find'), ('private-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find')] + for name, source, expected in probes: + file = output / (name + '.swift'); file.write_text(source) + run(config + '-' + name, common + ['-parse-as-library', '-c', str(file), + '-o', str(output / (config + '-' + name + '.o'))], expected) + consumer('address', ['swift', 'run', '-c', 'release', '--sanitize', 'address', + '--scratch-path', '.build/graph-public-consumer-address', '--package-path', + 'Tools/GraphPublicQualification', 'GraphPublicConsumer']) + receipt['status'] = 'passed' + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + receipt.update(status='failed', error=str(error)) + save() + print('PUBLIC_GRAPH_COMPILER ' + receipt['status'] + + ' configurations=2 consumer_cases=16 address_cases=16 guide_fences=3 negative_probes=26') + return 0 if receipt['status'] == 'passed' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index eff39db..1bf0030 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -14,6 +14,7 @@ from ci import check_result, commands from p2_acceptance import PUBLIC_FUNCTIONS, PUBLIC_SHARED, check_exact_cases, check_exact_receipt from p2_acceptance import EXPECTED as P2_FUNCTIONS, SHARED as P2_SHARED, BORROW_CHECKS, check_p2_cases, check_borrow_receipt, check_private_receipt +from graph_acceptance import EXPECTED as GRAPH_FUNCTIONS, check_graph_cases, check_graph_receipt, check_public_graph_receipt, PUBLIC_CHECKS as GRAPH_PUBLIC_CHECKS from projection_acceptance import EXPECTED as PROJECTION_FUNCTIONS, PUBLIC_CHECKS, check_projection_cases, check_public_projection_receipt class HarnessTests(unittest.TestCase): @@ -63,11 +64,11 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel # Combined-source checks must retain both independently owned optional products. for module, count, command in [ ('JSONNavigationTests', 47, ['swift', 'test', '--filter', 'navigation']), - ('JSONInspectionTests', 130, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB']), + ('JSONInspectionTests', 178, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, - parameter_case_invocations=0, function_names=sorted(PROJECTION_FUNCTIONS | P2_FUNCTIONS | PUBLIC_FUNCTIONS) if module == 'JSONInspectionTests' else []) + parameter_case_invocations=0, function_names=sorted(PROJECTION_FUNCTIONS | P2_FUNCTIONS | PUBLIC_FUNCTIONS | GRAPH_FUNCTIONS) if module == 'JSONInspectionTests' else []) self.assertTrue(check_result(command, 0, '', events)[0]) for broken in [dict(events, bundle_counts=[count-1]), dict(events, module_function_counts={'WrongModule': count}), @@ -77,12 +78,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 47) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONInspectionTests'], 130) + self.assertEqual(policy[key]['JSONInspectionTests'], 178) self.assertEqual(policy[key]['JSONIntegrationTests'], 32) for key in ('shared_state_functions', 'portable_shared_state_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 1) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 4) - self.assertEqual(policy[key]['JSONInspectionTests'], 7) + self.assertEqual(policy[key]['JSONInspectionTests'], 9) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): @@ -149,6 +150,39 @@ def test_public_exact_identities_and_receipts_are_fail_closed(self): self.assertFalse(check_exact_cases({'function_names': broken})) self.assertTrue(check_exact_cases({'function_names': sorted(PUBLIC_SHARED)}, shared=True)) self.assertFalse(check_exact_receipt({'status': 'passed', 'commands': [], 'consumer_results': {}})) + def test_graph_identity_and_compiler_receipts_reject_false_success(self): + names=sorted(GRAPH_FUNCTIONS) + self.assertEqual(len(names),48) + self.assertTrue(check_graph_cases({'function_names':names})) + for broken in [names[:-1],names+names[:1],['WrongModule.'+names[0].split('.',1)[1]]+names[1:]]: + self.assertFalse(check_graph_cases({'function_names':broken})) + for context,count,negatives in [(True,18,8),(False,28,18)]: + commands=[dict(name=str(n),status='passed',compiler_crash=False,exit_code=1 if n; storage and reducers remain non-public. No release is implied. diff --git a/Sources/JSONInspection/Documentation.docc/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md index a645228..e4a99f4 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -46,3 +46,14 @@ or numeric matrix. Sampling does not exempt any source from admission. - ``JSONStructuralSetProjection`` - ``JSONNumericMatrixProjection`` - ``JSONP2ProjectionFailure`` + +### Experimental scope + +- + +### Source-bound directed graphs + +- +- ``JSONGraphProfile`` +- ``JSONGraphProjection`` +- ``JSONGraphFailure`` diff --git a/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md b/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md new file mode 100644 index 0000000..47c7de7 --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md @@ -0,0 +1,62 @@ +# Build a source-bound directed graph + +An explicit profile names two disjoint physical arrays and their direct string fields. +Build validates every row and publishes only a complete directed simple graph. +ID and field-key identity uses exact decoded Unicode scalars. Arrays retain source +order. Duplicate IDs, dangling endpoints and repeated directed pairs throw; reverse +edges, self-edges, cycles and isolated nodes are valid data. + +## Capture the complete source + +```swift +let graphSource = try JSONParser().parseDocument(Data(#"{"nodes":[{"id":"A","n":9007199254740993},{"id":"B"}],"edges":[{"from":"A","to":"B"}]}"#.utf8)) +let graphProfile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +let graphBuild = try JSONGraphProjection.build(document: graphSource, profile: graphProfile) +precondition(graphBuild.result.nodeCount == 2 && graphBuild.result.edgeCount == 1) +precondition(graphBuild.statistics[.workUnits] > 0) +``` + +## Traverse a complete reachable component + +```swift +let graph = try JSONGraphProjection.build(document: .init(root: JSON.parse(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#)), profile: .init(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to")).result +let outgoing = try graph.outgoingEdges(of: 0).result +let reachable = try graph.breadthFirst(fromNode: 0).result +precondition(outgoing == [0]) +precondition(reachable == [0, 1]) +``` + +## Preserve semantic failures + +```swift +let duplicate = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#)) +let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +do { + _ = try JSONGraphProjection.build(document: duplicate, profile: profile) + preconditionFailure("Duplicate ID was accepted") +} catch JSONGraphFailure.duplicateNodeID(let first, let second) { + precondition(first == [.key("nodes"), .index(0)]) + precondition(second == [.key("nodes"), .index(1)]) +} +``` + +Every later value, span, outgoing-edge or BFS query is a new independent whole-source +admission under the supplied finite limits. Returned JSON aliases remain charged. +No constructor budget is silently reused. Build defaults to 10,000 nodes and 50,000 +edges; outgoing queries default to 50,000 results; BFS defaults to 10,000 live pending +nodes, 10,000 visited nodes and 10,000 results. All caps are inclusive. Zero caps +refuse nonempty work; cancellation, quotas and typed semantic failures throw without +successful partial artifacts. Missing, null and wrong kind remain distinguishable. +Core selection errors retain their typed cause; operational errors are not graph facts. + +Ordinals name positions in this immutable captured source, not durable graph IDs. +There is no replacement-source argument. Replacing a document root drops obsolete +source spans; an existing graph keeps its original source and map. Node spans can be +nil for a source without a map. Storage, reducers and mutable meters are private. + +BFS uses source-edge order and visited-at-enqueue, including the start. Its output +certifies the reachable component, not global connectivity, acyclicity or schema validity. +Exact construction uses bounded linear scans and can cost quadratic work; repeated +queries re-admit the whole source. Logical retention is not RSS. Controlled G4 +performance remains HOLD, with no speed claim. No new product, graph serializer, +mutation, topology, weighted edges or persistent identity is supplied. diff --git a/Sources/JSONInspection/Graph/JSONGraphProfile.swift b/Sources/JSONInspection/Graph/JSONGraphProfile.swift new file mode 100644 index 0000000..efcd0cb --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphProfile.swift @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Explicit physical arrays and direct string fields for a source-bound directed simple graph. +/// +/// Paths use exact typed components; roles are never guessed. Explicit field reuse is valid. +public struct JSONGraphProfile: Sendable { + /// Absolute typed location of the complete node array. + public let nodesAt: [JSONLocationComponent] + /// Absolute typed location of the complete edge array, disjoint from the node array. + public let edgesAt: [JSONLocationComponent] + /// Exact direct key of each node’s required string identifier. + public let nodeIDKey: JSONKey + /// Exact direct key of each edge’s required source string identifier. + public let edgeSourceKey: JSONKey + /// Exact direct key of each edge’s required target string identifier. + public let edgeTargetKey: JSONKey + /// Defines roles without interpreting path strings, normalizing keys or validating a source. + /// - Parameters: + /// - nodesAt: Absolute typed node-array location. + /// - edgesAt: Absolute typed edge-array location; build rejects overlap. + /// - nodeIDKey: Direct required string ID key in node rows. + /// - edgeSourceKey: Direct required string source ID key in edge rows. + /// - edgeTargetKey: Direct required string target ID key in edge rows. + public init(nodesAt: [JSONLocationComponent], edgesAt: [JSONLocationComponent], + nodeIDKey: JSONKey, edgeSourceKey: JSONKey, edgeTargetKey: JSONKey) { + self.nodesAt = nodesAt; self.edgesAt = edgesAt; self.nodeIDKey = nodeIDKey + self.edgeSourceKey = edgeSourceKey; self.edgeTargetKey = edgeTargetKey + } +} + +/// Graph semantic refusals retain exact original locations, never normalized IDs. +public enum JSONGraphFailure: Error, Sendable, Equatable { + /// The node array exceeds the explicit inclusive node cap. + case nodeLimitExceeded(limit: Int) + /// The edge array exceeds the explicit inclusive edge cap. + case edgeLimitExceeded(limit: Int) + /// An original node or edge row is not an object. + case rowNotObject(at: [JSONLocationComponent], kind: JSONTraversalKind) + /// A required direct ID or endpoint field is absent. + case idMissing(at: [JSONLocationComponent]) + /// A required direct ID or endpoint field is explicitly null. + case idNull(at: [JSONLocationComponent]) + /// A required direct ID or endpoint field is not a string. + case idWrongKind(at: [JSONLocationComponent], kind: JSONTraversalKind) + /// Two node IDs have identical decoded scalar sequences; both row paths are retained. + case duplicateNodeID(first: [JSONLocationComponent], second: [JSONLocationComponent]) + /// An edge endpoint has no exact matching node ID; its field path is retained. + case danglingEndpoint(at: [JSONLocationComponent]) + /// Two original edge rows have the same ordered endpoint pair. + case duplicateDirectedEdge(first: [JSONLocationComponent], second: [JSONLocationComponent]) + /// The requested node ordinal is outside this captured graph. + case nodeOutOfBounds(Int) + /// The requested edge ordinal is outside this captured graph. + case edgeOutOfBounds(Int) + /// BFS would exceed its live pending-node frontier cap. + case frontierLimitExceeded(limit: Int) + /// BFS would exceed its total visited-node cap. + case visitedLimitExceeded(limit: Int) + /// A query would exceed its complete-result cap. + case resultLimitExceeded(limit: Int) +} + +/// Trusted row ordinals in this captured source, not durable graph identifiers. +package struct JSONGraphEdgeRecord: Sendable, Equatable { + package let row: Int + package let source: Int + package let target: Int +} diff --git a/Sources/JSONInspection/Graph/JSONGraphProjection.swift b/Sources/JSONInspection/Graph/JSONGraphProjection.swift new file mode 100644 index 0000000..316044c --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphProjection.swift @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// An immutable directed simple graph attached to one completely admitted source document. +/// +/// Build validates every node, edge and adjacency slot before publication. Source-local +/// ordinals preserve physical row order; they are not durable or cross-graph identities. +/// Later queries independently admit the whole captured source under their explicit limits. +/// Construction uses bounded exact scans and may require quadratic work. +public struct JSONGraphProjection: Sendable { + /// The original captured source, including its coherent source map when supplied. + public let document: JSONDocument + /// The explicit immutable selection and field roles validated at build. + public let profile: JSONGraphProfile + package let nodeIDs: [String] + package let edges: [JSONGraphEdgeRecord] + package let offsets: [Int] + package let adjacency: [Int] + /// Complete construction coverage for both selected arrays. + public let traversal: JSONTraversalSummary + /// Number of completely validated original node rows. + public var nodeCount: Int { nodeIDs.count } + /// Number of completely validated original edge rows. + public var edgeCount: Int { edges.count } + fileprivate init(document: JSONDocument, profile: JSONGraphProfile, nodeIDs: [String], + edges: [JSONGraphEdgeRecord], offsets: [Int], adjacency: [Int], + traversal: JSONTraversalSummary) { + self.document = document; self.profile = profile; self.nodeIDs = nodeIDs + self.edges = edges; self.offsets = offsets; self.adjacency = adjacency; self.traversal = traversal + } + + /// Complete source admission, nodes, edges and adjacency share one original meter. + /// - Parameters: + /// - document: Immutable complete source; unselected subtrees are admitted too. + /// - profile: Strict disjoint typed array roots and exact direct string field keys. + /// - limits: One cumulative finite budget for source, validation, metadata and transfer. + /// - maximumNodes: Inclusive node-row cap; defaults to 10,000. + /// - maximumEdges: Inclusive edge-row cap; defaults to 50,000. + /// - Returns: A complete source-owning graph and its construction statistics. + /// - Throws: Typed graph/core selection errors, resource failure or cancellation; never a partial graph. + public static func build(document: JSONDocument, profile: JSONGraphProfile, + limits: JSONLimits = .balanced, maximumNodes: Int = 10_000, + maximumEdges: Int = 50_000) throws -> JSONOperationReport { + guard maximumNodes >= 0, maximumEdges >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + var reducer = GraphBuilder(profile: profile, limits: limits, maximumNodes: maximumNodes, maximumEdges: maximumEdges) + return try JSONTraversal(document: document, limits: limits).inspectOwned( + scope: .virtual(roots: [profile.nodesAt, profile.edgesAt], overlap: .rejectAmbiguous), + options: .init(depthLimit: 1), reducer: &reducer) + } + + /// A new whole-source operation; returns original physical outgoing edge ordinals. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: New independent whole-source and query budget. + /// - maximumResults: Inclusive returned edge-ordinal cap; defaults to 50,000. + /// - Returns: Every outgoing original edge-row ordinal in source order, or throws. + public func outgoingEdges(of node: Int, limits: JSONLimits = .balanced, maximumResults: Int = 50_000) + throws -> JSONOperationReport<[Int]> { + guard maximumResults >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + try checkNode(node) + var reducer = GraphQuery(graph: self, query: .outgoing(node), limits: limits, + maximumFrontier: 0, maximumVisited: 0, maximumResults: maximumResults) + return try JSONTraversal(document: document, limits: limits) + .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) + } + + /// Complete reachable component, including start; never a partial success. + /// Visited-at-enqueue and source-edge order make cycles/self-edges deterministic. + /// Queue history remains owned output; the frontier cap counts only pending nodes. + /// - Parameters: + /// - node: Original start node ordinal, included in the result. + /// - limits: New independent whole-source, metadata and traversal budget. + /// - maximumFrontier: Inclusive live pending-node cap; defaults to 10,000. + /// - maximumVisited: Inclusive total enqueued-node cap; defaults to 10,000. + /// - maximumResults: Inclusive returned node-ordinal cap; defaults to 10,000. + /// - Returns: The complete reachable component in BFS order, or throws without partial success. + public func breadthFirst(fromNode node: Int, limits: JSONLimits = .balanced, + maximumFrontier: Int = 10_000, maximumVisited: Int = 10_000, + maximumResults: Int = 10_000) throws -> JSONOperationReport<[Int]> { + guard maximumFrontier >= 0, maximumVisited >= 0, maximumResults >= 0 else { + throw JSONFailure(.configuration, code: .invalidLimits) + } + try checkNode(node) + var reducer = GraphQuery(graph: self, query: .breadthFirst(node), limits: limits, + maximumFrontier: maximumFrontier, maximumVisited: maximumVisited, + maximumResults: maximumResults) + return try JSONTraversal(document: document, limits: limits) + .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) + } + + /// Returns an original node row under a new whole-source admission and owned-output charge. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: Independent finite source/selection/output budget. + public func nodeValue(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + try checkNode(node) + return try value(at: profile.nodesAt, row: node, limits: limits) + } + /// Returns an original edge row under a new whole-source admission and owned-output charge. + /// - Parameters: + /// - edge: Original edge-row ordinal in this graph. + /// - limits: Independent finite source/selection/output budget. + public func edgeValue(at edge: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + guard edge >= 0, edge < edgeCount else { throw JSONGraphFailure.edgeOutOfBounds(edge) } + return try value(at: profile.edgesAt, row: edge, limits: limits) + } + /// Source-map lookup under a new explicit full-source admission. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: Independent finite source and source-map lookup budget. + /// - Returns: The original value span, or nil when this source has no matching map entry. + public func nodeSpan(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + try checkNode(node) + let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } + let state = try scope.check(); let path = try rowPath(profile.nodesAt, row: node, scope: scope, state: state) + let span = try document.sourceMap?.span(at: path.components, kind: .value, meter: &state.meter) + return try scope.complete(span) + } + private func checkNode(_ node: Int) throws { + guard node >= 0, node < nodeCount else { throw JSONGraphFailure.nodeOutOfBounds(node) } + } + private func value(at root: [JSONLocationComponent], row: Int, limits: JSONLimits) throws -> JSONOperationReport { + let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } + let state = try scope.check(); let path = try rowPath(root, row: row, scope: scope, state: state) + let value = try scope.run(path.components) { try scope.select(path.components, state: $0) } + // A returned JSON alias remains owned through handoff, independently of input. + try JSONAdmission.value(value, meter: &state.meter, owned: true) + return try scope.complete(value) + } + private func rowPath(_ root: [JSONLocationComponent], row: Int, scope: JSONReadScope, + state: JSONCodingState) throws -> JSONReadPath { + var path = JSONReadPath(scope: scope, components: [], bytes: 0) + for part in root { try state.meter.work(); path = try scope.append(part, to: path.components, state: state) } + return try scope.append(.index(row), to: path.components, state: state) + } +} + +internal enum GraphWorkStage: Sendable, Equatable { case nodeIdentity, endpointIdentity, edgePair, adjacency, breadthFirst } + +internal struct GraphBuilder: JSONTraversalOwnedReducer { + /// Qualification-only phase marker; no scope/capability is exposed. + var workObserver: (@Sendable (GraphWorkStage) -> Void)? = nil + let profile: JSONGraphProfile + let limits: JSONLimits + let maximumNodes: Int + let maximumEdges: Int + var ids: [String] = [] + var edges: [JSONGraphEdgeRecord] = [] + var nodeTotal = 0, edgeTotal = 0 + + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work() + if hit.relativeDepth == 0 { + let count = try context.arrayCount(value, at: hit.absoluteLocation) + if hit.scopeRootIndex == 0 { + guard count <= maximumNodes else { throw JSONGraphFailure.nodeLimitExceeded(limit: maximumNodes) } + nodeTotal = count + _ = try ProjectionAccounting.next(count, limits: limits) + try context.retain(256) + _ = try context.retainLocation(profile.nodesAt); _ = try context.retainLocation(profile.edgesAt) + for key in [profile.nodeIDKey, profile.edgeSourceKey, profile.edgeTargetKey] { + try context.work(); try context.retain(32); try context.retainKey(key, admittingCallerInput: true) + } + } else { + guard count <= maximumEdges else { throw JSONGraphFailure.edgeLimitExceeded(limit: maximumEdges) } + edgeTotal = count + } + return .continue + } + guard case .object = value else { throw JSONGraphFailure.rowNotObject(at: try context.retainLocation(hit.absoluteLocation), kind: hit.kind) } + if hit.scopeRootIndex == 0 { + let id = try string(value, key: profile.nodeIDKey, at: hit.absoluteLocation, context: &context) + for prior in ids.indices { + try context.work() + workObserver?(.nodeIdentity) + if try context.equalStrings(ids[prior], id) { + throw JSONGraphFailure.duplicateNodeID( + first: try context.appending(.index(prior), to: profile.nodesAt), + second: try context.retainLocation(hit.absoluteLocation)) + } + } + _ = try ProjectionAccounting.next(ids.count, limits: limits) + try context.retain(16); ids.append(id) + } else { + // Required fields are checked source then target before resolving either. + let source = try string(value, key: profile.edgeSourceKey, at: hit.absoluteLocation, context: &context) + let target = try string(value, key: profile.edgeTargetKey, at: hit.absoluteLocation, context: &context) + let from = try resolve(source, key: profile.edgeSourceKey, at: hit.absoluteLocation, context: &context) + let to = try resolve(target, key: profile.edgeTargetKey, at: hit.absoluteLocation, context: &context) + for prior in edges.indices { + try context.work() + workObserver?(.edgePair) + if edges[prior].source == from, edges[prior].target == to { + throw JSONGraphFailure.duplicateDirectedEdge( + first: try context.appending(.index(prior), to: profile.edgesAt), + second: try context.retainLocation(hit.absoluteLocation)) + } + } + _ = try ProjectionAccounting.next(edges.count, limits: limits) + try context.retain(24); edges.append(.init(row: edges.count, source: from, target: to)) + try context.releaseStringAlias(source); try context.releaseStringAlias(target) + } + return .skipDescendants + } + private func string(_ value: JSON, key: JSONKey, at row: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> String { + let member = try context.directStringMember(value, key: key, at: row) + if let text = member.text { return text } + let path = try context.appending(.key(key), to: row) + guard let kind = member.kind else { throw JSONGraphFailure.idMissing(at: path) } + if kind == .null { throw JSONGraphFailure.idNull(at: path) } + throw JSONGraphFailure.idWrongKind(at: path, kind: kind) + } + private func resolve(_ text: String, key: JSONKey, at row: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> Int { + for index in ids.indices { + try context.work() + workObserver?(.endpointIdentity) + if try context.equalStrings(ids[index], text) { return index } + } + throw JSONGraphFailure.danglingEndpoint(at: try context.appending(.key(key), to: row)) + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> JSONGraphProjection { + guard ids.count == nodeTotal, edges.count == edgeTotal else { throw JSONFailure(.configuration, code: .invalidOperation) } + var offsets: [Int] = [], adjacency: [Int] = [] + try context.work(); try context.retain(8); offsets.append(0) + // Deterministic source-ordered CSR, deliberately bounded linear scans. + for node in ids.indices { + try context.work() + for edge in edges.indices { + workObserver?(.adjacency) + try context.work() + if edges[edge].source == node { + _ = try ProjectionAccounting.next(adjacency.count, limits: limits) + try context.retain(8); adjacency.append(edge) + } + } + _ = try ProjectionAccounting.next(offsets.count, limits: limits) + try context.retain(8); offsets.append(adjacency.count) + } + try context.work() + let document = try context.transferDocumentOwnership() + return .init(document: document, profile: profile, nodeIDs: ids, edges: edges, + offsets: offsets, adjacency: adjacency, traversal: summary) + } +} diff --git a/Sources/JSONInspection/Graph/JSONGraphQuery.swift b/Sources/JSONInspection/Graph/JSONGraphQuery.swift new file mode 100644 index 0000000..e23bc48 --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphQuery.swift @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +internal struct GraphQuery: JSONTraversalOwnedReducer { + var workObserver: (@Sendable (GraphWorkStage) -> Void)? = nil + enum Query { case outgoing(Int), breadthFirst(Int) } + let graph: JSONGraphProjection + let query: Query + let limits: JSONLimits + let maximumFrontier: Int + let maximumVisited: Int + let maximumResults: Int + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work(); return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> [Int] { + let aliases = try retainAliases(context: &context) + var result: [Int] = [] + switch query { + case .outgoing(let node): + for position in graph.offsets[node].. UInt64 { + var total: UInt64 = 0 + func retain(_ bytes: UInt64, context: inout JSONTraversalContext) throws { + try context.retain(bytes) + // Each successful retain is bounded below UInt64.max by the core. + total += bytes + } + try context.work(); try retain(256, context: &context) + for path in [graph.profile.nodesAt, graph.profile.edgesAt] { + for part in path { + try context.work(); try retain(32, context: &context) + if case .key(let key) = part { + for _ in key.string.utf8 { try context.work(); try retain(1, context: &context) } + } + } + } + for key in [graph.profile.nodeIDKey, graph.profile.edgeSourceKey, graph.profile.edgeTargetKey] { + try context.work(); try retain(32, context: &context) + for _ in key.string.utf8 { try context.work(); try retain(1, context: &context) } + } + for id in graph.nodeIDs { + try context.work(); try retain(48, context: &context) + for _ in id.utf8 { try context.work(); try retain(1, context: &context) } + } + for _ in graph.edges { try context.work(); try retain(24, context: &context) } + for _ in graph.offsets { try context.work(); try retain(8, context: &context) } + for _ in graph.adjacency { try context.work(); try retain(8, context: &context) } + return total + } +} diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift index f827241..c8b9162 100644 --- a/Sources/JSONNavigation/JSONTraversalContext.swift +++ b/Sources/JSONNavigation/JSONTraversalContext.swift @@ -49,6 +49,34 @@ package struct JSONTraversalContext: ~Copyable { return try JSONComparison.equals(array[lhs], array[rhs], meter: &state.meter) } } + /// Scalar-exact comparison charged to the original admitted scope. + package mutating func equalStrings(_ lhs: String, _ rhs: String) throws -> Bool { + try perform { try JSONComparison.textEquals(lhs, rhs, meter: &$0.meter) } + } + /// Direct-member string selection. Nil kind means absent; null and other + /// kinds remain distinguishable. A returned string owns 32+UTF8 logical bytes. + /// No JSONKey hashing, second admission, cursor or ledger is created. + package mutating func directStringMember(_ value: JSON, key: JSONKey, + at path: [JSONLocationComponent]) throws + -> (text: String?, kind: JSONTraversalKind?) { + try perform(at: path) { state in + try JSONAdmission.text(key.string, key: true, meter: &state.meter) + try state.meter.work() + guard case .object(let object) = value else { throw JSONFailure(.typeMismatch, code: .wrongKind) } + guard let member = try object.lookup(key.string, meter: &state.meter) else { return (nil, nil) } + guard case .string(let text) = member else { return (nil, JSONTraversalKind(member)) } + try state.meter.retain(32) + for _ in text.utf8 { try state.meter.work(); try state.meter.retain(1) } + return (text, .string) + } + } + /// Relinquish a directStringMember alias after its last owned use. + package mutating func releaseStringAlias(_ text: String) throws { + try perform { state in + for _ in text.utf8 { try state.meter.work(); try state.meter.release(1) } + try state.meter.release(32) + } + } package mutating func arrayCount(_ value: JSON, at path: [JSONLocationComponent]) throws -> Int { try perform(at: path) { state in try state.meter.work() @@ -57,8 +85,9 @@ package struct JSONTraversalContext: ~Copyable { } } /// Reserve an additional owned alias, not another text admission or key model. - package mutating func retainKey(_ key: JSONKey) throws { + package mutating func retainKey(_ key: JSONKey, admittingCallerInput: Bool = false) throws { try perform { state in + if admittingCallerInput { try JSONAdmission.text(key.string, key: true, meter: &state.meter) } for _ in key.string.utf8 { try state.meter.work(); try state.meter.retain(1) } } } diff --git a/Tests/JSONInspectionTests/GraphAcceptanceTests.swift b/Tests/JSONInspectionTests/GraphAcceptanceTests.swift new file mode 100644 index 0000000..f98d2e3 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphAcceptanceTests.swift @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +@testable import JSONInspection + +private let graphProfile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], + nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +private func graphDocument(_ name: String) throws -> JSONDocument { + // Match the existing P1 loader: WASI Foundation named lookup truncates extensions. + let root = try #require(Bundle.module.resourceURL) + let url = URL(fileURLWithPath: root.path + "/GraphFixtures/" + name + ".json") + return try JSONParser().parseDocument(Data(contentsOf: url)) +} +private func graphBuild(_ name: String, profile: JSONGraphProfile = graphProfile) throws -> JSONOperationReport { + try JSONGraphProjection.build(document: graphDocument(name), profile: profile) +} +private func graphFailure(_ body: () throws -> Void) -> JSONGraphFailure? { + do { try body(); Issue.record("Expected graph refusal"); return nil } + catch let error as JSONGraphFailure { return error } + catch { Issue.record("Unexpected error: \(error)"); return nil } +} +private func graphCore(_ body: () throws -> Void) -> JSONFailure? { + do { try body(); Issue.record("Expected core refusal"); return nil } + catch let error as JSONFailure { return error } + catch let error as JSONAccessFailure { return error.cause } + catch { Issue.record("Unexpected error: \(error)"); return nil } +} +@Test func graphG01EmptySourceAndNoValidStart() throws { + let graph = try graphBuild("g01-empty").result + #expect(graph.nodeCount == 0 && graph.edgeCount == 0 && graph.offsets == [0]) + #expect(graph.document.sourceBytes != nil) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0) } == .nodeOutOfBounds(0)) +} +@Test func graphG02IndependentOracleAndSourceOrder() throws { + struct Oracle: Decodable { let cases: [Case] } + struct Case: Decodable { let name: String; let expected: Expected } + struct Expected: Decodable { + let kind: String; let ids_codepoints: [[UInt32]]?; let edge_pairs: [[Int]]? + let outgoing_edge_ordinals: [[Int]]?; let bfs_from_node0: [Int]? + } + let bundle = try #require(Bundle.module.resourceURL) + let url = URL(fileURLWithPath: bundle.path + "/GraphFixtures/GRAPH-FIXTURE-ORACLE.json") + let oracle = try JSONValueDecoder().decode(Oracle.self, from: JSON.parse(Data(contentsOf: url))) + var evaluated = 0 + for entry in oracle.cases where entry.expected.kind == "valid" { + let profile = entry.name == "g23-numeric-object-key" ? JSONGraphProfile( + nodesAt: [.key("0"), .key("nodes")], edgesAt: [.key("0"), .key("edges")], + nodeIDKey: "key/id", edgeSourceKey: "src~", edgeTargetKey: "dst") : graphProfile + let graph = try graphBuild(entry.name, profile: profile).result + #expect(graph.nodeIDs.map { $0.unicodeScalars.map(\.value) } == entry.expected.ids_codepoints) + #expect(graph.edges.map { [$0.source, $0.target] } == entry.expected.edge_pairs) + var outgoing: [[Int]] = [] + for node in 0.. 0 { #expect(try graph.breadthFirst(fromNode: 0).result == entry.expected.bfs_from_node0) } + let document = graph.document + #expect(try JSONGraphProjection.build(document: document, profile: profile, maximumNodes: graph.nodeCount, maximumEdges: graph.edgeCount).result.edges == graph.edges) + if graph.nodeCount > 0 { + #expect(graphFailure { _ = try JSONGraphProjection.build(document: document, profile: profile, maximumNodes: graph.nodeCount - 1) } == .nodeLimitExceeded(limit: graph.nodeCount - 1)) + let reached = try #require(entry.expected.bfs_from_node0) + var seen = Set([0]), queue = [0], head = 0, peak = 1 + while head < queue.count { + let node = queue[head]; head += 1 + for pair in try #require(entry.expected.edge_pairs) where pair[0] == node { + if seen.insert(pair[1]).inserted { queue.append(pair[1]); peak = max(peak, queue.count - head) } + } + } + #expect(queue == reached) + #expect(try graph.breadthFirst(fromNode: 0, maximumFrontier: peak, maximumVisited: reached.count, maximumResults: reached.count).result == reached) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumFrontier: peak - 1) } == .frontierLimitExceeded(limit: peak - 1)) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumVisited: reached.count - 1) } == .visitedLimitExceeded(limit: reached.count - 1)) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumResults: reached.count - 1) } == .resultLimitExceeded(limit: reached.count - 1)) + } + if graph.edgeCount > 0 { + #expect(graphFailure { _ = try JSONGraphProjection.build(document: document, profile: profile, maximumEdges: graph.edgeCount - 1) } == .edgeLimitExceeded(limit: graph.edgeCount - 1)) + } + evaluated += 1 + } + #expect(evaluated == 12) + var refused = 0 + for entry in oracle.cases where entry.expected.kind != "valid" { + if entry.expected.kind == "overlap" { + let profile = JSONGraphProfile(nodesAt: [.key("data")], edgesAt: [.key("data")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + #expect(throws: JSONTraversalFailure.ambiguousRoots(first: 0, second: 1)) { _ = try graphBuild(entry.name, profile: profile) } + } else if entry.expected.kind.hasSuffix("not_array") { + #expect(graphCore { _ = try graphBuild(entry.name) }?.code == .wrongKind) + } else { + let failure = graphFailure { _ = try graphBuild(entry.name) } + switch (entry.expected.kind, failure) { + case ("duplicate_node_id", .duplicateNodeID), ("node_id_missing", .idMissing), + ("edge_source_missing", .idMissing), ("node_id_null", .idNull), + ("edge_target_null", .idNull), ("node_id_wrong_kind", .idWrongKind), + ("edge_target_wrong_kind", .idWrongKind), ("node_row_wrong_kind", .rowNotObject), + ("edge_row_wrong_kind", .rowNotObject), ("dangling_endpoint", .danglingEndpoint), + ("duplicate_directed_edge", .duplicateDirectedEdge): break + default: Issue.record("Independent refusal mismatch: \(entry.name) \(String(describing: failure))") + } + } + refused += 1 + } + #expect(refused == 15) + print("GRAPH_ORACLE passed valid=12 refused=15") + +} +@Test func graphG03ScalarDistinctIDs() throws { + let graph = try graphBuild("g03-unicode-scalars").result + #expect(graph.nodeCount == 2 && graph.edges[0].target == 1) + #expect(graph.nodeIDs[0] == graph.nodeIDs[1]) // Swift canonical equivalence is intentionally different. + #expect(Array(graph.nodeIDs[0].utf8) != Array(graph.nodeIDs[1].utf8)) +} +@Test func graphG04SpecialIDsAreNotPaths() throws { + let graph = try graphBuild("g04-empty-and-special-ids").result + #expect(graph.nodeIDs.contains("")); #expect(graph.nodeCount == 3) + for node in 0..= 6 { fired.withLock { $0 = true }; withUnsafeCurrentTask { $0?.cancel() } } + } + } + let observer: @Sendable (GraphWorkStage) -> Void = { current in + if current == phase { armed.withLock { $0 = true } } + } + if phase == .breadthFirst { + let captured = try JSONGraphProjection.build(document: large, profile: graphProfile).result + var query = GraphQuery(workObserver: observer, graph: captured, query: .breadthFirst(0), limits: limits, + maximumFrontier: 12, maximumVisited: 12, maximumResults: 12) + #expect(throws: CancellationError.self) { _ = try walk.inspectOwned(options: .init(depthLimit: 0), reducer: &query) } + } else { + var reducer = GraphBuilder(workObserver: observer, profile: graphProfile, limits: limits, maximumNodes: 12, maximumEdges: 11) + #expect(throws: CancellationError.self) { + _ = try walk.inspectOwned(scope: .virtual(roots: [graphProfile.nodesAt, graphProfile.edgesAt], overlap: .rejectAmbiguous), options: .init(depthLimit: 1), reducer: &reducer) + } + } + } + try await job.value + #expect(fired.withLock { $0 } && cleared.withLock { $0 }) + print("GRAPH_CANCELLATION phase=\(phase) checkpoints=\(ticks.withLock { $0 }) root-and-state-released=true") + } + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + #expect(throws: CancellationError.self) { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile) } + #expect(throws: CancellationError.self) { _ = try graph.breadthFirst(fromNode: 0) } + } + await task.value + #expect(try graph.breadthFirst(fromNode: 0).result == [0, 1, 2, 3]) +} +@Test func graphG29FailurePrecedenceAndTerminalLatch() throws { + let doc = JSONDocument(root: try JSON.parse(#"{"nodes":[false,{"id":"A"},{"id":"A"}],"edges":[{"from":"X","to":"Y"}]}"#)) + #expect(graphCore { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile, maximumNodes: -1) }?.code == .invalidLimits) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile) } == .rowNotObject(at: [.key("nodes"), .index(0)], kind: .boolean)) + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + #expect(graphCore { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile, limits: limits) }?.category == .resource) + let prefix = (0..<32).map { "{\"id\":\"n\($0)\"}" }.joined(separator: ",") + let late = JSONDocument(root: try JSON.parse("{\"nodes\":[" + prefix + ",{\"id\":\"n0\"}],\"edges\":[{\"from\":\"X\",\"to\":\"Y\"}]}")) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: late, profile: graphProfile) } == .duplicateNodeID(first: [.key("nodes"), .index(0)], second: [.key("nodes"), .index(32)])) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: late, profile: graphProfile, maximumNodes: 32) } == .nodeLimitExceeded(limit: 32)) + let admitted = try JSONTraversal(document: late).visit(options: .init(depthLimit: 0)) { _,_ in .continue } + limits = .balanced; limits[.workUnits] = admitted.statistics[.workUnits] + 100 + #expect(graphCore { _ = try JSONGraphProjection.build(document: late, profile: graphProfile, limits: limits) }?.category == .resource) + let fields = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"A"}],"edges":[{"from":"MISSING","to":null}]}"#)) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: fields, profile: graphProfile) } == .idNull(at: [.key("edges"), .index(0), .key("to")])) + struct Catcher: JSONTraversalOwnedReducer { + enum Replacement: Error { case caller } + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { + do { try context.work(UInt64.max) } catch {} + throw Replacement.caller + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> Bool { true } + } + var reducer = Catcher() + #expect(graphCore { _ = try JSONTraversal(document: doc).inspectOwned(reducer: &reducer) }?.category == .resource) +} +@Test func graphG30ConcurrentViewsHaveIndependentHealth() async throws { + let doc = try graphDocument("g22-diamond") + let graph = try JSONGraphProjection.build(document: doc, profile: graphProfile).result + try await withThrowingTaskGroup(of: [Int].self) { group in + for _ in 0..<12 { + group.addTask { + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumResults: 1) } == .resultLimitExceeded(limit: 1)) + return try JSONGraphProjection.build(document: doc, profile: graphProfile).result.breadthFirst(fromNode: 0).result + } + } + for try await result in group { #expect(result == [0, 1, 2, 3]) } + } +} +@Test func graphG31CapturedRootAndSourceMapCoherence() throws { + let original = try graphDocument("g02-simple"), graph = try JSONGraphProjection.build(document: original, profile: graphProfile).result + let replaced = original.replacingRoot(try JSON.parse(#"{"nodes":[{"id":"NEW"}],"edges":[]}"#)) + let new = try JSONGraphProjection.build(document: replaced, profile: graphProfile).result + #expect(try graph.nodeSpan(at: 0).result != nil && new.nodeSpan(at: 0).result == nil) + #expect(graph.nodeIDs == ["A", "B", "C"] && new.nodeIDs == ["NEW"]) + #expect(graph.document.sourceBytes == original.sourceBytes) +} +@Test func graphG32CheckedMetadataAndExistingPublicSemantics() throws { + let graph = try graphBuild("g02-simple").result + #expect(graphFailure { _ = try graph.nodeValue(at: -1) } == .nodeOutOfBounds(-1)) + #expect(graphFailure { _ = try graph.edgeValue(at: graph.edgeCount) } == .edgeOutOfBounds(graph.edgeCount)) + #expect(try graph.edgeValue(at: 0).result == JSONPointer(parsing: "/edges/0").resolve(in: graph.document.root)) + #expect(try JSONTraversal(document: graph.document).visit { _, _ in .continue }.statistics[.retainedBytes] == 0) + #expect(try JSONShapeInspector(document: graph.document).inspectArray(at: [.key("nodes")]).result.totalRows == 3) +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json b/Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json new file mode 100644 index 0000000..b613cb8 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json @@ -0,0 +1,653 @@ +{ + "disclaimer": "Independent test-data oracle, NOT Swift-JSON validation", + "cases": [ + { + "name": "g01-empty", + "file": "g01-empty.json", + "sha256": "1ddf1d31190da6e68449f240aefa5e9b87896f675196008d26c8ea7431ec8b4c", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [], + "edge_pairs": [], + "outgoing_edge_ordinals": [], + "bfs_from_node0": [] + } + }, + { + "name": "g02-simple", + "file": "g02-simple.json", + "sha256": "97ce5fa7ec116a89a48606897295bbc2934fdc927b429a213d13e3474694f100", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 0, + 2 + ], + [ + 1, + 2 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0, + 1 + ], + [ + 2 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1, + 2 + ] + } + }, + { + "name": "g03-unicode-scalars", + "file": "g03-unicode-scalars.json", + "sha256": "47cf0ab06c0b72d663d8a7d807f46752414e34cb422ef526a4e155a60e194182", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 233 + ], + [ + 101, + 769 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g04-empty-and-special-ids", + "file": "g04-empty-and-special-ids.json", + "sha256": "45a06cd463be2eee805f145d4a386ab8f76d64bf53503aa5086df16724c02791", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [], + [ + 97, + 47, + 98, + 126, + 99 + ], + [ + 48 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 2, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [], + [ + 1 + ] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g05-duplicate-id", + "file": "g05-duplicate-id.json", + "sha256": "ebb7b9720976636fc4521f6b66c1e92544bae4ab1dfb244c4988af42cc56eb24", + "profile": {}, + "expected": { + "kind": "duplicate_node_id", + "node_index": 1, + "first_index": 0 + } + }, + { + "name": "g06-equal-payload-distinct-id", + "file": "g06-equal-payload-distinct-id.json", + "sha256": "f740b12b7a01d74c2b86b729e56c82abc5b7ef7be2e8672313286bf18e295ddf", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ] + ], + "edge_pairs": [], + "outgoing_edge_ordinals": [ + [], + [] + ], + "bfs_from_node0": [ + 0 + ] + } + }, + { + "name": "g07-node-id-missing", + "file": "g07-node-id-missing.json", + "sha256": "3473660136a3b01aa468cbc5a1c1f1f093ae0d9ac476292a9c44f79e296376f1", + "profile": {}, + "expected": { + "kind": "node_id_missing", + "node_index": 0 + } + }, + { + "name": "g08-node-id-null", + "file": "g08-node-id-null.json", + "sha256": "ad53d92263bb921749296c96f50470495a3758646e3777a4039c15a2dd4f548c", + "profile": {}, + "expected": { + "kind": "node_id_null", + "node_index": 0 + } + }, + { + "name": "g09-node-id-number", + "file": "g09-node-id-number.json", + "sha256": "311961bc71d065686c2be6ddc4b3bae46c1ffab1c5906fc4c0b8c22af13e3d37", + "profile": {}, + "expected": { + "kind": "node_id_wrong_kind", + "node_index": 0 + } + }, + { + "name": "g10-node-row-not-object", + "file": "g10-node-row-not-object.json", + "sha256": "d76618e34d1bc43d9de376b72d2a2f8cc4ed2d449b6fdc8537c901147fba4212", + "profile": {}, + "expected": { + "kind": "node_row_wrong_kind", + "node_index": 0 + } + }, + { + "name": "g11-edge-source-missing", + "file": "g11-edge-source-missing.json", + "sha256": "77e0e2a123ee05f3d842b00181e2463042ac9580169c6571d79146b1aebf988c", + "profile": {}, + "expected": { + "kind": "edge_source_missing", + "edge_index": 0 + } + }, + { + "name": "g12-edge-target-null", + "file": "g12-edge-target-null.json", + "sha256": "b873ad0efe1efe13215a283ec202c55407aeb1d754e8aba79dd38f72c3529244", + "profile": {}, + "expected": { + "kind": "edge_target_null", + "edge_index": 0 + } + }, + { + "name": "g13-edge-target-number", + "file": "g13-edge-target-number.json", + "sha256": "2bbbedb9d505d32ca01228c5b63ba71ee8510299371757ad741cc17c09a5e49a", + "profile": {}, + "expected": { + "kind": "edge_target_wrong_kind", + "edge_index": 0 + } + }, + { + "name": "g14-edge-row-not-object", + "file": "g14-edge-row-not-object.json", + "sha256": "21de4ba95ef61f4856d0eee1a02d0c17694b50cc291378fdcb05efbf6e721e01", + "profile": {}, + "expected": { + "kind": "edge_row_wrong_kind", + "edge_index": 0 + } + }, + { + "name": "g15-dangling", + "file": "g15-dangling.json", + "sha256": "c3a9c699115be44b60da5d165544ea9a4f9d443685422f5ab99fdee59a2d8c54", + "profile": {}, + "expected": { + "kind": "dangling_endpoint", + "edge_index": 0 + } + }, + { + "name": "g15b-dangling-empty", + "file": "g15b-dangling-empty.json", + "sha256": "074f22438edb27a566aba8491502909f894e0aefdf5dac04dd589758b0844d20", + "profile": {}, + "expected": { + "kind": "dangling_endpoint", + "edge_index": 0 + } + }, + { + "name": "g16-duplicate-edge", + "file": "g16-duplicate-edge.json", + "sha256": "76bffe5363e00f24cb4e6951eff04214d11170682e80e8e53cbd7ab1c321419e", + "profile": {}, + "expected": { + "kind": "duplicate_directed_edge", + "edge_index": 1, + "first_index": 0 + } + }, + { + "name": "g17-reverse-edges", + "file": "g17-reverse-edges.json", + "sha256": "62bc74cf4934b0c2f82d322b8f28c563dd70c1745df6661aaa1389caef55c780", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 1, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [ + 1 + ] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g18-self-edge", + "file": "g18-self-edge.json", + "sha256": "2bd68dc1cd578b9ac7b922c4fb2f13d2f50bfa6c4068af18966deb59e0205e8d", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ] + ], + "edge_pairs": [ + [ + 0, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ] + ], + "bfs_from_node0": [ + 0 + ] + } + }, + { + "name": "g19-cycle", + "file": "g19-cycle.json", + "sha256": "d0c0a81d2a87dfe714b963957f6b7305cdf2942df58ea2a2548aa450d1e0d349", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 1, + 2 + ], + [ + 2, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [ + 1 + ], + [ + 2 + ] + ], + "bfs_from_node0": [ + 0, + 1, + 2 + ] + } + }, + { + "name": "g20-isolate", + "file": "g20-isolate.json", + "sha256": "5137753f500607e15e29d3fc5510b62db8e23c10b647a776419972db3768f098", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 73, + 83, + 79 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [], + [] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g21-bfs-source-edge-order", + "file": "g21-bfs-source-edge-order.json", + "sha256": "73166cac74801e95d7954bcd7b39b1be9e990bd7bb5d730800a1d8c7d53b77ed", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ], + [ + 68 + ] + ], + "edge_pairs": [ + [ + 0, + 3 + ], + [ + 0, + 1 + ], + [ + 0, + 2 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0, + 1, + 2 + ], + [], + [], + [] + ], + "bfs_from_node0": [ + 0, + 3, + 1, + 2 + ] + } + }, + { + "name": "g22-diamond", + "file": "g22-diamond.json", + "sha256": "8b8b2934a1bdd720621c8045c54dde4e6eee9a97ce4b90020b0404d4ff26cedf", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ], + [ + 68 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 0, + 2 + ], + [ + 1, + 3 + ], + [ + 2, + 3 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0, + 1 + ], + [ + 2 + ], + [ + 3 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1, + 2, + 3 + ] + } + }, + { + "name": "g23-numeric-object-key", + "file": "g23-numeric-object-key.json", + "sha256": "ae3d0720da528a24240b629891b1d2d47928efa901fbb3d7c29417affb942385", + "profile": { + "nodes_at": [ + { + "key": "0" + }, + { + "key": "nodes" + } + ], + "edges_at": [ + { + "key": "0" + }, + { + "key": "edges" + } + ], + "node_id_key": "key/id", + "edge_source_key": "src~", + "edge_target_key": "dst" + }, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 48 + ], + [ + 65 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g24-overlap", + "file": "g24-overlap.json", + "sha256": "74011485b9b4c4cf9fe70d264ffe7db4ba3796034968c0773b3213f395475e93", + "profile": { + "nodes_at": [ + { + "key": "data" + } + ], + "edges_at": [ + { + "key": "data" + } + ] + }, + "expected": { + "kind": "overlap" + } + }, + { + "name": "g25-wrong-nodes-array", + "file": "g25-wrong-nodes-array.json", + "sha256": "e91a8449e7af8b0555eaeb0b90087a580c72ae239dfb875c1c138dd128e9a8b1", + "profile": {}, + "expected": { + "kind": "nodes_not_array" + } + }, + { + "name": "g26-wrong-edges-array", + "file": "g26-wrong-edges-array.json", + "sha256": "96f22c1ec9ff60642dc39f4188c0955ceeb01da3a7bf9570cbe885ca59f012c1", + "profile": {}, + "expected": { + "kind": "edges_not_array" + } + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g01-empty.json b/Tests/JSONInspectionTests/GraphFixtures/g01-empty.json new file mode 100644 index 0000000..ecb3718 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g01-empty.json @@ -0,0 +1,4 @@ +{ + "nodes": [], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g02-simple.json b/Tests/JSONInspectionTests/GraphFixtures/g02-simple.json new file mode 100644 index 0000000..72cefa8 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g02-simple.json @@ -0,0 +1,27 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "C" + }, + { + "from": "B", + "to": "C" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json b/Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json new file mode 100644 index 0000000..1d0f4ab --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json @@ -0,0 +1,16 @@ +{ + "nodes": [ + { + "id": "é" + }, + { + "id": "é" + } + ], + "edges": [ + { + "from": "é", + "to": "é" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json b/Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json new file mode 100644 index 0000000..230b184 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json @@ -0,0 +1,23 @@ +{ + "nodes": [ + { + "id": "" + }, + { + "id": "a/b~c" + }, + { + "id": "0" + } + ], + "edges": [ + { + "from": "", + "to": "a/b~c" + }, + { + "from": "0", + "to": "" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json b/Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json new file mode 100644 index 0000000..f74f5cb --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A", + "name": "x" + }, + { + "id": "A", + "name": "y" + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json b/Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json new file mode 100644 index 0000000..709b3b0 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json @@ -0,0 +1,17 @@ +{ + "nodes": [ + { + "id": "A", + "same": { + "x": 1 + } + }, + { + "id": "B", + "same": { + "x": 1 + } + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json b/Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json new file mode 100644 index 0000000..6892396 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json @@ -0,0 +1,8 @@ +{ + "nodes": [ + { + "name": "A" + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json b/Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json new file mode 100644 index 0000000..4cdcf9a --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json @@ -0,0 +1,8 @@ +{ + "nodes": [ + { + "id": null + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json b/Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json new file mode 100644 index 0000000..d15d147 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json @@ -0,0 +1,8 @@ +{ + "nodes": [ + { + "id": 22 + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json b/Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json new file mode 100644 index 0000000..4a98b06 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json @@ -0,0 +1,6 @@ +{ + "nodes": [ + "A" + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json b/Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json new file mode 100644 index 0000000..d3fa950 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json @@ -0,0 +1,12 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json b/Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json new file mode 100644 index 0000000..95b6210 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": null + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json b/Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json new file mode 100644 index 0000000..b6e64fb --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": 3 + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json b/Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json new file mode 100644 index 0000000..1872870 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json @@ -0,0 +1,10 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + 3 + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json b/Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json new file mode 100644 index 0000000..f70a7b2 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json b/Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json new file mode 100644 index 0000000..d7ec452 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json @@ -0,0 +1,9 @@ +{ + "nodes": [], + "edges": [ + { + "from": "A", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json b/Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json new file mode 100644 index 0000000..2aa7a8d --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json @@ -0,0 +1,20 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "B" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json b/Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json new file mode 100644 index 0000000..8fb4dfc --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json @@ -0,0 +1,20 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "B", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json b/Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json new file mode 100644 index 0000000..140674c --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json b/Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json new file mode 100644 index 0000000..44c5e1c --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json @@ -0,0 +1,27 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "B", + "to": "C" + }, + { + "from": "C", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json b/Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json new file mode 100644 index 0000000..c3c485c --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json @@ -0,0 +1,19 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "ISO" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json b/Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json new file mode 100644 index 0000000..01322f0 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json @@ -0,0 +1,30 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + }, + { + "id": "D" + } + ], + "edges": [ + { + "from": "A", + "to": "D" + }, + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "C" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json b/Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json new file mode 100644 index 0000000..88d162e --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json @@ -0,0 +1,34 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + }, + { + "id": "D" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "C" + }, + { + "from": "B", + "to": "D" + }, + { + "from": "C", + "to": "D" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json b/Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json new file mode 100644 index 0000000..b985501 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json @@ -0,0 +1,18 @@ +{ + "0": { + "nodes": [ + { + "key/id": "0" + }, + { + "key/id": "A" + } + ], + "edges": [ + { + "src~": "0", + "dst": "A" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json b/Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json new file mode 100644 index 0000000..7e521c7 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json @@ -0,0 +1,7 @@ +{ + "data": [ + { + "id": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json b/Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json new file mode 100644 index 0000000..a649225 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json @@ -0,0 +1,4 @@ +{ + "nodes": {}, + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json b/Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json new file mode 100644 index 0000000..69e5bff --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json @@ -0,0 +1,4 @@ +{ + "nodes": [], + "edges": {} +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py b/Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py new file mode 100755 index 0000000..e95e9d2 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Independent design-fixture generator. NOT a Swift-JSON semantic or runtime qualification.""" +from pathlib import Path +import json, hashlib +base=Path(__file__).resolve().parent +cases={ + 'g01-empty':({'nodes':[],'edges':[]},{},'valid'), + 'g02-simple':({'nodes':[{'id':'A'},{'id':'B'},{'id':'C'}], 'edges':[{'from':'A','to':'B'},{'from':'A','to':'C'},{'from':'B','to':'C'}]}, {},'valid'), + 'g03-unicode-scalars':({'nodes':[{'id':'é'},{'id':'e\u0301'}],'edges':[{'from':'é','to':'e\u0301'}]}, {},'valid'), + 'g04-empty-and-special-ids':({'nodes':[{'id':''},{'id':'a/b~c'},{'id':'0'}], 'edges':[{'from':'','to':'a/b~c'},{'from':'0','to':''}]},{},'valid'), + 'g05-duplicate-id':({'nodes':[{'id':'A','name':'x'},{'id':'A','name':'y'}],'edges':[]},{},'duplicate_node_id'), + 'g06-equal-payload-distinct-id':({'nodes':[{'id':'A','same':{'x':1}},{'id':'B','same':{'x':1}}],'edges':[]},{},'valid'), + 'g07-node-id-missing':({'nodes':[{'name':'A'}],'edges':[]},{},'node_id_missing'), + 'g08-node-id-null':({'nodes':[{'id':None}],'edges':[]},{},'node_id_null'), + 'g09-node-id-number':({'nodes':[{'id':22}],'edges':[]},{},'node_id_wrong_kind'), + 'g10-node-row-not-object':({'nodes':['A'],'edges':[]},{},'node_row_wrong_kind'), + 'g11-edge-source-missing':({'nodes':[{'id':'A'}],'edges':[{'to':'A'}]},{},'edge_source_missing'), + 'g12-edge-target-null':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':None}]},{},'edge_target_null'), + 'g13-edge-target-number':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':3}]},{},'edge_target_wrong_kind'), + 'g14-edge-row-not-object':({'nodes':[{'id':'A'}],'edges':[3]},{},'edge_row_wrong_kind'), + 'g15-dangling':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':'B'}]},{},'dangling_endpoint'), + 'g15b-dangling-empty':({'nodes':[],'edges':[{'from':'A','to':'A'}]},{},'dangling_endpoint'), + 'g16-duplicate-edge':({'nodes':[{'id':'A'},{'id':'B'}],'edges':[{'from':'A','to':'B'},{'from':'A','to':'B'}]},{},'duplicate_directed_edge'), + 'g17-reverse-edges':({'nodes':[{'id':'A'},{'id':'B'}],'edges':[{'from':'A','to':'B'},{'from':'B','to':'A'}]},{},'valid'), + 'g18-self-edge':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':'A'}]},{},'valid'), + 'g19-cycle':({'nodes':[{'id':'A'},{'id':'B'},{'id':'C'}],'edges':[{'from':'A','to':'B'},{'from':'B','to':'C'},{'from':'C','to':'A'}]},{},'valid'), + 'g20-isolate':({'nodes':[{'id':'A'},{'id':'B'},{'id':'ISO'}],'edges':[{'from':'A','to':'B'}]},{},'valid'), + 'g21-bfs-source-edge-order':({'nodes':[{'id':x} for x in ['A','B','C','D']],'edges':[{'from':'A','to':'D'},{'from':'A','to':'B'},{'from':'A','to':'C'}]},{},'valid'), + 'g22-diamond':({'nodes':[{'id':x} for x in ['A','B','C','D']],'edges':[{'from':'A','to':'B'},{'from':'A','to':'C'},{'from':'B','to':'D'},{'from':'C','to':'D'}]},{},'valid'), + 'g23-numeric-object-key':({'0':{'nodes':[{'key/id':'0'},{'key/id':'A'}],'edges':[{'src~':'0','dst':'A'}]}},{'nodes_at':[{'key':'0'},{'key':'nodes'}],'edges_at':[{'key':'0'},{'key':'edges'}],'node_id_key':'key/id','edge_source_key':'src~','edge_target_key':'dst'},'valid'), + 'g24-overlap':({'data':[{'id':'A'}]},{'nodes_at':[{'key':'data'}],'edges_at':[{'key':'data'}]},'overlap'), + 'g25-wrong-nodes-array':({'nodes':{},'edges':[]},{},'nodes_not_array'), + 'g26-wrong-edges-array':({'nodes':[],'edges':{}},{},'edges_not_array'), +} +def tok_path(root,segs): + val=root + for seg in segs: + if 'key' in seg: + if not isinstance(val,dict):raise TypeError('path_expected_object') + val=val[seg['key']] + else: + if not isinstance(val,list):raise TypeError('path_expected_array') + val=val[seg['index']] + return val +def reference(obj, prof): + np=prof.get('nodes_at',[{'key':'nodes'}]);ep=prof.get('edges_at',[{'key':'edges'}]);node=prof.get('node_id_key','id'); fr=prof.get('edge_source_key','from');to=prof.get('edge_target_key','to') + if np==ep or (len(np)<=len(ep) and np==ep[:len(np)]) or (len(ep)<=len(np) and ep==np[:len(ep)]):return {'kind':'overlap'} + try:nodes=tok_path(obj,np);edges=tok_path(obj,ep) + except Exception:return {'kind':'missing_or_invalid_selection'} + if not isinstance(nodes,list):return {'kind':'nodes_not_array'} + if not isinstance(edges,list):return {'kind':'edges_not_array'} + ids=[] + for i,n in enumerate(nodes): + if not isinstance(n,dict):return {'kind':'node_row_wrong_kind','node_index':i} + if node not in n:return {'kind':'node_id_missing','node_index':i} + if n[node] is None:return {'kind':'node_id_null','node_index':i} + if not isinstance(n[node],str):return {'kind':'node_id_wrong_kind','node_index':i} + if n[node] in ids:return {'kind':'duplicate_node_id','node_index':i,'first_index':ids.index(n[node])} + ids.append(n[node]) + pairs=[];outgoing=[[] for _ in nodes] + for eindex,e in enumerate(edges): + if not isinstance(e,dict):return {'kind':'edge_row_wrong_kind','edge_index':eindex} + for field,field_name in ((fr,'edge_source'),(to,'edge_target')): + if field not in e:return {'kind':field_name+'_missing','edge_index':eindex} + if e[field] is None:return {'kind':field_name+'_null','edge_index':eindex} + if not isinstance(e[field],str):return {'kind':field_name+'_wrong_kind','edge_index':eindex} + if e[fr] not in ids or e[to] not in ids:return {'kind':'dangling_endpoint','edge_index':eindex} + pair=(ids.index(e[fr]),ids.index(e[to])) + if pair in pairs:return {'kind':'duplicate_directed_edge','edge_index':eindex,'first_index':pairs.index(pair)} + pairs.append(pair);outgoing[pair[0]].append(eindex) + visited=[False]*len(ids);order=[];queue=[] + if nodes: + queue=[0];visited[0]=True;head=0 + while head JSONDocument { try JSONParser().parseDocument(Data(text.utf8)) } + static func build(_ text: String = text) throws -> JSONOperationReport { try JSONGraphProjection.build(document: doc(text), profile: profile) } + static func graphError(_ expected: JSONGraphFailure, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(100) } catch let failure as JSONGraphFailure { try check(failure == expected, 101) } + } + static func resource(_ metric: JSONResourceMetric, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(102) } catch let failure as JSONFailure { try check(failure.category == .resource && failure.resource?.metric == metric, 103) } + } + static func run(_ id: Int) async throws { + switch id { + case 1: + let result = try build(); let graph: JSONGraphProjection = result.result + try check(graph.nodeCount == 4 && graph.edgeCount == 3 && result.statistics[.workUnits] > 0, id) + case 2: + let document = try doc(#"{"":{"0":[{"id":"A"}]},"edges":[]}"#) + let exact = JSONGraphProfile(nodesAt: [.key(""), .key("0")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let graph = try JSONGraphProjection.build(document: document, profile: exact).result + try check(try graph.nodeValue(at: 0).result == JSONPointer(parsing: "//0/0").resolve(in: document.root), id) + let indexed = JSONGraphProfile(nodesAt: [.index(0)], edgesAt: exact.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + do { _ = try JSONGraphProjection.build(document: document, profile: indexed); throw CheckFailure.failed(id) } + catch let failure as JSONAccessFailure { try check(failure.cause.code == .wrongKind, id) } + case 3: + let graph = try build().result + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + try check(try graph.outgoingEdges(of: 0).result == [0,1], id) + try check(try graph.breadthFirst(fromNode: 3).result == [3], id) + case 4: + let graph = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + let explicitReuse = JSONGraphProfile(nodesAt: profile.nodesAt, edgesAt: profile.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "from") + let reused = try JSONGraphProjection.build(document: doc(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A"}]}"#), profile: explicitReuse).result + try check(try reused.breadthFirst(fromNode: 0).result == [0], id) + case 5: + try graphError(.duplicateNodeID(first: [.key("nodes"),.index(0)], second: [.key("nodes"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#) + } + case 6: + let graph = try build(#"{"nodes":[{"id":"é"},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#).result + let a = try graph.nodeValue(at: 0).result.requiredMember("id"), b = try graph.nodeValue(at: 1).result.requiredMember("id") + try check(a != b && graph.nodeCount == 2, id) + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + case 7: + try graphError(.duplicateDirectedEdge(first: [.key("edges"),.index(0)], second: [.key("edges"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"A"}]}"#) + } + let reverse = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(reverse.edgeCount == 2, id) + case 8: + try graphError(.danglingEndpoint(at: [.key("edges"),.index(0),.key("to")])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"missing"}]}"#) + } + case 9: + let path: [JSONLocationComponent] = [.key("nodes"),.index(0),.key("id")] + for (row,error) in [("{}", JSONGraphFailure.idMissing(at: path)), (#"{"id":null}"#, .idNull(at: path)), (#"{"id":1}"#, .idWrongKind(at: path, kind: .number)), ("false", .rowNotObject(at: [.key("nodes"),.index(0)], kind: .boolean))] { + try graphError(error) { _ = try build("{\"nodes\":[" + row + "],\"edges\":[]}") } + } + try graphError(.idNull(at: [.key("edges"),.index(0),.key("to")])) { _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":null}]}"#) } + case 10: + let graph = try build().result + try graphError(.nodeOutOfBounds(-1)) { _ = try graph.nodeValue(at: -1) } + try graphError(.edgeOutOfBounds(3)) { _ = try graph.edgeValue(at: 3) } + try graphError(.frontierLimitExceeded(limit: 0)) { _ = try graph.breadthFirst(fromNode: 0, maximumFrontier: 0) } + try graphError(.resultLimitExceeded(limit: 0)) { _ = try graph.outgoingEdges(of: 0, maximumResults: 0) } + let empty = try JSONGraphProjection.build(document: doc(#"{"nodes":[],"edges":[]}"#), profile: profile, maximumNodes: 0, maximumEdges: 0).result + try check(empty.nodeCount == 0 && empty.edgeCount == 0, id) + do { _ = try graph.breadthFirst(fromNode: 0, maximumResults: -1); throw CheckFailure.failed(id) } + catch let failure as JSONFailure { try check(failure.code == .invalidLimits, id) } + case 11: + let graph = try build().result, baseline = try graph.breadthFirst(fromNode: 0) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + try check(try graph.breadthFirst(fromNode: 0, limits: limits).statistics[.workUnits] == baseline.statistics[.workUnits], id) + limits[.workUnits] -= 1 + try resource(.workUnits) { _ = try graph.breadthFirst(fromNode: 0, limits: limits) } + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 12: + let result = try build(), graph = result.result, row = try graph.nodeValue(at: 0) + try check(graph.document.sourceBytes == doc().sourceBytes && result.statistics[.retainedBytes] > 0 && row.statistics[.retainedBytes] > 0, id) + let number = try row.result.requiredMember("n").numberValue + try check(try number?.integer(as: UInt64.self) == 9_007_199_254_740_993, id) + try check(try graph.edgeValue(at: 0).result == JSONPointer(parsing: "/edges/0").resolve(in: graph.document.root), id) + case 13: + let graph = try build().result + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + do { _ = try graph.breadthFirst(fromNode: 0); throw CheckFailure.failed(id) } catch is CancellationError {} + do { _ = try build(); throw CheckFailure.failed(id) } catch is CancellationError {} + } + try await task.value + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 14: + let graph = try build().result + try await withThrowingTaskGroup(of: [Int].self) { group in + for _ in 0..<8 { group.addTask { + try graphError(.resultLimitExceeded(limit: 1)) { _ = try graph.breadthFirst(fromNode: 0, maximumResults: 1) } + return try graph.breadthFirst(fromNode: 0).result + } } + for try await result in group { try check(result == [0,2,1], id) } + } + case 15: + let original = try doc(), graph = try JSONGraphProjection.build(document: original, profile: profile).result + let replacement = original.replacingRoot(try JSON.parse(#"{"nodes":[{"id":"new"}],"edges":[]}"#)) + let next = try JSONGraphProjection.build(document: replacement, profile: profile).result + try check(try graph.nodeSpan(at: 0).result != nil && next.nodeSpan(at: 0).result == nil, id) + try check(graph.nodeCount == 4 && next.nodeCount == 1 && graph.document.root == original.root, id) + case 16: + // The selected graph is tiny, but an unselected subtree still undergoes core admission. + let invalid = JSONDocument(root: .object(try JSONObject(members: [JSONMember("nodes", value: .array([])),JSONMember("edges", value: .array([])),JSONMember("unselected", value: .string(String(repeating: "x", count: 32)))]))) + var limits = JSONLimits.balanced; limits[.stringBytes] = 16 + try resource(.stringBytes) { _ = try JSONGraphProjection.build(document: invalid, profile: profile, limits: limits) } + default: throw CheckFailure.failed(id) + } + } +} + +@Test func graphGP01OptInSurface() async throws { try await GraphPublicCases.run(1) } + +@Test func graphGP02TypedRootAndEmptyKeys() async throws { try await GraphPublicCases.run(2) } + +@Test func graphGP03SourceOrderBFS() async throws { try await GraphPublicCases.run(3) } + +@Test func graphGP04SelfLoopAndCycle() async throws { try await GraphPublicCases.run(4) } + +@Test func graphGP05DuplicateIDPaths() async throws { try await GraphPublicCases.run(5) } + +@Test func graphGP06ScalarDistinctIDs() async throws { try await GraphPublicCases.run(6) } + +@Test func graphGP07OrderedPairs() async throws { try await GraphPublicCases.run(7) } + +@Test func graphGP08DanglingEndpoint() async throws { try await GraphPublicCases.run(8) } + +@Test func graphGP09MissingNullWrongKind() async throws { try await GraphPublicCases.run(9) } + +@Test func graphGP10BoundsAndZeroCaps() async throws { try await GraphPublicCases.run(10) } + +@Test func graphGP11IndependentWholeSourceBudget() async throws { try await GraphPublicCases.run(11) } + +@Test func graphGP12OwnedValueAndSourceHandoff() async throws { try await GraphPublicCases.run(12) } + +@Test func graphGP13CancellationAndHealth() async throws { try await GraphPublicCases.run(13) } + +@Test func graphGP14ConcurrentQueries() async throws { try await GraphPublicCases.run(14) } + +@Test func graphGP15SourceMapCoherence() async throws { try await GraphPublicCases.run(15) } + +@Test func graphGP16UnselectedAdmission() async throws { try await GraphPublicCases.run(16) } diff --git a/Tools/GraphPublicQualification/Package.swift b/Tools/GraphPublicQualification/Package.swift new file mode 100644 index 0000000..e099d57 --- /dev/null +++ b/Tools/GraphPublicQualification/Package.swift @@ -0,0 +1,4 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "GraphPublicQualification", platforms: [.macOS("27.0"), .iOS("27.0")], dependencies: [.package(name: "Swift-JSON", path: "../..")], targets: [.executableTarget(name: "GraphPublicConsumer", dependencies: [.product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONNavigation", package: "Swift-JSON"), .product(name: "JSONInspection", package: "Swift-JSON")])], swiftLanguageModes: [.v6]) diff --git a/Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift b/Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift new file mode 100644 index 0000000..d8e126a --- /dev/null +++ b/Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONNavigation +import JSONInspection + +// Shared scenario inputs are also executed by the distinct-package public consumer. +enum GraphPublicCases { + enum CheckFailure: Error { case failed(Int) } + static let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + static let text = #"{"nodes":[{"id":"A","n":9007199254740993},{"id":"B"},{"id":"C"},{"id":"isolate"}],"edges":[{"from":"A","to":"C"},{"from":"A","to":"B"},{"from":"C","to":"A"}]}"# + static func check(_ value: Bool, _ id: Int) throws { if !value { throw CheckFailure.failed(id) } } + static func doc(_ text: String = text) throws -> JSONDocument { try JSONParser().parseDocument(Data(text.utf8)) } + static func build(_ text: String = text) throws -> JSONOperationReport { try JSONGraphProjection.build(document: doc(text), profile: profile) } + static func graphError(_ expected: JSONGraphFailure, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(100) } catch let failure as JSONGraphFailure { try check(failure == expected, 101) } + } + static func resource(_ metric: JSONResourceMetric, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(102) } catch let failure as JSONFailure { try check(failure.category == .resource && failure.resource?.metric == metric, 103) } + } + static func run(_ id: Int) async throws { + switch id { + case 1: + let result = try build(); let graph: JSONGraphProjection = result.result + try check(graph.nodeCount == 4 && graph.edgeCount == 3 && result.statistics[.workUnits] > 0, id) + case 2: + let document = try doc(#"{"":{"0":[{"id":"A"}]},"edges":[]}"#) + let exact = JSONGraphProfile(nodesAt: [.key(""), .key("0")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let graph = try JSONGraphProjection.build(document: document, profile: exact).result + try check(try graph.nodeValue(at: 0).result == JSONPointer(parsing: "//0/0").resolve(in: document.root), id) + let indexed = JSONGraphProfile(nodesAt: [.index(0)], edgesAt: exact.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + do { _ = try JSONGraphProjection.build(document: document, profile: indexed); throw CheckFailure.failed(id) } + catch let failure as JSONAccessFailure { try check(failure.cause.code == .wrongKind, id) } + case 3: + let graph = try build().result + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + try check(try graph.outgoingEdges(of: 0).result == [0,1], id) + try check(try graph.breadthFirst(fromNode: 3).result == [3], id) + case 4: + let graph = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + let explicitReuse = JSONGraphProfile(nodesAt: profile.nodesAt, edgesAt: profile.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "from") + let reused = try JSONGraphProjection.build(document: doc(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A"}]}"#), profile: explicitReuse).result + try check(try reused.breadthFirst(fromNode: 0).result == [0], id) + case 5: + try graphError(.duplicateNodeID(first: [.key("nodes"),.index(0)], second: [.key("nodes"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#) + } + case 6: + let graph = try build(#"{"nodes":[{"id":"é"},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#).result + let a = try graph.nodeValue(at: 0).result.requiredMember("id"), b = try graph.nodeValue(at: 1).result.requiredMember("id") + try check(a != b && graph.nodeCount == 2, id) + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + case 7: + try graphError(.duplicateDirectedEdge(first: [.key("edges"),.index(0)], second: [.key("edges"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"A"}]}"#) + } + let reverse = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(reverse.edgeCount == 2, id) + case 8: + try graphError(.danglingEndpoint(at: [.key("edges"),.index(0),.key("to")])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"missing"}]}"#) + } + case 9: + let path: [JSONLocationComponent] = [.key("nodes"),.index(0),.key("id")] + for (row,error) in [("{}", JSONGraphFailure.idMissing(at: path)), (#"{"id":null}"#, .idNull(at: path)), (#"{"id":1}"#, .idWrongKind(at: path, kind: .number)), ("false", .rowNotObject(at: [.key("nodes"),.index(0)], kind: .boolean))] { + try graphError(error) { _ = try build("{\"nodes\":[" + row + "],\"edges\":[]}") } + } + try graphError(.idNull(at: [.key("edges"),.index(0),.key("to")])) { _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":null}]}"#) } + case 10: + let graph = try build().result + try graphError(.nodeOutOfBounds(-1)) { _ = try graph.nodeValue(at: -1) } + try graphError(.edgeOutOfBounds(3)) { _ = try graph.edgeValue(at: 3) } + try graphError(.frontierLimitExceeded(limit: 0)) { _ = try graph.breadthFirst(fromNode: 0, maximumFrontier: 0) } + try graphError(.resultLimitExceeded(limit: 0)) { _ = try graph.outgoingEdges(of: 0, maximumResults: 0) } + let empty = try JSONGraphProjection.build(document: doc(#"{"nodes":[],"edges":[]}"#), profile: profile, maximumNodes: 0, maximumEdges: 0).result + try check(empty.nodeCount == 0 && empty.edgeCount == 0, id) + do { _ = try graph.breadthFirst(fromNode: 0, maximumResults: -1); throw CheckFailure.failed(id) } + catch let failure as JSONFailure { try check(failure.code == .invalidLimits, id) } + case 11: + let graph = try build().result, baseline = try graph.breadthFirst(fromNode: 0) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + try check(try graph.breadthFirst(fromNode: 0, limits: limits).statistics[.workUnits] == baseline.statistics[.workUnits], id) + limits[.workUnits] -= 1 + try resource(.workUnits) { _ = try graph.breadthFirst(fromNode: 0, limits: limits) } + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 12: + let result = try build(), graph = result.result, row = try graph.nodeValue(at: 0) + try check(graph.document.sourceBytes == doc().sourceBytes && result.statistics[.retainedBytes] > 0 && row.statistics[.retainedBytes] > 0, id) + let number = try row.result.requiredMember("n").numberValue + try check(try number?.integer(as: UInt64.self) == 9_007_199_254_740_993, id) + try check(try graph.edgeValue(at: 0).result == JSONPointer(parsing: "/edges/0").resolve(in: graph.document.root), id) + case 13: + let graph = try build().result + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + do { _ = try graph.breadthFirst(fromNode: 0); throw CheckFailure.failed(id) } catch is CancellationError {} + do { _ = try build(); throw CheckFailure.failed(id) } catch is CancellationError {} + } + try await task.value + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 14: + let graph = try build().result + try await withThrowingTaskGroup(of: [Int].self) { group in + for _ in 0..<8 { group.addTask { + try graphError(.resultLimitExceeded(limit: 1)) { _ = try graph.breadthFirst(fromNode: 0, maximumResults: 1) } + return try graph.breadthFirst(fromNode: 0).result + } } + for try await result in group { try check(result == [0,2,1], id) } + } + case 15: + let original = try doc(), graph = try JSONGraphProjection.build(document: original, profile: profile).result + let replacement = original.replacingRoot(try JSON.parse(#"{"nodes":[{"id":"new"}],"edges":[]}"#)) + let next = try JSONGraphProjection.build(document: replacement, profile: profile).result + try check(try graph.nodeSpan(at: 0).result != nil && next.nodeSpan(at: 0).result == nil, id) + try check(graph.nodeCount == 4 && next.nodeCount == 1 && graph.document.root == original.root, id) + case 16: + // The selected graph is tiny, but an unselected subtree still undergoes core admission. + let invalid = JSONDocument(root: .object(try JSONObject(members: [JSONMember("nodes", value: .array([])),JSONMember("edges", value: .array([])),JSONMember("unselected", value: .string(String(repeating: "x", count: 32)))]))) + var limits = JSONLimits.balanced; limits[.stringBytes] = 16 + try resource(.stringBytes) { _ = try JSONGraphProjection.build(document: invalid, profile: profile, limits: limits) } + default: throw CheckFailure.failed(id) + } + } +} + +@main struct GraphPublicConsumer { + static func main() async throws { + for id in 1...16 { + try await GraphPublicCases.run(id) + print("GRAPH_PUBLIC_CHECK GP\(String(format: "%02d", id)) passed") + } + let report = try GraphPublicCases.build() + print("GRAPH_PUBLIC_RESOURCE work=\(report.statistics[.workUnits]) retained=\(report.statistics[.retainedBytes]) peak=\(report.statistics.peakLogicalRetainedBytes) nodes=\(report.statistics[.nodes])") + print("GRAPH_PUBLIC_CONSUMER passed cases=16") + } +} From dcc3bb95543eecc100ad6f31c610dbacb7dc1267 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:12:47 +0100 Subject: [PATCH 23/25] Use the actual public result metric and throwing macro syntax in integration tests --- Tests/JSONIntegrationTests/ConsolidationTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/JSONIntegrationTests/ConsolidationTests.swift b/Tests/JSONIntegrationTests/ConsolidationTests.swift index 44bab11..8a0ad01 100644 --- a/Tests/JSONIntegrationTests/ConsolidationTests.swift +++ b/Tests/JSONIntegrationTests/ConsolidationTests.swift @@ -35,8 +35,8 @@ import JSONInspection #expect(validation.isValid) let program = try JSONTransformProgram.compile(".nodes[].count") let evaluation = try program.withExecution(input: value) { execution in - let first = try #require(execution.next()) - let second = try #require(execution.next()) + let first = try #require(try execution.next()) + let second = try #require(try execution.next()) #expect(first.value == .number(try JSONNumber(parsing: "9007199254740993"))) #expect(second.value == .null) #expect(try execution.next() == nil) @@ -55,7 +55,7 @@ import JSONInspection let value = try JSONValueDSL.array { Element(1); Element(2) } let program = try JSONTransformProgram.compile(".[]") var limits = JSONTransformLimits() - limits[.totalResults] = 1 + limits[.results] = 1 do { _ = try program.withExecution(input: value, limits: limits) { execution in #expect(try execution.next()?.value == 1) @@ -65,13 +65,13 @@ import JSONInspection } catch let failure as JSONTransformFailure { #expect(failure.category == .resource) #expect(failure.code == "limitExceeded") - #expect(failure.metric == "totalResults") + #expect(failure.metric == "results") } } Issue.record("Catching an owned failure reopened scope completion") } catch let failure as JSONTransformFailure { #expect(failure.category == .resource) #expect(failure.code == "limitExceeded") - #expect(failure.metric == "totalResults") + #expect(failure.metric == "results") } } From ed80c389e094fb9d54b640c8e131984c098c7764 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:43:27 +0100 Subject: [PATCH 24/25] Clarify consolidated source authority and bind portable compiler probes to their SDK --- CHANGELOG.md | 16 +++++++- Documentation/Architecture/API.md | 43 ++++++++++----------- Documentation/Qualification/ReleaseLines.md | 4 +- Documentation/README.md | 16 ++++---- README.md | 5 ++- Scripts/ci.py | 2 +- Scripts/qualify_release_1_1.py | 6 +++ Scripts/test_qualification_harness.py | 3 ++ 8 files changed, 59 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd94b4..1f240da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog -## 1.7.0 — Release candidate (unpublished) +## Unreleased — 1.7.0-dev consolidation + +- Reconcile admitted set/matrix APIs with the reviewed public directed graph, + shared JSONTransform compiler/pull VM and optional exact value/schema DSLs. +- Retain package-only configured-store preparation/construction with one original + scope/ledger; no public store, follow or persistence API is introduced. +- Keep twelve libraries plus CLI and thirteen DocC catalogs including the shared + JSONDSLCore reference. Root dependencies and Apple deployment floors remain unchanged. +- Preserve finite exact semantics and all predecessor fixtures, failures and + performance/platform holds. Require fresh combined-source qualification; a merge + is development integration and does not publish a version. +- See [combined qualification](Documentation/Qualification/Consolidation2026.md). + +### Historical JTX10 release-candidate checkpoint (unpublished) + - Add opt-in JSONTransform over the existing shared compiler and pull VM; retain immutable Sendable plans, guarded invocation-owned execution, exact native output diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index 6e5ad79..1becebf 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -1,26 +1,21 @@ # Current API inventory -Unreleased1.7.0-dev consolidation has twelve library products plus swift-json and +Unreleased 1.7.0-dev consolidation has twelve library products plus swift-json and thirteen DocC catalogs including the non-product JSONDSLCore reference. The native compiler graphs define the complete authored inventory. JSONTransform and the trusted-authored DSLs are opt-in; the core products retain their semantic authority. -The stable1.6 inventory and chronological component records below retain their +The stable 1.6 inventory and chronological component records below retain their original scope, not the current combined product count. See [combined qualification](../Qualification/Consolidation2026.md). # Public API contract -The product table below records the published 1.6 line: eight libraries plus the -CLI and eight DocC catalogs. The current unreleased 1.7 source also contains -JSONInspection, as described in the README and structural authority map. This -feature candidate adds JSONTransform as a tenth library and tenth catalog; its -additive contract appears below. Existing published contracts remain unchanged. -Native compiler symbol graphs supply the complete declarations. Examples in the -[README](../../README.md) are compiled and executed by public consumers. - -This document describes the implemented public interface. Native compiler symbol -graphs supply the complete declarations in the eight product DocC catalogs and the shared DSL structure reference. Examples -in the [README](../../README.md) are compiled and executed by the public consumer. +The current product table below describes this development source. Stable 1.6.1 +retains eight libraries plus CLI and eight catalogs. Development adds Inspection, +Transform and both optional authored DSL products, with a shared JSONDSLCore +reference catalog. Existing released contracts remain unchanged. Compiler symbol +graphs supply the complete authored declarations; the README examples are compiled +and executed by public consumers. ## Products and ownership @@ -28,7 +23,8 @@ in the [README](../../README.md) are compiled and executed by the public consume | --- | --- | --- | | JSON | JSON, JSONNumber, JSONObject, JSONParser, JSONWriter, JSONValueEncoder, JSONValueDecoder, JSONPointer, JSONDocument | Shared strict codec, exact decimal model, bounded traversal and native coding | | JSONNavigation | JSONTraversal, JSONTraversalScope, JSONTraversalOptions | Exact many-node DFS, strict physical/virtual scopes, bounded metadata and explicit coverage; one core read-scope meter | - +| JSONInspection | JSONShapeInspector, JSONStructuralSnapshot, JSONGraphProjection | Source-attached observed evidence, immutable exact projections, set/matrix and directed graph views; finite invocation-owned reads | +| JSONTransform | JSONTransformProgram, JSONTransformOutput, JSONTransformExecution | Shared query compiler/pull VM, exact native values, finite guarded execution and typed terminal errors | | JSONValueDSL | JSONValueDSL.object/array, Field, Element, Elements | Trusted authored staging; finite iterative exact finalization | | JSONSchemaDSL | JSONSchemaDSL.document/boolean, Schema, Property, Definition, PatternProperty, DependentSchema, RequiredDependency, Rule | Trusted authored schema declarations; existing document/compiler/validator authority | | JSONBridges | BinaryInteger.jsonNumber, FixedWidthInteger/Double/Decimal.init(jsonNumber:), Double.jsonNumberFromRoundTripText | Optional one-delegate numeric conversions; explicit fidelity, no new authority | @@ -36,19 +32,20 @@ in the [README](../../README.md) are compiled and executed by the public consume | JSONSchemaExamples | JSONSchemaExampleGenerator, JSONSchemaExampleResult | Conservative deterministic witnesses certified by the existing native validator; separate finite preparation and cumulative generation | | JSONStreaming | JSONEventSequence, JSONValueSequence, NDJSONValueSequence, JSONTopLevelArraySequence, JSONAsyncWriter | Exclusive pull state, shared codec, strict framing and awaited handoffs | | JSONPatch | JSONPatch, JSONPatchOperation, JSONPatchResult, JSONMergePatch | Transaction staging, exact comparison and iterative editing | -| swift-json | format, validate, pointer, patch, query, stream, schema, render | Bounded process/file adapters and private CLI engine/filter runtime; no public query library | +| swift-json | format, validate, pointer, patch, query, stream, schema, render | Bounded process/file adapters; the public Transform facade and CLI use the same private query compiler/VM | | JSONHTML | JSONHTMLRenderer, JSONHTMLRenderingOptions, JSONHTMLLimits, JSONHTMLRenderReport, JSONHTMLAnnotation, JSONExportDocument, JSONExportOptions, JSONExportReport | Structured forms/cards and bounded native previews, exact HTML, native Apple transfer, immutable prepared JSON/text/Markdown/static HTML/opt-in styled RTF and cumulative budgets | JSON imports no siblings. JSONSchema depends on JSON and package-private JSONRegexRuntime. Streaming, Patch, HTML, Bridges and Navigation depend on JSON. -JSONSchemaExamples depends only on JSON and JSONSchema. There are nine public products: eight libraries and the swift-json executable, -and no external root-package dependencies. No HTTP, MCP or documentation-tool dependency is introduced. JSONHTML conditionally -uses Apple SDK frameworks for explicit native transfer/PDF preparation; the portable -value, schema, Patch and streaming contracts do not acquire UI semantics. - -JSONRegexRuntime. Streaming, Patch, HTML and Bridges depend on JSON. There are eight public -products and no external root-package dependencies. Runtime code uses no HTTP, -UI, MCP or documentation-tool dependencies. +JSONSchemaExamples depends only on JSON and JSONSchema. JSONInspection depends on +JSON and JSONNavigation. JSONTransform depends on JSON and the package-private +JSONFilterRuntime; JSONCLIEngine uses the same engine. Optional DSL products depend +on JSON/JSONSchema and JSONDSLCore as declared in Package.swift. There are twelve +library products plus swift-json and no external root-package dependencies. The +shared helper has no standalone library product. JSONHTML conditionally uses Apple +SDK frameworks for explicit native transfer/PDF preparation; core value, schema, +Patch and streaming semantics remain portable. Package-only store preparation is +not a public API or a second ledger. No online resolver or runtime dependency is added. Immutable facades store value configuration. Every operation owns its mutable context; every stream iterator requires exclusive access. Sendable follows the diff --git a/Documentation/Qualification/ReleaseLines.md b/Documentation/Qualification/ReleaseLines.md index cc672bd..c2b2326 100644 --- a/Documentation/Qualification/ReleaseLines.md +++ b/Documentation/Qualification/ReleaseLines.md @@ -4,7 +4,7 @@ | --- | --- | --- | | Published 1.6.0 | `f975ac1c5a18af9b422db0994cbc551fd37eee2f` | Eight libraries plus CLI, eight catalogs, 1,077 authored declarations | | Published 1.6.1 (latest stable) | `e431f157ececc0cf3d061192420b0e36a9062f83` | Same eight-library public API and runtime as 1.6.0; no Inspection | -| Development main before candidate integration | Main, retaining N2 admitted at `2b24f47e7405821240cf8dfc66b840c9a7f8efa5` | Twelve libraries plus CLI; thirteen catalogs including shared JSONDSLCore; compiler-derived API inventory required | +| Consolidated development source | Main, retaining N2 admitted at `2b24f47e7405821240cf8dfc66b840c9a7f8efa5` | Twelve libraries plus CLI; thirteen catalogs including shared JSONDSLCore; compiler-derived API inventory required | Source versions do not establish publication. Consult the [official releases](https://github.com/ThorFuchs/Swift-JSON/releases) for published @@ -37,7 +37,7 @@ Stable 1.6.1 has neither. Consolidated development source stays 1.7.0-dev with t performance remains HOLD without speed claims; this is distinct from technical qualification and does not authorize any release/tag. -## Unpublished 1.7.0 candidate +## Historical JTX10 unpublished 1.7.0 candidate The isolated JTX10 successor integrates main `1a0306d076a08c8c43281f91412a11cd744ff3f6` with accepted Transform diff --git a/Documentation/README.md b/Documentation/README.md index 2f1d669..8392303 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,15 +1,16 @@ # Current documentation authority -Unreleased1.7.0-dev consolidation has twelve libraries plus CLI and thirteen DocC -catalogs (including the shared non-product JSONDSLCore reference). Stable1.6.1 +Unreleased 1.7.0-dev consolidation has twelve libraries plus CLI and thirteen DocC +catalogs (including the shared non-product JSONDSLCore reference). Stable 1.6.1 remains eight libraries. [Combined qualification](Qualification/Consolidation2026.md) separates current requirements from all preserved predecessor records below. # Swift-JSON documentation The [release-line matrix](Qualification/ReleaseLines.md) separates the eight-library -1.6 maintenance source from this unpublished ten-library 1.7.0 candidate. -See [candidate qualification and exceptions](Qualification/Release1.7.0.md). The website tracks +1.6 maintenance source from this unreleased twelve-library 1.7.0-dev source. +See [combined qualification and holds](Qualification/Consolidation2026.md); +[the frozen JTX10 candidate](Qualification/Release1.7.0.md) is historical. The website tracks development main; use matching release assets for a stable compiled reference. Start with the [package README](../README.md) for installation, product selection @@ -60,8 +61,8 @@ Online references track main; tagged releases include matching compiled catalogu - [Eight-library compiled source example](../Examples/Consumer/README.md) - [Schema example public API](Architecture/Release1.6/API.md), [resource scope](Architecture/Release1.6/SchemaExamplesLimits.md) and [runnable guide examples](../Examples/SchemaExamplesConsumer/README.md) - [Published Navigation and historical N0/N1 source authority](Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md), [original traversal decisions](Architecture/StructuralIntelligence/API-DECISIONS.md) and [N1 promotion history](Architecture/StructuralIntelligence/N1Promotion.md). JSONNavigation is released in 1.6.0; the original N0/N1 stage gates remain dated history. -- [Unreleased N2 Inspection source](Architecture/StructuralIntelligence/Inspection.md) adds opt-in observed array evidence. This candidate has ten library catalogs including Transform; stable 1.6 release tags and assets retain eight. -- The source authority map distinguishes this unreleased Inspection source from the dated unmerged-candidate record. N3 snapshots, private P1, composition accounting, root reuse, DSL and deployment retain separate source-bound qualification and approval gates. +- [Unreleased N2 Inspection source](Architecture/StructuralIntelligence/Inspection.md) adds opt-in observed array evidence. This development source has thirteen catalogs including the DSL structure reference; stable 1.6 release tags and assets retain eight. +- The source authority map distinguishes this unreleased Inspection source from the dated unmerged-candidate record. Snapshots, public projections and set/matrix APIs are admitted development capabilities. Graph, Transform, authored DSLs and private store construction require combined-source qualification. Composition/root and lower deployment proposals retain their held gates. - [Downstream JSON/JSONSchema interface](ConsumerInterface.md) and [runnable public consumer](../Tools/MCPReadiness/README.md) - [Independent-suite policies and qualification](Qualification/README.md) @@ -83,7 +84,8 @@ from the empty library dependency graph. The candidate also provides [Inspection](../Sources/JSONInspection/Documentation.docc/JSONInspection.md) and [Transform](../Sources/JSONTransform/Documentation.docc/JSONTransform.md): -ten catalogs and 1,214 authored public declarations in total. These products are +their guides are joined by the optional DSL catalogs and shared generic reference. +Derive the complete authored inventory from current compiler graphs. These products are absent from stable 1.6.1. Candidate docs do not establish publication or a platform runtime pass; source-bound qualification receipts define the tested scope. diff --git a/README.md b/README.md index 8d84142..332abd3 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ Historical N0/N1 plans and later candidates are distinguished in the [source authority map](Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md). This development source includes opt-in Inspection, Transform and authored DSLs: -twelve libraries plus the CLI, thirteen products in total. Inspection is absent from stable 1.6 releases. Use this -source checkout for Inspection; its inclusion in main does not publish 1.7.0. +twelve libraries plus the CLI, thirteen products in total. Inspection, Transform and DSLs are absent from stable 1.6 releases. Pin a reviewed +unreleased source commit to use those products; the compatible stable installation +requirement below does not supply them. Integration into main does not publish 1.7.0. The 1.5.1 maintenance repairs remain included: best-effort schema diagnostics, the complete 1.2.1 regression suites and Git-free qualification. See the diff --git a/Scripts/ci.py b/Scripts/ci.py index aeb451e..ec9d29b 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -145,7 +145,7 @@ def commands(lane, output): # Root behavior/corpora remain required. SwiftData and Apple SDK/lifetime # probes are separate Apple acceptance; they never become Linux passes. return [c for c in commands('native', ROOT / output) - if not any(x in c for x in ['Tools/MobileQualification', '--mode', 'Scripts/qualify_cli_ownership.py'])] + [ + if not any(x in c for x in ['Tools/MobileQualification', 'Scripts/qualify_release_1_1.py', 'Scripts/qualify_cli_ownership.py'])] + [ ['python3', '-B', 'Scripts/qualify_linux_reproduction.py', '--output', str(output / 'reproduction')]] if lane == 'linux-safety': # Every behavior case uses Swift Testing. Do not start the generated diff --git a/Scripts/qualify_release_1_1.py b/Scripts/qualify_release_1_1.py index efbfcee..705a004 100644 --- a/Scripts/qualify_release_1_1.py +++ b/Scripts/qualify_release_1_1.py @@ -83,6 +83,12 @@ def run(name, command, expected=0, diagnostic=None): # SwiftPM 6.4's default Swift Build engine publishes native modules/libraries here. products = ROOT / '.build/out/Products/Debug' swift = ['xcrun', 'swiftc', '-swift-version', '6'] + if args.mode in ('ownership', 'reader'): + # Match the actual SwiftPM SDK rather than swiftc's embedded default. + # SDK selection does not lower the deployment or language baseline. + sdk = run('native-sdk-path', ['xcrun', '--sdk', 'macosx', '--show-sdk-path']).strip() + swift += ['-sdk', sdk] + if args.mode == 'ownership': if not (products / 'libJSON.a').is_file(): raise FileNotFoundError('Run swift test first to build the real testable JSON module') module = directory / 'ReaderProbe.swiftmodule' diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 8ae8f7b..83accc2 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -359,6 +359,9 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): self.assertIn('--scope', commands('wasm-qa', output)[1]) self.assertNotIn('--scope', commands('wasm', output)[1]) self.assertEqual(commands('coverage', output), commands('linux-coverage', output)) + linux_full = commands('linux', output) + self.assertEqual(sum('Scripts/qualify_dual_dsl.py' in c for c in linux_full), 1) + self.assertFalse(any('Tools/MobileQualification' in c or 'Scripts/qualify_release_1_1.py' in c for c in linux_full)) self.assertEqual(commands('linux-coverage', output)[1][1:3], ['-B', 'Scripts/coverage.py']) def test_darwin_executable_membership_is_exact(self): From a9e1c80cff29165d3ab7aa4115eff2174cce206c Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:00:55 +0100 Subject: [PATCH 25/25] Preserve scoped catalog validation and reconcile current qualification inventories --- .../SOURCE-AUTHORITY.md | 14 +++++- Documentation/CI/README.md | 6 +-- Documentation/DocumentationTooling.md | 47 ++++++++----------- Documentation/Qualification/README.md | 15 +++++- Scripts/ci.py | 2 +- Scripts/qualify_navigation.py | 4 +- Scripts/test_qualification_harness.py | 30 +++++++++++- 7 files changed, 80 insertions(+), 38 deletions(-) diff --git a/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md b/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md index 9895555..a7011ae 100644 --- a/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md +++ b/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md @@ -1,4 +1,16 @@ -# N0 source authority +# Structural source authority + +## Consolidated development edition — 25 October 2026 + +This unreleased 1.7.0-dev source combines admitted N2/snapshots/projections and +set/matrix APIs with the reviewed public directed graph, Transform and authored +DSL proposals. It has twelve libraries plus CLI and thirteen catalogs including +the shared non-product DSL reference. Store preparation/construction stays +package-only. Fresh [combined qualification](../../Qualification/Consolidation2026.md) +is required; a historical component pass does not qualify the composite. Stable +1.6.1 remains eight libraries with no Inspection. The dated N0/N1/N2 and research +records below are preserved history, including their earlier inventories, gates, +compiler failures and approval boundaries. No release follows from integration. ## Latest stable publication diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index e4678b5..585b0c9 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,9 +1,9 @@ # Current combined-source qualification The 25 October2026 consolidation combines exact set/matrix, graph, Transform and -trusted-authored DSL proposals. Required discovery is953 native /937 Linux /849 -WASI functions,325 native/Linux and288 WASI parameter invocations, and58 native/ -57 portable sharing functions. These are required cardinalities, not observed passes. +trusted-authored DSL and package-only store construction proposals. Required +discovery is 975 native / 959 Linux / 871 WASI functions, 390 native/Linux and +353 WASI parameter invocations, and 60 native / 59 portable sharing functions. These are required cardinalities, not observed passes. All49 core cap probes,16 CLI metrics,126 restoration identities, public consumers, ordinary compiler rejections and existing product lanes stay required. Twelve library products have thirteen catalogs because JSONDSLCore has a shared reference diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index e2c7f47..b34fee2 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -1,17 +1,19 @@ # Building and maintaining the DocC catalogs Swift source comments are the API documentation authority. Native compiler symbol -graphs supply the declarations and links for ten candidate Swift DocC catalogs. Each catalog -combines the product reference with curated Topics and a focused usage guide. - -graphs supply the declarations and links for eight public-product Swift DocC catalogs plus a shared DSL structure reference. Each product catalog combines its reference with curated Topics and a focused -usage guide; the supporting DSL catalog documents shared structural declarations. -See the [Swift DocC documentation](https://www.swift.org/documentation/docc/). +graphs supply declarations and links for thirteen development DocC catalogs: +twelve library references plus the shared non-product JSONDSLCore reference. +Product catalogs combine the reference with curated Topics and usage guides. +Stable 1.6.1 assets retain their separate eight-catalog scope. See the +[Swift DocC documentation](https://www.swift.org/documentation/docc/). ## Build locally Use macOS/SDK 27+, Swift compiler/tools 6.4+ and Swift language mode 6. Select the -supported Xcode through `DEVELOPER_DIR` if it is not the system default. DocC comes +supported Xcode through `DEVELOPER_DIR` if it is not the system default. If the +standalone compiler defaults to a different SDK than SwiftPM, set `SDKROOT` to +the supported path reported by `xcrun --sdk macosx --show-sdk-path`; keep the +selected SDK consistent across compilation and probes. DocC comes with that toolchain; the Python scripts use only their standard library. No extra documentation engine or root-package dependency is required. @@ -22,10 +24,8 @@ python3 Scripts/ci.py --lane docs The lane records native preflight, extracts fresh public symbol graphs, requires summaries and named-parameter documentation on every authored public declaration, checks README Swift fences against their executable consumer source, runs that -consumer, and compiles all ten candidate DocC catalogs with warnings as errors. Zero public - -consumer, and compiles all nine DocC catalogs with warnings as errors. Zero public -symbols, missing comments, stale examples or a failed catalog fail the lane. +consumer, and compiles all thirteen catalogs with warnings as errors. Missing +symbols/comments, stale examples or a failed catalog fail the lane. The lane also executes the standalone schema-examples consumer (15 preserved cases and two expected compiler rejections), plus every Swift block in all five JSONSchemaExamples @@ -46,23 +46,16 @@ statistics; the consumer imports no private meters. | `.build/handoff/docc/JSONNavigation.doccarchive` | Opt-in exact structural traversal and coverage | | `.build/handoff/docc/JSONInspection.doccarchive` | Observed array evidence and row coverage | | `.build/handoff/docc/JSONTransform.doccarchive` | Exact compiled plans, guarded pull scopes, finite reports and typed failures | -| `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | -| `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | -| `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | -| `.build/handoff/docc/compiled-catalogs.tar.gz` | All ten candidate compiled catalogs with their exact native filenames | -| `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Independently verifiable hashes for every archived catalog file | -| `.build/handoff/pages/site/` | Static site, landing page and all ten candidate references under the project base path | - | `.build/handoff/docc/JSONValueDSL.doccarchive` | Trusted value authoring API and guide | | `.build/handoff/docc/JSONSchemaDSL.doccarchive` | Trusted schema authoring API and guide | | `.build/handoff/docc/JSONDSLCore.doccarchive` | Shared structural reference; no standalone product | -| `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | +| `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by module | | `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | | `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | -| `.build/handoff/docc/compiled-catalogs.tar.gz` | All nine compiled catalogs with their exact native filenames | -| `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Independently verifiable hashes for every archived catalog file | -| `.build/handoff/pages/site/` | Static site, landing page and eight public-product references and the supporting DSL reference under the project base path | -| `.build/handoff/pages/receipt.json` | Native transformations, input/output hashes and route/asset verification | +| `.build/handoff/docc/compiled-catalogs.tar.gz` | All thirteen catalogs with exact native filenames | +| `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Hashes for every archived catalog file | +| `.build/handoff/pages/site/` | Static site, product landing page and all thirteen references | +| `.build/handoff/pages/receipt.json` | Transformations, input/output hashes and route/asset verification | Open a `.doccarchive` in Xcode for the compiled reference. To preview a source catalog in a local browser after the lane has produced its symbol graphs: @@ -90,11 +83,9 @@ The manifest and receipt preserve file-level and archive-level digests. The docs lane uses native `docc process-archive transform-for-static-hosting` to prepare each qualified archive under `/Swift-JSON/`. A shared landing -page links all ten candidate references and guides. The build verifies product/guide routes, - -page links all eight product references and guides. The supporting DSL reference is -also deployed at `/Swift-JSON/JSONDSLCore/documentation/jsondslcore/`, without a -product card. The build verifies product/guide and supporting-reference routes, +page links all twelve product references and guides. The supporting DSL reference +is also deployed at `/Swift-JSON/JSONDSLCore/documentation/jsondslcore/`, without +a product card. The build verifies product/guide and supporting-reference routes, local HTML links and assets, rejects private-path/credential markers and symlinks, and records a file manifest. Only the generated site is packaged for Pages; source graphs, command logs and private maintainer records are not website content. diff --git a/Documentation/Qualification/README.md b/Documentation/Qualification/README.md index e6c1394..9edbbf1 100644 --- a/Documentation/Qualification/README.md +++ b/Documentation/Qualification/README.md @@ -1,4 +1,17 @@ -# Qualification +# Current combined-source qualification + +The unreleased consolidation requires 975 native / 959 Linux / 871 WASI functions, +390 native/Linux and 353 WASI parameter invocations, plus 60 native / 59 portable +sharing functions. These are gate cardinalities, not preclaimed passes. Use +[TestDiscovery.json](TestDiscovery.json), actual source-bound receipts and the +[consolidation contract](Consolidation2026.md). All 49 cap probes, independent +fixture policies and 126 restoration identities remain required. Ordinary invalid +schema instances, operational failures and suite policy exclusions remain distinct. + +The chronological notes below retain their original source scope and counts; +they do not describe this combined tree or inherit a pass onto it. + +## Historical qualification checkpoints The 1.5.1 repair candidate restores all 126 functions from the separate 1.2.1 maintenance line without removing the later HTML, bridge, provider or CLI suites. diff --git a/Scripts/ci.py b/Scripts/ci.py index ec9d29b..707114f 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -431,7 +431,7 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=25' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=26' in log.splitlines() return passed, counts def main(): diff --git a/Scripts/qualify_navigation.py b/Scripts/qualify_navigation.py index 4d200fe..893d7b4 100644 --- a/Scripts/qualify_navigation.py +++ b/Scripts/qualify_navigation.py @@ -132,8 +132,8 @@ def run(name, command, expected=None): for path in (ROOT / '.build/out/symbolgraph').glob('JSONNavigation*.symbols.json'): shutil.copyfile(path, graphs / path.name) import documentation_catalog - # The public site separately qualifies the complete seven-library inventory. - documentation_catalog.PRODUCTS = ('JSONNavigation',) + # The public site separately qualifies the complete catalog inventory. + documentation_catalog.CATALOGS = ('JSONNavigation',) coverage = documentation_catalog.verify_comments(documentation_catalog.public_symbols(graphs)) receipt['experimental_api_coverage'] = coverage if coverage['errors']: diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 83accc2..4662219 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -9,6 +9,8 @@ from generate_cli_identity import rendered import tempfile import unittest +from unittest.mock import patch +import documentation_catalog from coverage import executable_objects, shared_objects, validate_export, without_schema_objects from test_discovery import DISCOVERY, ROOT_COUNTS, PARAMETER_INVOCATIONS, check_root, check_restored, RESTORED_FUNCTIONS from ci import check_result, commands, SHARED_TESTS, SHARED_COUNTS @@ -26,6 +28,30 @@ def store_markers(): 'STORE_RUNTIME R%02d passed' % n for n in range(1,11)]) class HarnessTests(unittest.TestCase): + def test_scoped_catalog_check_cannot_weaken_the_full_site_inventory(self): + with tempfile.TemporaryDirectory() as temporary: + root = pathlib.Path(temporary).resolve() + source = root / 'Sources/JSONNavigation/Probe.swift' + source.parent.mkdir(parents=True); source.write_text('public struct Probe {}\n') + graphs = root / 'graphs'; graphs.mkdir() + symbol = dict(location=dict(uri=source.as_uri()), kind=dict(identifier='swift.struct'), + accessLevel='public', identifier=dict(precise='probe'), names=dict(title='Probe'), + pathComponents=['Probe'], docComment=dict(lines=[dict(text='A scoped probe.')])) + graph = graphs / 'JSONNavigation.symbols.json' + graph.write_text(json.dumps(dict(module=dict(name='JSONNavigation'), symbols=[symbol]))) + with patch.object(documentation_catalog, 'ROOT', root): + with self.assertRaisesRegex(ValueError, 'No authored symbols for JSON'): + documentation_catalog.public_symbols(graphs) + with patch.object(documentation_catalog, 'CATALOGS', ('JSONNavigation',)): + symbols = documentation_catalog.public_symbols(graphs) + self.assertEqual(len(symbols), 1) + self.assertEqual(documentation_catalog.verify_comments(symbols)['errors'], []) + symbol['accessLevel'] = 'package' + graph.write_text(json.dumps(dict(module=dict(name='JSONNavigation'), symbols=[symbol]))) + with self.assertRaisesRegex(ValueError, 'Non-public symbol'): + documentation_catalog.public_symbols(graphs) + self.assertEqual(len(documentation_catalog.CATALOGS), 13) + def test_store_requires_exact_engine_vectors_runtime_and_compiler_primary_diagnostics(self): events = dict(function_names=sorted(STORE_FUNCTIONS), module_function_counts={'JSONInspectionTests':12}, parameterized_functions={'storeD1NativeVector(id:)':65}, parameter_case_invocations=65) @@ -338,8 +364,8 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): identity['function_names'][0] = 'JSONTests.unrelatedReplacement()' self.assertFalse(check_restored(identity)) harness = ['python3', '-B', 'Scripts/test_qualification_harness.py'] - self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=25')[0]) - for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=18', 'HARNESS_TESTS passed cases=19', 'HARNESS_TESTS passed cases=20', 'HARNESS_TESTS passed cases=21', '']: + self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=26')[0]) + for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=18', 'HARNESS_TESTS passed cases=19', 'HARNESS_TESTS passed cases=20', 'HARNESS_TESTS passed cases=21', 'HARNESS_TESTS passed cases=25', '']: self.assertFalse(check_result(harness, 0, stale)[0]) lifetime = ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py'] marker = 'SCHEMA_EXAMPLES_CORPUS passed mode=owned scopes=128 values=6 exact=1'