diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 4f286e5..9a2349c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -11,13 +11,13 @@ concurrency: cancel-in-progress: true jobs: qualification: - name: Linux / ${{ matrix.architecture }} / ${{ matrix.lane }} + name: Linux execution / ${{ matrix.architecture }} / ${{ matrix.lane }} runs-on: ${{ matrix.runner }} timeout-minutes: 60 strategy: fail-fast: false matrix: - lane: [linux, linux-safety] + lane: [linux, linux-safety, composition] architecture: [x86_64, arm64] include: - architecture: x86_64 @@ -47,9 +47,6 @@ jobs: 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: Public composition and reference imports - if: matrix.lane == 'linux' - run: python3 -B Scripts/ci.py --lane composition - name: Preserve Linux receipts if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 @@ -60,3 +57,21 @@ jobs: path: | .build/ci/ !.build/ci/**/reproduction/extracted/** + required: + # Both architectures and all three lanes must succeed. Preserve the four + # existing context names without converting an incomplete matrix to green. + name: Linux / ${{ matrix.architecture }} / ${{ matrix.lane }} + needs: qualification + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + lane: [linux, linux-safety] + architecture: [x86_64, arm64] + steps: + - name: Require complete qualification + env: + QUALIFICATION_RESULT: ${{ needs.qualification.result }} + run: test "$QUALIFICATION_RESULT" = success diff --git a/.github/workflows/native.yml b/.github/workflows/native.yml index 2f1e707..efed818 100644 --- a/.github/workflows/native.yml +++ b/.github/workflows/native.yml @@ -11,13 +11,13 @@ concurrency: cancel-in-progress: true jobs: qualification: - name: Native / ${{ matrix.lane }} + name: Native execution / ${{ matrix.lane }} runs-on: xcode-27 timeout-minutes: 60 strategy: fail-fast: false matrix: - lane: [native, safety, docs, archive] + lane: [native, safety, docs, archive, composition] env: DEVELOPER_DIR: /Applications/Xcode_27.2.app/Contents/Developer PYTHONDONTWRITEBYTECODE: '1' @@ -34,9 +34,6 @@ jobs: - run: python3 -B Scripts/ci.py --lane '${{ matrix.lane }}' env: SWIFT_JSON_RELEASE_METADATA: ${{ github.workspace }}/.build/handoff/pages/latest-release.json - - name: Public composition and reference imports - if: matrix.lane == 'native' - run: python3 -B Scripts/ci.py --lane composition - name: Preserve native receipts if: always() uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 @@ -63,10 +60,27 @@ jobs: with: path: .build/handoff/pages/site include-hidden-files: true # Preserve the verified .nojekyll file; only the generated site is uploaded. + required: + # Preserve protected context names. Every execution lane, including + # composition, must succeed; a failed/cancelled/skipped dependency fails here. + name: Native / ${{ matrix.lane }} + needs: qualification + if: ${{ always() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + lane: [native, safety, docs, archive] + steps: + - name: Require complete qualification + env: + QUALIFICATION_RESULT: ${{ needs.qualification.result }} + run: test "$QUALIFICATION_RESULT" = success publish: name: Documentation / publish if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - needs: qualification + needs: [qualification, required] # Native compilation and all qualification happened in the jobs above. # This job only publishes their exact static artifact. runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index b0ca32d..b414076 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,9 +1,9 @@ # Contributor instructions -Prepared release source is 1.7.0; publication is a separate owner action. Its -parent development line is 1.7.0-dev and stable 1.6.1 remains separate. This is -not a maintenance release or the frozen 6fe8067 RC. Preserve all source-bound -qualification history; source integration never publishes a version. +Development source is 1.8.0-dev, based on published 1.7.0 at +8557badef562cf2f059e964c113d4a7f60ecbe6c. Stable 1.6.1 remains a separate +maintenance line. Preserve every source-bound qualification record. No 1.8 +merge/tag/publication follows from implementation or passing tests. 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 @@ -50,13 +50,24 @@ 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. NAV10 set/matrix and NAV11 directed-simple-graph APIs are development capabilities inside JSONInspection; no graph product, mutation or serializer is introduced. -Private NAV12 store construction/preparation remains package-only and shares one -original invocation ledger; no public store/follow API or persistence is exposed. -Wider retrieval/persistence, NAV13 batches and JSON text-sequence framing remain -separate, unadmitted work. +The 1.8 candidate promotes the checked immutable configured store inside +JSONInspection and adds ordered path/snapshot/store value batches, exact-ID lookup +and bounded complete BFS follow. Preserve the original construction phases and +one invocation-owned ledger per operation. Positions remain unforgeable and +source-instance-local. No persistence or mutable store is exposed. See +Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md. +Wider retrieval/persistence and JSON text-sequence framing remain separate work. JSONTransform uses one private compiler/VM and the CLI's accounted adapter. Preserve its accepted 42 authored declarations and exact semantics. The original 1.7.0 P4 waiver and later partial measurements are historical scoped evidence; no allocation, speed, RSS or controlled-performance pass follows from consolidation. See Documentation/Qualification/Consolidation2026.md for combined gates and holds. + +The commissioned 1.8 Transform binding train adds a source-definition codec and +compile reports while preserving those original declarations. Sprint 1 +retains its historical test-only selection experiment. Sprint 2 adds public +guarded selection scopes, owner-bound positions and EOF completion reports; +preserve ordinary Transform semantics and require one cumulative invocation ledger. +See Documentation/Architecture/Release1.8/TransformBindings.md. No Content code, +lock, message, merge or publication is authorized by implementing this sprint. diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe6eb8..988c5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## Unreleased — 1.8.0-dev + +- Add exact versioned Transform source definitions, a bounded native codec and + successful compilation reports through the existing compiler. +- Add finite indexed selection scopes over explicit populations, owner-bound + positions and private-constructor EOF/accounting witnesses. Admission, projection, + VM evaluation, provider/output handling and completion share one ledger. + Preserve order/repeats, first failures and terminal lease accounting; ordinary + Transform/CLI semantics and all thirteen products remain unchanged. + +- Extend public acceptance across schema validation, store/transform reads and + checked Patch rebuilds, with exact numeric/scalar identity and lifecycle cases. +- Add a frozen capacity/owned-counter matrix and compiled cross-product recipes; + distinguish diagnostic refusals from completed cells and logical retention from + timing, RSS and allocations. Library APIs/algorithms and default policies unchanged. + +- Add ordered typed-path, snapshot-ordinal and store-position value batches using + one cumulative native ledger per batch, complete source admission and atomic results. +- Promote the original checked configured-store profile/construction inside + JSONInspection; add exact-ID lookup and finite complete BFS reference following. +- Preserve exact numbers/scalar identity, unforgeable source-local positions, + explicit absent/null/link states and original construction error precedence. +- Add external SwiftPM examples, ordinary compiler refusals, boundary/lifecycle/ + concurrency tests and exact scoped CI discovery. Qualification remains source-bound; + no 1.8 release or performance/platform pass is implied. + +## Publication note — 28 October 2026 + +Version 1.7.0 was published from 8557badef562cf2f059e964c113d4a7f60ecbe6c. +The preparation records below retain their original historical wording. + ## 1.7.0 — prepared source; publication separate - Reconcile admitted set/matrix APIs with the reviewed public directed graph, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfa44f0..9171704 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,15 @@ +# 1.8 scoped workflow qualification + +This isolated development source is 1.8.0-dev from published 1.7.0. +Run `python3 Scripts/ci.py --lane structural-workflows` for exact Debug/Release +behavior, preserved store cases and public compiler/consumer checks. M2 adds +`--lane structural-acceptance` and `--lane structural-acceptance-safety` for exact +public cross-product cases, deterministic capacity observations and their safety +checks. These extend the existing gates. Run +`--lane structural-workflows-safety` for scoped ASan, shared-state TSan and strict +memory checks; complete native/Linux/docs/WASI gates still apply before release. +Do not infer a pass when storage or an actual supported runtime is unavailable. + # Contributing Choose the source line deliberately: maintenance changes use the 1.6 line; diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index c8e167f..885c757 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -1,3 +1,22 @@ +# 1.8 additive API candidate + +The latest released source is 1.7.0 (8557bade); this feature source is 1.8.0-dev. +No product or dependency is added. JSONInspection now includes +`JSONInspectionBatch`, snapshot ordinal batches, public `JSONStoreBuilder`/ +`JSONConfiguredStore` and typed profile/metadata/error declarations, exact-ID +lookup, position value batches and finite complete BFS follow. +See [the exact behavior/ownership boundary](StructuralIntelligence/BoundedWorkflows1.8.md) +and [qualification](../Qualification/Release1.8.md). +The optional JSONTransform candidate also adds immutable versioned source definitions, +a bounded native codec and successful compilation reports. Finite public selection +scopes produce owner-bound positions and private-constructor EOF completion reports +through the same compiler/VM. See the [binding contract](Release1.8/TransformBindings.md) +and [selection qualification](../Qualification/TransformSelection1.8.md). No released +1.7 signature or ordinary execution/CLI semantics change. +Navigation public signatures stay unchanged; its new selection capability is +package-only and uses the original admitted scope. Earlier inventories below +remain historical; compiler graphs must establish the final additive surface. + # Current API inventory Unreleased 1.7.0-dev consolidation has twelve library products plus swift-json and diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index e174b41..4e48765 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -355,6 +355,17 @@ "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" + }, + { + "component": "1.8 bounded structural workflows", + "source": "Original Swift-JSON implementation over the existing native prepared traversal and NAV12 checked store construction", + "paths": [ + "Sources/JSONInspection/JSONInspectionBatch.swift", + "Sources/JSONInspection/Store/JSONStoreQueries.swift", + "Examples/BoundedWorkflows" + ], + "validation": "Exact Debug/Release behavior, preserved 65 store vectors, public SwiftPM consumer, codegen negatives and safety/docs/platform gates; see Documentation/Qualification/Release1.8.md for actual status", + "dependencies": "No new dependencies or numeric/key/schema authority; Apache-2.0 original contributions" } ], "original_implementation_notes": [ diff --git a/Documentation/Architecture/Release1.8/TransformBindings.md b/Documentation/Architecture/Release1.8/TransformBindings.md new file mode 100644 index 0000000..d93a155 --- /dev/null +++ b/Documentation/Architecture/Release1.8/TransformBindings.md @@ -0,0 +1,144 @@ +# Transform bindings — commissioned 1.8 candidate + +The maintainer commissioned the source-definition codec, indexed selection and +compile reporting on 29 October 2026. Base is b8ddeb26042005a06360f50fcf20b1f840e66d1c. +This additive train preserves the existing 42 authored Transform declarations, +ordinary execution/CLI semantics, thirteen products and empty root dependencies. +Implementation and a successful spike are not release or downstream admission. + + +## Sprint 2 current selection contract + +The maintainer commissioned the public selection increment on 29 October 2026. +Its exact base is Sprint 1 commit 70df9ba8d47322abf0d4aec1439256d7a5e97e94. +Historical Sprint 1 scope/evidence below remain source-bound. This increment +ships the selection facade in JSONTransform without a new product/dependency or +changes to the private compiler/VM, ordinary execution or CLI adapters. + +Selection convention V1 is explicit: input [0,...,N-1], reserved declared `$values` +containing the admitted population, and exactly the other declared bindings from +the caller. Overriding values or missing/foreign/invalid binding names fails. +The finite population default is 65,536; zero is valid. Native and Transform +limits remain additional ceilings, including live collection slots. Projection +slots/text/metadata and numeric construction are admitted before owned growth. +Population admission occurs once, including unused tails, on the same context as +projection, VM work, native index validation, output leases and completion. + +Optional input/inputs callbacks use existing query semantics and explicit caller +code. They cannot expand the declared population. Nil is provider EOF, null is a +value. Provider admission/calls triggered by evaluation consume the same finite +input/work/retention policy; arbitrary callback instructions and prior caller +allocations remain caller work. A separate health cell supplies prepaid entry +snapshots and prevents context borrowing during reentry. Provider/reentry/cancel +failures latch before accepting another value or another callback. + +Public positions retain one shared population owner and a per-output slot lease. +The population owner includes unforgeable invocation identity. Completion exists +only after explicit evaluator EOF and successful cleanup; its identity/report +lease does not retain the population. Completion.ordinal(of:) rejects foreign +positions through constant metadata inspection, without reopening a completed +operation. Raw ordinals/caller mappings are not stable IDs or read grants. + +Boundary invalidity bypasses query catches. Uncaught language, resource, provider, +cancellation and lifecycle failures latch. Caught errors, abandoned prefixes and +late failures cannot certify. A first operational failure wins over a later caller +error; otherwise caller errors retain identity. Escaped scopes/closures cannot +resume after close. report() drains deferred refunds after success/error/cancel; +work never refunds. Completion.statistics is a close-time snapshot; current +terminal accounting is observed through the guarded scope. Scope/position/ +completion handles are deliberately non-Sendable; plans remain checked Sendable. + +Completion, report, scope and position constructors are inaccessible to public +callers. The generic JSONOperationReport initializer cannot construct a selection +witness. No provenance propagation, second interpreter, VM serialization, online +resolution, Content code/lock or unbounded streaming/sort promise is introduced. +See the [public guide](../../../Sources/JSONTransform/Documentation.docc/IndexedSelection.md) for usage. + +## Definition V1 + +JSONTransformProgramDefinition stores exact source and unique ASCII variable names +in ascending UTF-8 order. It does not add source storage to compiled plans. There +is no synthesized String-based equality/hash or serialized VM/host state. + +Canonical encoding is compact UTF-8 without a BOM or newline, with fields in this +order: definition, version, dialect, source, variables. Their values are +"swift-json-transform-source", mathematical integer 1, "swift-json-query-v1", the +exact source string, and the ordered name array. Native JSONWriter supplies all +escaping: direct non-ASCII UTF-8, escaped quote/backslash, lowercase control-byte +escapes. This format is distinct from an implementation digest and execution policy. + +Decoding accepts one strict native JSON document with exactly those five decoded +keys and correct field types. Equivalent JSON escaping, field order, whitespace +and numeric spelling of version 1 are accepted; re-encoding is canonical. Unknown +fields, duplicate decoded keys/names and unknown versions/dialects fail. This is +strict semantic decoding, not a canonical-input assertion. A storage gateway that +requires canonical input must compare against bounded canonical output under its +own explicitly reviewed policy. Query syntax is checked by the existing compiler, +not the codec. Names use [A-Za-z_][A-Za-z0-9_]* without dollar prefixes and are +validated before any Set conversion. No credentials, files, closures, Content +objects, SCCV/SCCD data, artifact hashes or online resolution are encoded. + +The finite codec defaults are source 1 MiB, 128 names, 128 bytes/name, 8192 total name +bytes and 2 MiB wire bytes, alongside the existing balanced JSON limits. Native work +meters sorting, parsing, scanning, construction and writing; retention is reserved +before owned growth. Limits are checked at invocation entry; helpers share that +one meter. Existing 49 core policies and their probes are unchanged. Logical bytes +are reservations, not RSS. Supplied input storage and later caller copies are caller +work. Prepare, decode, encode and compile are explicitly separate operations; no +decode-and-compile convenience secretly restarts a single composed invocation. + +Successful preparation/codec operations return native reports. Compilation uses +the existing compiler/context and returns actual native work. The additive +compileWithReport path preserves old compile behavior; the definition adapter +also charges its owned variable Set construction. No public meter is exposed. + +## Historical Sprint 1 selection plan + +The proposed convention is an invocation-owned ordered array [0,...,N-1] as input +and a reserved explicit values binding containing the complete population. The +selection convention needs its own revision, separate from the language/wire +revisions. Reserved-name collisions and provider policy must be frozen before +public selection API admission. Content-specific mapping/identity stays outside. + +The existing query engine alone filters/sorts; it must explicitly emit numeric +ordinals. Native mathematical integer conversion admits 42.0 and negative zero as +their exact integer identities; fractions, negative values, other kinds, overflow, +native-width failure and out-of-population values fail. Output order/repeats are +preserved. Deterministic tie keys belong in the program; no hidden deduplication. + +One invocation must own population admission, index construction, VM evaluation, +numeric checks and typed output slots. Position handles need an unforgeable owner +and a joint population lease even when the numeric output was newly computed. +An explicit EOF state is required to certify completion. A caught boundary error +or abandoned prefix cannot produce a completion witness. Generic JSONOperationReport +has a public pairing initializer and is not that witness; public completion will +need a private initializer and owner-produced statistics. Query catches cannot +swallow boundary failures. First cause, cancellation and terminal lease refunds +must remain observable without reopening execution. Handles stay guarded and +non-Sendable; immutable plans/definitions remain checked Sendable. + +Sprint 1 keeps this experiment solely in IndexedSelectionSpike.swift in the test +target. It reuses FilterVM/TransformRuntimeContext and native values; it is not a +second interpreter or a shipped selection facade. Its 1024 population bound and +test provider are experimental, not approved public defaults/provider semantics. +Full reentry/public compiler-negative/lifetime/profile qualification belongs to +the next increment. General provenance propagation, persistence of executions, +unbounded input/sort guarantees and Content implementation are outside scope. + +## Qualification and admission + +Run Scripts/ci.py lanes transform-bindings and transform-bindings-safety for this +sprint's actual cases and public consumer. Preserve the original 23 Transform cases, +all old suites/fixtures and CLI checks. Source/CLI identity, public declarations, +native strict-memory checks and DocC must bind the final checkpoint. Full Native, +Linux ARM64/x86_64, WASI and relevant Apple checks are required for the eventual +release; unavailable/unrun lanes stay open. Earlier 1.8 receipts and 1.7 performance +waivers do not qualify these source changes. Existing performance/allocation holds +remain. No merge, tag, publication or cross-session message is commissioned here. + +The downstream S06/S13 profile mapping and deliberate Content lock selection remain +separate decisions after a qualified public binding. M0 stays independent; no S04 +history, Content repository, source pin or admission gate is changed by this train. + +The [Sprint 1 qualification record](../../Qualification/TransformBindings1.8.md) +separates completed local checks from the remaining public-selection/release gates. diff --git a/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md b/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md new file mode 100644 index 0000000..77c6890 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md @@ -0,0 +1,80 @@ +# 1.8 bounded structural workflows + +Implementation contract for the maintainer-authorized 1.8 feature branch from +published1.7.0 (8557bade). This is an additive implementation, not merge or release +approval. Original NAV12 construction fixtures, failures and qualification retain +their source-bound historical meaning; public promotion adds its own evidence. + +## Batch reads + +JSONInspectionBatch returns exact values for ordered typed paths. Snapshot values +accept ordered snapshot-local ordinals. Store values accept owner-bound positions. +Repeats are preserved and charged; empty requests still admit the complete source. +An invocation returns all results or throws, never publishes a partial result and +never substitutes null for an invalid selection. Explicit JSON null remains a value. + +Each batch creates one original native read/traversal scope. Profile/request work, +one complete canonical source/source-map admission, every selection and output +slot share its ledger. It does not loop over independently admitted public reads. +Request counts are inclusively capped (default1024); native path/work/retention/ +cancellation caps remain. Output JSON aliases follow existing requiredValue/read +semantics; subsequent caller copies/work are outside library-owned quotas. Logical +slot accounting is conservative policy, not heap/RSS or throughput measurement. + +## Immutable public configured store + +Promote the existing typed closed construction profile and checked store facade +inside JSONInspection; no new product, parser, database, resolver or index engine. +References stay disabled unless explicitly configured. Strict markers retain exact +collection/id spelling; only declared fields are interpreted. Preserve all eight +construction phases, duplicate/error precedence, first/second locations, source +spans, source-local owner tokens and the original one-ledger preparation/admission. +The twelve existing construction ceilings remain finite policies, not calibrated +joint capacity guarantees; the49 core metrics and all fixtures remain unchanged. + +Fixed record/collection/reference metadata access is trusted bounded-memory access. +Lookup, value batches and follow are separately reported operations, independent +of construction. Each admits the captured source once and reserves its immutable +metadata aliases; helpers cannot reset budgets or attach another source. Lookup +uses decoded scalar names/IDs, returns nil for a missing ID in a known collection, +and throws unknownCollection for an undeclared collection. Position construction +and the fully admitted store initializer remain inaccessible to external callers. + +Follow is deterministic breadth-first over configured link slots in field order, +including its starting position exactly once. Absent/null slots remain distinct +metadata and contribute no edge. Cycles use visited physical positions, not string +normalization or recursive callbacks. Depth, frontier, visited and result ceilings +are inclusive and finite. An unvisited reachable node exceeding any ceiling throws; +no depth-truncated or partially successful closure is returned. Owner mismatch is +an error. This operation performs no I/O and creates no persistent identity. + +## Compatibility and qualification + +Existing singular reads/traversal/graph/Transform semantics and public signatures +remain unchanged. A caller pipeline is not implicitly one global ledger. New +source needs actual Debug/Release, public consumer and compiler-boundary evidence, +cancellation/late-failure/resource cases, supported safety/platform lanes and DocC. +Update exact discovery identities/cardinalities rather than relaxing them. Reuse +original independent fixtures and49 caps; add explicit new batch/follow policy +cases. Keep partial profiling/G4 HOLD and the1.7-only P4 exception historical; +1.8 performance is unqualified until measured or explicitly dispositioned. + +No persistence, public mutable store, online lookup, new query grammar, lower +platform floor, parallel batch execution or wider retrieval belongs to this wave. + +## M2 application acceptance and counter observations + +Schema validation, store construction, reads, transformation and Patch remain +separate public operations with independent reports. A recipe does not attach a +replacement ledger or claim a pipeline-wide budget. Ordinary schema invalidity, +operational quota failure and root removal remain distinct. Patches require a +new source wrapper/store; old positions and source spans do not silently migrate. +Accepted bytes of an awaited sink cannot be recalled after cancellation. + +The authored public examples and Integration behavior cases qualify those +boundaries without changing any engine API. The capacity matrix in +Documentation/Qualification/StructuralWorkflows1.8Matrix.json records fixed +fixtures, exact boundaries and separately reported diagnostic refusals. Its +counter comparisons are not elapsed-time, allocation or RSS evidence. Existing +49 core metrics, twelve construction caps, profile defaults, fixture identities, +performance review trigger and controlled-performance holds are unchanged. diff --git a/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md b/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md index a7011ae..5d1582f 100644 --- a/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md +++ b/Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md @@ -1,3 +1,12 @@ +# 1.8 source authority — 28 October 2026 + +The 1.8 candidate starts at published 1.7.0 commit +`8557badef562cf2f059e964c113d4a7f60ecbe6c`, tree +`c914b8914c9130a3a07a3ed82fb41dde33c37402`. The maintainer commissioned +bounded batches and the immutable public store; see [the contract](BoundedWorkflows1.8.md). +Earlier private-only/pending status remains truthful for the dated sources below. +Qualification and merge/release acceptance of this successor remain separate. + # Structural source authority ## Consolidated development edition — 25 October 2026 diff --git a/Documentation/Qualification/MaintenanceCI.md b/Documentation/Qualification/MaintenanceCI.md index 0e4d8fc..9058f8c 100644 --- a/Documentation/Qualification/MaintenanceCI.md +++ b/Documentation/Qualification/MaintenanceCI.md @@ -19,6 +19,27 @@ keeps the existing main-only condition; maintenance pushes must not replace the development website. A skipped Pages publication on a maintenance branch is expected. Required product qualification must still execute and pass. +Composition has a separate execution matrix lane on macOS and on each Linux +architecture. The native, safety, docs, archive and composition commands retain +their existing 60-minute execution limits; no command or fixture is omitted. +The existing `Native / ...` and `Linux / ...` check names are final gates over +their complete workflow execution matrix. They run even after a dependency fails +and accept only its `success` result. Failure, cancellation, a skipped execution +or a missing result cannot produce a successful required context. Consequently, +one failed execution makes all of that workflow's final gates fail. Inspect the +`Native execution / ...` or `Linux execution / ...` job and its receipts for the +specific cause. Pages also waits for the final native gates, with its main-only +condition unchanged. No repository protection setting is changed by scheduling. + +This separation follows the 1.8 PR74 execution at `48dca4d`: the normal native and +both Linux lanes passed, but the native and x86 Linux jobs reached their one-hour +job limits during the appended composition step. Native's nine-command composition +receipt had completed just before cancellation; x86 had completed only five of +nine commands. Preserve those cancelled checks and partial receipts. They are not +retroactively green, and the successor must qualify its actual source. Moving +the unchanged composition commands to a separate job does not waive them or +extend a timeout. It does not qualify held runtime/performance/allocation gates. + If an expected automatic run is absent, inspect the branch/event/commit and workflow filters. Existing manual dispatch may qualify the exact source while investigation proceeds, but it does not prove that automatic triggering works. diff --git a/Documentation/Qualification/Release1.8.md b/Documentation/Qualification/Release1.8.md new file mode 100644 index 0000000..daff11a --- /dev/null +++ b/Documentation/Qualification/Release1.8.md @@ -0,0 +1,160 @@ +# Milestone 2 — cross-product acceptance and capacity observations + +M2 is an additive qualification increment from local checkpoint +`07ed28095dde5e8063570aa6a2436039b5bd43ca`, tree +`5e0961a13fa4b8746854beca23a424db3075ff96`. The sealed M1 archive/receipts remain +unchanged. No public library signature, algorithm, root manifest, default profile +or fixture comparator is changed by this increment. A new guide updates the +existing catalog and therefore requires regenerated CLI source identity. + +The completed local M2 receipts pass all43 Integration functions in Debug/Release +and ASan, the new sharing TSan function and strict test-target compilation. The +existing49-cap public test passes every exact expected probe. Six new public +consumer witnesses/configuration, the original twelve and their ordinary compiler +refusals pass; all16 counter/capacity cells complete with exact boundary proofs. +All13 native DocC catalogs compile with warnings as errors, and1,639 authored +public declarations/parameters are documented. Compared to published1.7.0, only +Inspection has139 additive declarations from M1; no existing declaration fragment +or identity changes in the complete package graph. Native pinned qualification +observes318 syntax cases (287 evaluated successes,31 policy rejections) and2,337 +schema cases (2,300 evaluated successes,37 policy exclusions), with82 authored +semantics,9 budget cases and4,396 deterministic malformed replays, no failures. +The full native/safety/docs CI lanes and platform/runtime/performance gates remain +separate; these local results do not publish or admit1.8. + +This completion note is a documentary successor to the raw receipts. Final private +binding records its one-path delta; the earlier safety receipt also precedes the +ci.py addition of the49-cap native command. Its three safety commands, product/test/ +fixture bytes and flags are unchanged and rechecked by the current exact harness. +Original failures and M1 evidence remain intact. This is scoped agent review and +native qualification, not independent human release approval. + +Nine named public-import Integration tests cover schema/store/transform/Patch +composition, exact bytes/scalar collection names, source-map/owner replacement, +dangling-link refusal, terminal accounting, accepted sink prefixes after cancellation +and independent shared-plan health. Native/portable discovery is now +1,008/992 functions,390 parameter invocations; WASI expects904/353. Integration +has43 functions and12 sharing cases; combined sharing62/61. These are discovery +requirements, not claims that all platform lanes have run on this source. + +The expanded public SwiftPM example adds six cross-product witnesses per +configuration, preserving all twelve M1 witnesses and compiler refusals. The +[capacity matrix](StructuralWorkflows1.8Matrix.json) freezes16 cells and five +independently hashed fixtures. Batches use a fixed eight-record source and +1/8/64/512 requests; follow uses a growing chain and explicit finite bounds. +Exact request, work and retention boundaries are checked. Independent singular +reads report sums of cumulative counts and maxima of invocation retention, never +summed process memory. Diagnostic typed refusals are separate incomplete capacity +observations, not successes. No timing, allocations, RSS or speedup is measured. + +Run `Scripts/ci.py --lane structural-acceptance` and +`--lane structural-acceptance-safety`; full existing native/Linux/safety/docs gates +remain. Actual M2 outcomes belong in source-bound private receipts under +Artifacts/Release1.8M2. Compiler/fixture reconciliation failures are retained. +M2 also executes the existing49-cap public qualification test, preserving its +exact expected outcomes. The full43-function Integration target is checked in +Debug/Release/ASan; its original names/assertions remain. Strict test-target +compilation uses narrowly scoped acknowledgements for three preexisting +UnsafeCurrentTask callback accesses; those handles never escape or cross tasks. +No compiler flag or unsafe production annotation is added. The new cancellation +case instead uses a safe Task handle and checked-continuation rendezvous. + +The M1 record below remains historical. Broad QA/performance/publication gates +remain pending until their actual outcomes are recorded; no1.7 waiver is inherited. + +# 1.8 bounded structural workflow qualification + +Status: implemented local milestone, broader release qualification PARTIAL, on +isolated `codex/1.8-batches-and-store`. +No merge/tag/release/publication is commissioned or claimed. Source base is +published 1.7.0 `8557badef562cf2f059e964c113d4a7f60ecbe6c`, tree +`c914b8914c9130a3a07a3ed82fb41dde33c37402`. + +The [contract](../Architecture/StructuralIntelligence/BoundedWorkflows1.8.md) +and [compiled public example](../../Examples/BoundedWorkflows) define this additive +wave. Twelve library products plus CLI, thirteen catalogs, empty root dependencies, +Swift6.4/language6 and Apple27 minima remain. Original NAV12 fixtures and49 metric +policies are unchanged. Current discovery adds24 named nonparameterized behavior +functions and one shared-state function to JSONInspection (214 roots,11 shared). +Original390 native/portable and353 WASI parameter invocations are unchanged. + +## Milestone evidence + +Private raw receipts stay in ignored `Artifacts/Release1.8` in the feature worktree. +Initial compiler reconciliation passed `swift build --target JSONInspection --jobs 2` +and20 then22 targeted native behavior functions. Those were unfrozen-source probes, +not final qualification. The independent sibling review requested native path-limit +admission on store reads and deterministic inner-loop cancellation. Both are +addressed in source; the final24 cases also pin nested snapshot scratch cleanup, +metadata aliases, self-cycles and narrow chains. Final gates must record exact +source manifests/commands/environments/events; no index entry implies a pass. + +## Completed local milestone — 28 October 2026 + +Apple Swift6.4.2, Xcode27.2, macOS/SDK27 supported native preflight passed. +`Scripts/ci.py --lane structural-workflows` passed:24 exact new functions in +Debug and Release, original12 store functions with65 native-build parameter +invocations plus the separate strict-parser case in each configuration, preserved +26-command store compiler/consumer receipt, and12 public SwiftPM witnesses per +configuration. The new public qualifier also compiles the DocC example and six +ordinary public-boundary refusals per configuration. Policy/compile-only vectors +remain distinct from native evaluated outcomes. + +`--lane structural-workflows-safety` passed24 ASan functions, one shared-state +TSan function and strict-memory compilation. Supplemental exact-event regression +passed all57 Navigation and214 Inspection functions in Debug/Release. The existing +CLI version case passed. The harness passed28 checks; site regressions passed12. +The affected Inspection DocC catalog compiled with warnings as errors. Compiled +public graphs contain301 authored Inspection declarations (139 added, none removed) +and56 Navigation declarations (unchanged), all documented. Three legacy Self versus +concrete-owner graph renderings were explicitly compared against unchanged owning +source files; no byte-identical graph claim is made. Full13-module extraction remains +pending. The independent sibling source review closed its path-limit/cancellation +findings; this is scoped agent review, not human release approval. + +Preserve the original failed external-consumer attempt: its fixture placed a null +field outside the configured collections object and was correctly refused as an +unconfigured collection. The harness trapped on that uncaught error; it was not a +library/compiler crash. The successor scopes collections explicitly and passes. +Original attempt logs and source manifests remain in private evidence. + +This status update changes only this qualification document after the passing +runtime receipts. Final checkpoint evidence must show that all product, test, +fixture, DocC and harness bytes match those receipts, and record the documentation +successor separately. No broader qualification pass follows from these local results. + +## Required gates + +- Scoped Debug/Release: `Scripts/ci.py --lane structural-workflows`, including + original12 store functions/65 vectors, ordinary compiler refusals, public + SwiftPM consumer and compiled DocC example. +- Scoped safety: `--lane structural-workflows-safety` (ASan24, TSan1, strict memory). +- Complete native, safety and docs lanes, current public API graphs and no + removed public declarations; preserve Navigation's original56 and Transform42. +- ARM64/x86_64 Linux, supported WASI and Apple SDK/runtime scopes on the final + source. Unavailable environments/storage remain blocked, not inherited passes. +- Clean source inventory, deterministic archive and Git-free external consumer. +- Measured capacity/performance before any improvement claim. Original1.7 P4 + waiver, loaded-host timing, partial Instruments profiles, streaming disposition + and graph G4 HOLD remain historical and cannot qualify changed1.8 source. + +The maintainer chose to continue implementation without reclaiming other +sessions' files. At kickoff about1.4GiB was free; fresh broad safety/docs/platform +builds may remain storage-blocked. Record actual attempts and unrun commands in +the private milestone report. No cleanup, weaker check or gate waiver is implied. + +## Read-only store decision + +The public surface promotes typed configuration, immutable admitted store/metadata, +owner-bound positions and typed failures. New operations are exact-ID lookup, +ordered value reads and complete finite BFS follow. Construction uses the original +eight phases, one exclusive ledger, duplicate timing and marker/target precedence. +The prior compiler-negative asserting profile invisibility is superseded by an +external private-read-scope refusal; the other original seven negatives and all +65 original vectors remain. Store/position forging is still forbidden. This is +an intentional additive promotion; it adds no product or runtime dependency. + +Fixed metadata access and supplied callbacks are trusted caller work. Each later +query is independent of construction and other queries. Logical retained bytes +are conservative owned reservations, not process memory. No persistence, mutable +store, online resolution, asynchronous batch scheduler or durable token exists. diff --git a/Documentation/Qualification/ReleaseLines.md b/Documentation/Qualification/ReleaseLines.md index 40adade..c575737 100644 --- a/Documentation/Qualification/ReleaseLines.md +++ b/Documentation/Qualification/ReleaseLines.md @@ -1,3 +1,13 @@ +# Current source lines — 28 October 2026 + +Latest published stable is [1.7.0](https://github.com/ThorFuchs/Swift-JSON/releases/tag/1.7.0), +commit `8557badef562cf2f059e964c113d4a7f60ecbe6c`, tree +`c914b8914c9130a3a07a3ed82fb41dde33c37402`: twelve libraries plus CLI, +thirteen catalogs, 1,500 authored declarations. The isolated 1.8.0-dev successor +keeps those products and adds bounded Inspection batches/store operations. +See [its separate qualification](Release1.8.md). 1.6.1 stays the eight-library +maintenance line. The dated preparation inventories below remain history. + # Stable and development source lines | Line | Source identity | Public scope | diff --git a/Documentation/Qualification/StructuralWorkflows1.8Matrix.json b/Documentation/Qualification/StructuralWorkflows1.8Matrix.json new file mode 100644 index 0000000..11d69b9 --- /dev/null +++ b/Documentation/Qualification/StructuralWorkflows1.8Matrix.json @@ -0,0 +1,88 @@ +{ + "schema_version": 1, + "profile": "balanced", + "sizes": [ + 1, + 8, + 64, + 512 + ], + "batch_source_records": 8, + "batch_operations": [ + "path-batch", + "snapshot-batch", + "store-values" + ], + "cells": [ + "path-batch-1", + "snapshot-batch-1", + "store-values-1", + "follow-chain-1", + "path-batch-8", + "snapshot-batch-8", + "store-values-8", + "follow-chain-8", + "path-batch-64", + "snapshot-batch-64", + "store-values-64", + "follow-chain-64", + "path-batch-512", + "snapshot-batch-512", + "store-values-512", + "follow-chain-512" + ], + "required_complete": [ + "path-batch-1", + "snapshot-batch-1", + "store-values-1", + "path-batch-8", + "snapshot-batch-8", + "store-values-8", + "path-batch-64", + "snapshot-batch-64", + "store-values-64", + "path-batch-512", + "snapshot-batch-512", + "store-values-512", + "follow-chain-1", + "follow-chain-8" + ], + "diagnostic_capacity": [ + "follow-chain-64", + "follow-chain-512" + ], + "follow_bounds": "depth=n-1; pending frontier=1; visited/results=n; unchanged balanced native and store defaults", + "fixtures": [ + { + "name": "fixed-eight.json", + "records": 8, + "bytes": 608, + "sha256": "f8372702217632eaa0903d11407534f44dd9f59e1c5916b3b2395258500912b7" + }, + { + "name": "chain-1.json", + "records": 1, + "bytes": 62, + "sha256": "8fe8b3ce7bc73d5213c7152637bb3f5a0cc64e3bef04afde0b8f521cab0eaf8c" + }, + { + "name": "chain-8.json", + "records": 8, + "bytes": 608, + "sha256": "f8372702217632eaa0903d11407534f44dd9f59e1c5916b3b2395258500912b7" + }, + { + "name": "chain-64.json", + "records": 64, + "bytes": 5084, + "sha256": "b6c2c1f666a54393b191df5e903b4d3d33a926b7326b2a05717a5de95bcad7f2" + }, + { + "name": "chain-512.json", + "records": 512, + "bytes": 41748, + "sha256": "37472330ac6dff6c5fa6142a0c69e867b548e54098d178a47731df150695d2c9" + } + ], + "policy": "A diagnostic typed resource refusal is an observed incomplete capacity cell, never an evaluated success or a default/joint-capacity claim. No timings, RSS or allocations are measured. Native limits/defaults and >10% performance review trigger are unchanged." +} diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index ee42d25..fddc9ab 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -6,14 +6,14 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 34, + "JSONIntegrationTests": 43, "JSONHTMLTests": 97, "JSONBridgesTests": 10, "JSONCLITests": 88, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 190, - "JSONTransformTests": 23, + "JSONInspectionTests": 214, + "JSONTransformTests": 57, "JSONDSLTests": 47 }, "parameterized_invocations": 390, @@ -31,14 +31,14 @@ "JSONSchemaTests": 3, "JSONRegexRuntimeTests": 1, "JSONStreamingTests": 16, - "JSONIntegrationTests": 11, + "JSONIntegrationTests": 12, "JSONHTMLTests": 5, "JSONBridgesTests": 1, "JSONCLITests": 1, "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 10, - "JSONTransformTests": 1, + "JSONInspectionTests": 11, + "JSONTransformTests": 4, "JSONDSLTests": 1 }, "coverage_unmapped": { @@ -78,14 +78,14 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 34, + "JSONIntegrationTests": 43, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONCLITests": 88, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 190, - "JSONTransformTests": 23, + "JSONInspectionTests": 214, + "JSONTransformTests": 57, "JSONDSLTests": 47 }, "wasi_root_functions": { @@ -94,13 +94,13 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 34, + "JSONIntegrationTests": 43, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 190, - "JSONTransformTests": 23, + "JSONInspectionTests": 214, + "JSONTransformTests": 57, "JSONDSLTests": 47 }, "wasi_parameterized_invocations": 353, @@ -109,14 +109,14 @@ "JSONSchemaTests": 3, "JSONRegexRuntimeTests": 1, "JSONStreamingTests": 16, - "JSONIntegrationTests": 11, + "JSONIntegrationTests": 12, "JSONHTMLTests": 4, "JSONBridgesTests": 1, "JSONCLITests": 1, "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 10, - "JSONTransformTests": 1, + "JSONInspectionTests": 11, + "JSONTransformTests": 4, "JSONDSLTests": 1 }, "export_functions": { @@ -148,5 +148,146 @@ "JSONInspectionTests.storeR08DeclarationAndPhysicalPermutationMetamorphisms()", "JSONInspectionTests.storeR09CompleteRowsDeepSourcesAndZeroCapacity()", "JSONInspectionTests.storeR10NoInferenceOrModelMutationAndExactPayloads()" + ], + "workflow_function_ids": [ + "JSONInspectionTests.workflowB01PathsPreserveOrderRepeatsNullAndExactScalars()", + "JSONInspectionTests.workflowB02OneCompleteAdmissionAndEmptyBatch()", + "JSONInspectionTests.workflowB03RequestCapacityConfigurationAndExactBoundary()", + "JSONInspectionTests.workflowB04MissingAndWrongKindRetainTypedPaths()", + "JSONInspectionTests.workflowB05UnselectedSuffixAndCumulativeWorkCannotReset()", + "JSONInspectionTests.workflowB06SnapshotBatchKeepsOneSourceAndOrdinalMeaning()", + "JSONInspectionTests.workflowB07InnerCancellationInvalidatesOriginalScope()", + "JSONInspectionTests.workflowB08RetentionBoundaryIsFiniteAndSourceIsUnchanged()", + "JSONInspectionTests.workflowS01PublicStorePreservesPhysicalAndDeclaredOrder()", + "JSONInspectionTests.workflowS02LookupUsesDecodedScalarIDsAndDistinguishesMissing()", + "JSONInspectionTests.workflowS03OwnerTokensCannotRebindAnotherInstance()", + "JSONInspectionTests.workflowS04StoreBatchValuesPreserveOrderExactnessAndOneAdmission()", + "JSONInspectionTests.workflowS05ReferenceSlotsKeepAbsentNullAndLinkDistinct()", + "JSONInspectionTests.workflowS06FollowCompletesCyclesAndPreservesBFSOrder()", + "JSONInspectionTests.workflowS07FollowAllFourInclusiveBoundsThrowInsteadOfTruncate()", + "JSONInspectionTests.workflowS08FollowAndLookupHaveCumulativeQuotaAndNoSecondAdmission()", + "JSONInspectionTests.workflowS09FollowInnerCancellationAndInvalidation()", + "JSONInspectionTests.workflowS10SourceReplacementCannotInventSpansOrRebindPositions()", + "JSONInspectionTests.workflowS11DisabledReferencesRemainOrdinaryData()", + "JSONInspectionTests.workflowSharedImmutablePlansHaveIndependentBatchAndStoreHealth()", + "JSONInspectionTests.workflowS12StorePathsHonorInvocationPointerAndDiagnosticLimits()", + "JSONInspectionTests.workflowS13FollowSelfCycleAndLongNarrowChain()", + "JSONInspectionTests.workflowB09NestedSnapshotRepeatsQuotasAndLateCancellation()", + "JSONInspectionTests.workflowS14MetadataAliasesAndScratchHaveExactRetentionDisposition()" + ], + "structural_acceptance_function_ids": [ + "JSONIntegrationTests.acceptance18A01SchemaStoreTransformKeepsExactGoldenBytes()", + "JSONIntegrationTests.acceptance18A02InvalidSchemaVerdictIsDistinctFromOperationalRefusal()", + "JSONIntegrationTests.acceptance18A03PatchedRebuildHasNewOwnerAndNoInventedSourceMap()", + "JSONIntegrationTests.acceptance18A04DanglingPatchedReferenceCannotPublishStore()", + "JSONIntegrationTests.acceptance18A05ScalarCollectionNamesSurvivePatchAndCrossLinks()", + "JSONIntegrationTests.acceptance18A06TransformFailureStaysTerminalOverStoreInput()", + "JSONIntegrationTests.acceptance18A07RootRemovalIsNotNullOrAnEmptyStore()", + "JSONIntegrationTests.acceptance18A08CancelledWriterKeepsOnlyItsAcceptedPrefix()", + "JSONIntegrationTests.acceptance18SharedPlansKeepIndependentCrossProductHealth()" + ], + "integration_function_ids": [ + "JSONIntegrationTests.acceptance18A01SchemaStoreTransformKeepsExactGoldenBytes()", + "JSONIntegrationTests.acceptance18A02InvalidSchemaVerdictIsDistinctFromOperationalRefusal()", + "JSONIntegrationTests.acceptance18A03PatchedRebuildHasNewOwnerAndNoInventedSourceMap()", + "JSONIntegrationTests.acceptance18A04DanglingPatchedReferenceCannotPublishStore()", + "JSONIntegrationTests.acceptance18A05ScalarCollectionNamesSurvivePatchAndCrossLinks()", + "JSONIntegrationTests.acceptance18A06TransformFailureStaysTerminalOverStoreInput()", + "JSONIntegrationTests.acceptance18A07RootRemovalIsNotNullOrAnEmptyStore()", + "JSONIntegrationTests.acceptance18A08CancelledWriterKeepsOnlyItsAcceptedPrefix()", + "JSONIntegrationTests.acceptance18SharedPlansKeepIndependentCrossProductHealth()", + "JSONIntegrationTests.caughtOperationalFailureCannotComplete()", + "JSONIntegrationTests.composedRetentionAndURIUseSameScope()", + "JSONIntegrationTests.composedWorkThresholdAndOneAdditionalUnit()", + "JSONIntegrationTests.compositionCallerErrorAndReportProvenance()", + "JSONIntegrationTests.compositionCancellationInsideOwnedScansIsSticky()", + "JSONIntegrationTests.compositionCheckedArithmeticAndStickyUnderflow()", + "JSONIntegrationTests.compositionExactSourceAndNativeErrors()", + "JSONIntegrationTests.compositionIndependentScopesCanBeSharedAcrossTasks()", + "JSONIntegrationTests.compositionInputAndCopyAdmissionBeforeAllocation()", + "JSONIntegrationTests.compositionMonotoneAliasesAndNoPlanPollution()", + "JSONIntegrationTests.compositionNestedScopesHaveExplicitIndependentBudgets()", + "JSONIntegrationTests.compositionRestrictionPreservesCapacityHistory()", + "JSONIntegrationTests.consolidationAuthoredInputCannotReopenFailedTransformLedger()", + "JSONIntegrationTests.consolidationAuthoredValuesValidateTransformAndTraverseWithoutScalarLoss()", + "JSONIntegrationTests.qaE2EAnnotationsFromFailedBranchesDoNotAuthorizeProperties()", + "JSONIntegrationTests.qaE2EAwaitedSinkCancellationKeepsOnlyAcceptedBytes()", + "JSONIntegrationTests.qaE2EAwaitedSinkFailureNeverCertifiesAcceptedPrefix()", + "JSONIntegrationTests.qaE2EBuiltinRegistryHasAllPinnedOfflineIdentities()", + "JSONIntegrationTests.qaE2ECumulativeWorkSucceedsAtNAndFailsAtNMinusOne()", + "JSONIntegrationTests.qaE2EExactScalarAndIntegerConstraintsRejectNearMatches()", + "JSONIntegrationTests.qaE2EExactWorkflowHasIndependentGoldenOutput()", + "JSONIntegrationTests.qaE2EMinimizedMalformedReplaysHavePreciseFailures()", + "JSONIntegrationTests.qaE2ENDJSONImportFailuresKeepAcceptedPrefixWithoutCommit()", + "JSONIntegrationTests.qaE2ENDJSONImportStrictFramingCommitsOnlyAtEOF()", + "JSONIntegrationTests.qaE2EOfflineReferenceRequiresExplicitRegistry()", + "JSONIntegrationTests.qaE2EPatchRollbackRootAbsenceAndMergeNullRemainDistinct()", + "JSONIntegrationTests.qaE2ERootImportCancellationAfterProvisionalAcceptanceIsTerminal()", + "JSONIntegrationTests.qaE2ERootImportCommitsOnlyAfterCertifiedCompletion()", + "JSONIntegrationTests.qaE2ERootImportMalformedTailDoesNotCommitAcceptedElements()", + "JSONIntegrationTests.qaE2ERootImportQuotaStopsBeforeCommitAndFurtherPulls()", + "JSONIntegrationTests.qaE2ESeededModelsMatchOrderedMemberAndIntegerOracles()", + "JSONIntegrationTests.qaE2ESharedPlanRecoversAfterIndependentFailures()", + "JSONIntegrationTests.qaE2EUnknownEnvelopeVersionIsCorruption()", + "JSONIntegrationTests.qaE2EVersionOneEnvelopePreservesExactInnerState()" + ], + "transform_function_ids": [ + "JSONTransformTests.bindingD01CanonicalGoldenBytesAndReload()", + "JSONTransformTests.bindingD02CanonicalEscapingAndExactScalarSource()", + "JSONTransformTests.bindingD03StrictSemanticDecodeCanonicalizesPresentation()", + "JSONTransformTests.bindingD04ClosedFieldsVersionsAndKindsArePrecise()", + "JSONTransformTests.bindingD05NativeGrammarAndDuplicateFieldsStayAuthoritative()", + "JSONTransformTests.bindingD06NamesRejectBeforeSetConversion()", + "JSONTransformTests.bindingD07SpecificCapsAreInclusiveAndFinite()", + "JSONTransformTests.bindingD08WireAndOwnedWorkRetentionHaveExactBoundaries()", + "JSONTransformTests.bindingD09CompileReportsPreserveExistingPlanBehaviorAndBudget()", + "JSONTransformTests.bindingD10CodecDoesNotInventQueryValidationOrDecimalAuthority()", + "JSONTransformTests.bindingD11CancellationOccursInsideOwnedCodecWork()", + "JSONTransformTests.bindingP01ExplicitIndicesPreserveDuplicatesOrderAndTies()", + "JSONTransformTests.bindingP02InvalidOutputAndAbandonedPrefixNeverCertify()", + "JSONTransformTests.bindingP03PopulationLeaseSurvivesCloseAndForeignOwnerRefuses()", + "JSONTransformTests.bindingP04ResourceAndProviderFailuresStayTerminal()", + "JSONTransformTests.bindingP05ProjectionEvaluationAndCancellationShareOneLedger()", + "JSONTransformTests.bindingSharedDefinitionAndCompilationHaveIndependentHealth()", + "JSONTransformTests.bindingSharedSelectionSpikeReusesOnePlanIndependently()", + "JSONTransformTests.jtxBindingNamesUseTheCompilerIdentifierGrammar()", + "JSONTransformTests.jtxBindingsAndProviderAreExplicitAndLazy()", + "JSONTransformTests.jtxCancellationLatchesAndStopsProviderCalls()", + "JSONTransformTests.jtxCompiledLiteralsCannotBypassTighterExecutionValueLimits()", + "JSONTransformTests.jtxConfiguredCompilerGrowthHasInclusiveBoundaries()", + "JSONTransformTests.jtxCopiedHandlesShareOneCumulativeLedger()", + "JSONTransformTests.jtxCumulativeProviderAndResultCapsCannotResetPerPull()", + "JSONTransformTests.jtxEscapedHandleInvalidatesAndReleasesProvider()", + "JSONTransformTests.jtxExactNumbersAndScalarKeysKeepCoreAuthority()", + "JSONTransformTests.jtxOutputAdmissionWorkAndTemporaryFramesStayOnTheOneLedger()", + "JSONTransformTests.jtxPlanIsSendableAndExecutionsHaveIndependentHealth()", + "JSONTransformTests.jtxProviderCanObserveSnapshotAndCancelAfterOwnedProgress()", + "JSONTransformTests.jtxProviderFailureIsOperationalAndNeverCaughtByLanguageTry()", + "JSONTransformTests.jtxPublicEngineMatchesNativeExactDialectMatrix()", + "JSONTransformTests.jtxReentrantProviderFailureLatchesWithoutAccessConflict()", + "JSONTransformTests.jtxReportsUseCoreDomainAndCallerErrorRetainsIdentity()", + "JSONTransformTests.jtxResourceErrorBypassesCatchAndLatchesAfterHostCatch()", + "JSONTransformTests.jtxRuntimeCapsRejectBeforePublishingAndRemainTyped()", + "JSONTransformTests.jtxSelectedAliasKeepsSourceReservationUntilLastHandleDies()", + "JSONTransformTests.jtxTaskCancellationIsObservedAfterProviderOwnedProgress()", + "JSONTransformTests.jtxTerminalAliasesRefundBeforeThrowingTheSameFailure()", + "JSONTransformTests.jtxTypedCompileLocationAndLanguageFailureStayDistinct()", + "JSONTransformTests.jtxZeroNullAndManyAreDistinctOrderedPullResults()", + "JSONTransformTests.selectionS01ExplicitOrdinalsPreserveDuplicateValuesOrderRepeatsAndTies()", + "JSONTransformTests.selectionS02EmptyEOFNullAndIncompletePrefixAreDistinct()", + "JSONTransformTests.selectionS03EveryInvalidOutputIsExactAndTerminalOutsideQueryCatch()", + "JSONTransformTests.selectionS04CompletionRejectsForeignInvocationsAndPositionsRetainPopulation()", + "JSONTransformTests.selectionS05FinalAliasAndCompletionReleaseSettleToZeroWithoutDataPulls()", + "JSONTransformTests.selectionS06BindingsAreExplicitReservedAndScalarChecked()", + "JSONTransformTests.selectionS07PopulationPolicyIsInclusiveFiniteAndCannotSkipCompleteAdmission()", + "JSONTransformTests.selectionS08ProjectionEvaluationAndCompletionShareExactCumulativeWorkAndRetention()", + "JSONTransformTests.selectionS09QuotaAndCancellationRefundTerminalAliasesAndPreserveFirstCause()", + "JSONTransformTests.selectionS10ProviderEOFNullErrorAndNoFurtherPullsRemainDistinct()", + "JSONTransformTests.selectionS11ReentryAndProviderCancellationUseSafeEntrySnapshots()", + "JSONTransformTests.selectionS12CallerErrorAndEscapedClosuresReleaseProviderWithoutCompletion()", + "JSONTransformTests.selectionS13InnerProjectionTaskCancellationReleasesUnpublishedPreparation()", + "JSONTransformTests.selectionS14FailedPreparationAndCompletionAllocationsRefundExactBoundaries()", + "JSONTransformTests.selectionS15LanguageCatchesRemainOrdinaryAndUncaughtFailuresStayTerminal()", + "JSONTransformTests.selectionSharedImmutablePlanHasIndependentPopulationAndFailureHealth()" ] } diff --git a/Documentation/Qualification/TransformBindings1.8.md b/Documentation/Qualification/TransformBindings1.8.md new file mode 100644 index 0000000..37bd195 --- /dev/null +++ b/Documentation/Qualification/TransformBindings1.8.md @@ -0,0 +1,89 @@ +# Transform bindings 1.8 — first sprint + +Status: implemented and scoped native qualification complete; full release +qualification remains PARTIAL. This isolated candidate extends +`b8ddeb26042005a06360f50fcf20b1f840e66d1c` on +`codex/1.8-transform-bindings-s1`. The original batches/store candidate, its +PR and all earlier source-bound receipts remain independent and unchanged. +No merge, tag, publication, Content pin or downstream admission is implied. + +The [contract](../Architecture/Release1.8/TransformBindings.md) defines the +candidate wire format and next selection boundary. Public additions are the +immutable source definition, finite definition policy, typed definition failures, +codec operations and compile-with-report route. Existing JSON/decimal/Unicode +and query semantics remain authoritative. The indexed-selection feasibility code +exists only in the test target; no public position, completion or selection API +ships in this sprint. + +## Observed qualification — 29 October 2026 + +Supported host: macOS 27.0 arm64, Apple Swift 6.4.2 +(`swiftlang-6.4.2.1.9`, driver 1.168.6), Xcode 27.2/27B48, SDK 27.2, +Swift language mode 6. Raw commands, exits, events, source manifests and failed +attempts are retained privately under `Artifacts/TransformBindingsS1`. + +| Check | Observed outcome | +| --- | --- | +| `Scripts/ci.py --lane transform-bindings` | Passed: 41 functions in Debug and Release; original 23 preserved; 31 Python harness cases; public privacy/identity checks | +| `Scripts/ci.py --lane transform-bindings-safety` | Passed: all 41 Release ASan functions; three shared-plan TSan functions; strict-memory target build with warnings as errors | +| New public-only consumer outside Git | Passed: six witnesses in each configuration, one exact compiled DocC fence, four ordinary full-codegen constructor/immutability refusals | +| Existing public Transform consumer | Passed: original twelve witnesses in each configuration | +| Existing Transform ownership compiler cases | Passed: seven cases; intended compiler refusals distinguished from import failures/crashes | +| `Scripts/ci.py --lane cli` | Passed: 13 commands, 88 functions/37 parameter invocations and all existing command/vector/provider checks | +| Existing manifest/dependency/cap checks | Passed through unchanged CI comparators: thirteen products, empty external graph, one test reaching all 49 core caps with exact outcomes | +| Scoped JSONTransform native symbol graph and DocC | Passed: 67 authored declarations and parameter comments; original 42 identities/signatures preserved; warnings as errors | + +The public graph adds 25 authored declarations. Three historical graph fragment +differences are retained and explicitly reviewed: two nested types acquire their +parent qualification while retaining the same precise type IDs and byte-identical +owning sources; generic Result metadata loses a local preciseIdentifier with +unchanged spelling. The original withExecution declaration/body is byte-identical. +This is a scoped Transform catalog result, not a fresh thirteen-catalog pass. + +Initial compiler errors were in new test assertions with throwing expressions; +they remain in the private logs. The first native harness run rejected a stale +30-case self-test expectation after a meaningful 31st case was added. The +successor checks both the new exact population and rejection of stale populations. +The first strict graph comparison retained the three rendering differences above; +its source-backed successor records their precise disposition. No failed attempt +is relabeled as a passed check or omitted from the return. + +## Semantic and ownership coverage + +D01–D03 pin literal canonical bytes, native escaping, exact decoded source scalars, +UTF-8 name ordering and canonical reload. D04–D06 reject closed-field/type/version/ +dialect/name errors, exact duplicates and malformed native JSON. D07–D11 pin zero, +exact, one-over and overflow-policy boundaries, work/retention accounting, compile +reports, compiler errors and deterministic inner-loop cancellation. Shared cases +exercise independent concurrent reuse. The reports describe actual native work; +caller storage/instructions and process RSS remain outside those claims. + +The P01–P05 test-only selection prototype exercises explicit indices over duplicate +values, order/repeats/ties, empty completion, invalid late output bypassing query +catch, incomplete-prefix refusal, foreign owner refusal, post-close leases/refunds, +terminal quota/provider failures and cumulative projection/evaluation/cancellation. +It uses the existing VM and private context, not a second evaluator. Its profile, +provider and completion types are experiments, not public contracts or conformance. + +## Next bounded increment and held gates + +Sprint 2 must freeze reserved binding/provider policy and a versioned selection +convention, implement a guarded public selection scope with private position and +completion constructors, and qualify reentry, public constructor refusals, aliases, +first-cause terminal accounting, exact inclusive capacities and independent plans. +Retain the admitted population through every output's joint lease. Full EOF is +required before an owner-produced completion witness. Ordinary execution stays +unchanged. The downstream S06/S13 mapping remains a separate deliberate decision. + +Fresh full Native/Linux ARM64/x86_64/safety/docs, WASI, relevant Apple SDK/runtime, +archive reproduction and complete API checks remain final-release gates. The +portable harness is prepared but this sprint makes no new Linux/WASI/device pass. +The existing 49 policies/probes are unchanged; the codec's five finite capacities +are additive policy, not replacements. No new benchmark/allocation campaign ran. +Existing 1.8 loaded-host comparisons, unresolved regressions, unmeasured allocations +and older scoped holds remain visible; the 1.7 waiver does not apply to 1.8. + +The final checkpoint binding separately records this documentary closeout and +architecture text polish after the passing receipts. Product, manifest, tests, +DocC, fixtures, generator, CLI identity and executable harness bytes remain equal +to those checked. No independent human review or release approval is claimed. diff --git a/Documentation/Qualification/TransformSelection1.8.md b/Documentation/Qualification/TransformSelection1.8.md new file mode 100644 index 0000000..0913cc8 --- /dev/null +++ b/Documentation/Qualification/TransformSelection1.8.md @@ -0,0 +1,104 @@ +# Transform indexed selection — Sprint 2 + +Status: implemented with scoped native qualification complete; full 1.8 release +qualification remains PARTIAL. This local candidate extends sealed Sprint 1 +`70df9ba8d47322abf0d4aec1439256d7a5e97e94` on +`codex/1.8-transform-selection-s2`. The previous branch/checkpoint, batches/store +PR and all source-bound receipts remain intact. No merge, tag, publication or +Content source-lock change is implied. + +The [contract](../Architecture/Release1.8/TransformBindings.md) and +[compiled public guide](../../Sources/JSONTransform/Documentation.docc/IndexedSelection.md) +define selection convention V1. The input is an owned index array and `$values` +is the completely admitted population. Bindings supply exactly the other declared +names. The finite population cap defaults to 65,536, alongside existing native +and Transform policies. One private invocation context owns population admission, +projection/index slots, evaluation, provider/output admission, exact conversion, +position leases and completion. No engine language or numeric authority changes. + +## Ownership and failure decision + +Positions have private construction, exact ordinals, invocation identity and a +joint population/output-slot lease. Completion/report construction is inaccessible +to public callers. Explicit evaluator EOF and successful cleanup are mandatory; +an abandoned prefix never certifies. A completion retains only identity/report +storage. Its constant identity check rejects foreign positions even for identical +populations. It does not reopen or retroactively fail a completed invocation. + +Scopes, positions and completions are deliberately non-Sendable. Escaped handles +are guarded after close; plans remain checked Sendable for independent concurrent +scopes. Providers are optional and explicit. Their values may influence selection +but cannot enlarge the population. Callback work and prior caller allocations +remain caller work. During callbacks, observation uses an admitted entry snapshot +and reentry/cancellation touches health before the loaned context. + +Boundary invalidity, resources, uncaught language/provider failures, cancellation +and reentry latch. Query catches retain their ordinary semantics but cannot catch +boundary/operational failures. A first operational cause wins over a later caller +error; otherwise caller errors retain identity. report() settles actual deferred +refunds after failure/close without resetting work. Completion statistics are a +close-time snapshot, not a later ARC settlement. Logical weights are not RSS, +allocation-event measurements or arbitrary caller instruction quotas. + +## Actual native qualification — 29 October 2026 + +Host: macOS 27.0 arm64, Apple Swift 6.4.2 (`swiftlang-6.4.2.1.9`, driver 1.168.6), +Xcode 27.2/27B48, SDK 27.2, language mode 6. Private receipts/logs/events/source +manifests are retained under `Artifacts/TransformBindingsS2`. + +| Check | Observed outcome | +| --- | --- | +| `Scripts/ci.py --lane transform-bindings` | Passed: 57 functions in Debug and Release, original 41 preserved, 32 Python harness cases, privacy/source identity and both public consumers | +| `Scripts/ci.py --lane transform-bindings-safety` | Passed: 57 Release ASan functions, four shared-plan TSan functions, strict-memory target build with warnings as errors | +| New outside-Git JSON/JSONTransform consumer | Passed: ten witnesses/configuration, exact compiled DocC guide, fourteen ordinary constructor/non-Sendable compiler refusals and two guarded-alias positives across Debug/Release | +| Original source-definition public consumer | Passed: six witnesses/configuration and its four ordinary compiler refusals | +| Original public Transform consumer and ownership compiler cases | Passed: twelve witnesses/configuration and all seven compiler cases | +| `Scripts/ci.py --lane cli` | Passed: thirteen commands, 88 functions/37 parameter invocations and all existing command/vector/provider outcomes | +| Existing manifest/dependency/cap commands | Passed through unchanged CI comparators: thirteen products, empty root external graph and all 49 exact cap outcomes | +| Native Transform graph/DocC | Passed: 93 authored declarations/parameters documented, all 67 Sprint 1 identities/declaration fragments unchanged, 26 additions, warnings as errors | + +Sixteen new Swift Testing functions cover explicit duplicates/order/repeats/ties, +exact scalar/number behavior, empty/EOF/null/incomplete distinctions, malformed/ +negative/fractional/overflow/native-width/out-of-population outputs, late invalidity +outside query catch, foreign owners, final aliases and report-only settlement, +reserved/invalid/missing bindings, complete unused-tail admission and zero/exact/ +one-over/overflow policies. Tests also pin cumulative work/retention/collection +slots, completion-allocation failure, terminal quota/cancel refunds, provider error/ +EOF/null/reentry snapshots, caller/provider release, deterministic inner-projection +task cancellation, uncaught language failure and independent concurrent reuse. + +The first test compilation retained ordinary errors in throwing assertion inference +and one incorrect native metric name. Its corrected 13-case run passed before the +final 16-case increment. No production semantic failure or compiler crash occurred. +A supplemental private runner initially lacked its output directory and executed +no command; the raw setup failure remains, and the diagnosed successor runs the +same three commands. No failed attempt is relabeled as a pass or hidden. + +## Final binding and remaining gates + +The checkpoint binding separately maps this qualification closeout and the API +index paragraph after passing receipts. Product, test, fixture, manifest, DocC, +generator, identity and executable harness bytes match those checked. Native +receipts are scoped to this actual source. No independent human review or broad +platform acceptance is inferred. The scripted graph comparison is exact for all +67 predecessor declaration fragments; full thirteen-catalog graphs remain separate. + +The private return retains deterministic source/checkpoint/diff and reproduction +results. Full final-source Native/Linux ARM64+x86_64/safety/docs/WASI/relevant Apple +SDK/runtime/API/archive gates and branch reconciliation remain release work. +No new hosted CI, Linux, WASI or device pass follows from this sprint. Preserve +main's independent documentation automation when deliberately reconciling later. +No push/merge/tag/publication is performed by local qualification. + +The current expected discovery is 57 Transform roots/four sharing functions; all +other populations and 390 native/portable parameter invocations (353 WASI) remain. +Existing 49 policy probes preserve their exact distinctions, including zero-scratch +success and invalid cancellation configuration. The population cap is additive +policy, not a replacement metric or a calibrated RSS bound. + +No benchmark/allocation campaign ran. Earlier 1.8 loaded-host observations and +unresolved regressions remain bound to 5e27899. Allocations remain unmeasured/ +unwaived for 1.8; the 1.7 waiver and historical holds are not promoted. Content's +S06/S13 profile mapping and deliberate public source re-resolution remain separate +acceptance work. This implementation supplies no Content IDs, locator/read grants, +persistence, provenance propagation, online resolution or unbounded streaming. diff --git a/Documentation/README.md b/Documentation/README.md index 8392303..f280c1a 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,3 +1,10 @@ +# Current authority — 1.8 candidate + +Published 1.7.0 has twelve libraries plus CLI and thirteen catalogs. This isolated +1.8.0-dev branch adds bounded batches and a public immutable store inside +JSONInspection. See [current qualification](Qualification/Release1.8.md). +The dated predecessor inventories below are historical, not 1.8 pass evidence. + # Current documentation authority Unreleased 1.7.0-dev consolidation has twelve libraries plus CLI and thirteen DocC diff --git a/Documentation/ReleaseRoadmap.md b/Documentation/ReleaseRoadmap.md index 550bb6c..1e7bd96 100644 --- a/Documentation/ReleaseRoadmap.md +++ b/Documentation/ReleaseRoadmap.md @@ -1,3 +1,13 @@ +# Execution update — 28 October 2026 + +1.7.0 is published at `8557badef562cf2f059e964c113d4a7f60ecbe6c`. +The maintainer has now commissioned the 1.8 bounded structural wave, including +the public read-only store, on isolated `codex/1.8-batches-and-store`. +[The implementation contract](Architecture/StructuralIntelligence/BoundedWorkflows1.8.md) +freezes this wave; [qualification](Qualification/Release1.8.md) records actual results. +The proposal and historical inventories below remain source-bound. Persistence, +platform-floor reduction and new query grammars are not included. + # Release roadmap: 1.7 through 2.0 Maintainer working proposal, audited 27 October 2026. The next step is a release diff --git a/Examples/BoundedWorkflows/Package.swift b/Examples/BoundedWorkflows/Package.swift new file mode 100644 index 0000000..6fb8b27 --- /dev/null +++ b/Examples/BoundedWorkflows/Package.swift @@ -0,0 +1,16 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package( + name: "BoundedWorkflows", + platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "BoundedWorkflows", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), + .product(name: "JSONSchema", package: "Swift-JSON"), + .product(name: "JSONPatch", package: "Swift-JSON"), + .product(name: "JSONTransform", package: "Swift-JSON"), + .product(name: "JSONNavigation", package: "Swift-JSON"), + .product(name: "JSONInspection", package: "Swift-JSON") + ])], swiftLanguageModes: [.v6] +) diff --git a/Examples/BoundedWorkflows/README.md b/Examples/BoundedWorkflows/README.md new file mode 100644 index 0000000..c052061 --- /dev/null +++ b/Examples/BoundedWorkflows/README.md @@ -0,0 +1,50 @@ +# Bounded structural workflows + +A public SwiftPM consumer for the unreleased 1.8 source. It imports JSON, +JSONSchema, JSONNavigation, JSONInspection, JSONPatch and JSONTransform; no private +meter/runtime module or external dependency is used. Swift6.4/language6 and the +supported platform baselines still apply. Released1.7 does not supply the new APIs. + +From the repository root: + +```sh +swift run --package-path Examples/BoundedWorkflows BoundedWorkflows +swift run -c release --package-path Examples/BoundedWorkflows BoundedWorkflows +``` + +The twelve original witnesses preserve exact values, request order/repetition, +missing versus null, source-local ownership, bounded follow and public reports. +Six additional witnesses compose native schema validation, source interpretation, +transformation and Patch. They check a golden exact-decimal byte sequence, a new +owner/source wrapper after Patch, ordinary invalidity versus operational refusal, +root removal versus null, and terminal transform completion after a caught error. +The separate Swift Testing Integration cases also exercise declared-link corruption, +scalar-distinct collection names, awaited-sink cancellation and concurrent plans. + +Each stage is its own native operation, not one implicit global pipeline ledger. +Caller array assembly and callbacks are ordinary caller work. Validate a schema +against its explicit offline registry/selected root before interpreting a source; +the store does not infer schema requiredness. Rebuild after Patch and obtain new +positions. Atomic publication into a sink is a caller staging decision. + +## Counter and capacity observations + +```sh +swift run -c release --package-path Examples/BoundedWorkflows BoundedWorkflows --capacity /tmp/swift-json-capacity +``` + +Use a new output directory. This writes cells.json and five deterministic fixture +files. The frozen matrix is in Documentation/Qualification/StructuralWorkflows1.8Matrix.json. +The qualification script independently verifies fixture hashes, cell identities, +expected results and exact/one-below request/work/retention boundaries. +Batch cells use1/8/64/512 requests against eight records. Follow cells use growing +chains with frontier1 and explicit depth/visited/results bounds; native balanced +and store construction defaults stay unchanged. A typed diagnostic refusal is +recorded as an incomplete capacity observation, never a successful closure. + +Independent singular invocations report summed work/admission counters and the +maximum of their per-invocation retention. A batch reports its one original ledger. +Those quantities do not measure elapsed time, heap allocations, process RSS, +caller-held copies or jointly achievable default maxima. No performance improvement +or policy change follows from the counter experiment. The controlled native +performance gate remains a separate final-source requirement. diff --git a/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Acceptance.swift b/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Acceptance.swift new file mode 100644 index 0000000..caccc6d --- /dev/null +++ b/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Acceptance.swift @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONSchema +import JSONPatch +import JSONInspection +import JSONTransform + +// Authored application recipe. These stages are separate native operations; +// caller array assembly is not a library-wide instruction or memory quota. +func structuralAcceptance() throws { + func witness(_ name: String, _ condition: Bool) { + precondition(condition, name); print("ACCEPTANCE18_CASE \(name) passed") + } + let source = try JSONParser().parseDocument(Data(#"{"u":[{"id":"é","amount":9007199254740993,"next":{"collection":"u","id":"e\u0301"}},{"id":"e\u0301","amount":0.10000000000000001,"next":null}]}"#.utf8)) + let schema = try JSONSchemaCompiler().compile(JSONSchemaDocument(JSONParser().parse( + #"{"type":"object","required":["u"],"properties":{"u":{"type":"array","items":{"type":"object","required":["id","amount","next"],"properties":{"id":{"type":"string"},"amount":{"type":"number"},"next":{"type":["object","null"]}}}}}}"#))) + let validation = try JSONSchemaValidator().validate(source.root, against: schema) + precondition(validation.isValid) + let profile = JSONStoreProfile(version: .v1, collectionsAt: [], collections: [ + .init(name: "u", recordsAt: [.key("u")], idKey: "id", referenceFields: [.init(key: "next", presence: .required, nullable: true)]) + ], references: .strict(marker: .v1, validateTargets: true)) + let store = try JSONStoreBuilder(document: source, profile: profile).build().result + guard let first = try store.lookup(collection: "u", id: "é").result else { preconditionFailure() } + let values = try store.values(at: store.follow(from: first).result) + let transform = try JSONTransformProgram.compile(".[] | .amount") + let output = try transform.withExecution(input: .array(values.result)) { execution in + var output: [JSON] = [] + while let value = try execution.next() { output.append(value.value) } + return JSON.array(output) + } + witness("schema-store-transform", try JSONWriter(options: .init(numberStyle: .preserved)).write(output.result) == Data("[9007199254740993,0.10000000000000001]".utf8)) + let patch = try JSONPatch(operations: [.replace(path: JSONPointer(parsing: "/u/1/amount"), value: .number(JSONNumber(parsing: "1.25")))]) + guard case .document(let changed) = try patch.applying(to: source.root) else { preconditionFailure() } + let document = source.replacingRoot(changed), rebuilt = try JSONStoreBuilder(document: document, profile: profile).build().result + do { _ = try rebuilt.value(at: first); preconditionFailure() } + catch let error as JSONStoreFailure { witness("patched-owner", error.code == .invalidPosition) } + witness("source-map-reset", document.sourceMap == nil && document.sourceBytes == nil && store.document.sourceMap != nil) + let invalid = try JSONSchemaValidator().validate(.null, against: schema) + var native = JSONLimits.balanced; native[.evaluationWorkUnits] = 0 + do { _ = try JSONSchemaValidator(options: .init(limits: native)).validate(source.root, against: schema); preconditionFailure() } + catch let error as JSONFailure { witness("schema-refusal", !invalid.isValid && error.resource?.metric == .evaluationWorkUnits) } + let rootPointer = try JSONPointer(parsing: "") + let removed = try JSONPatch(operations: [.remove(path: rootPointer)]).applying(to: source.root) + let null = try JSONPatch(operations: [.replace(path: rootPointer, value: .null)]).applying(to: source.root) + witness("root-removal", removed == .removed && null == .document(.null) && removed != null) + var limits = JSONTransformLimits(); limits[.results] = 1 + do { + _ = try transform.withExecution(input: .array(values.result), limits: limits) { execution in + let yielded = try execution.next(); precondition(yielded != nil) + do { _ = try execution.next(); preconditionFailure() } + catch let error as JSONTransformFailure { precondition(error.category == .resource && error.metric == "results") } + } + preconditionFailure("A caught failure cannot reopen successful completion") + } catch let error as JSONTransformFailure { witness("terminal-transform", error.category == .resource && error.metric == "results") } + print("ACCEPTANCE18 passed cases=6") +} diff --git a/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Capacity.swift b/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Capacity.swift new file mode 100644 index 0000000..de0a8bd --- /dev/null +++ b/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Capacity.swift @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONInspection + +private struct CapacityCounters: Encodable { + let work: UInt64, nodes: UInt64, retained: UInt64, peak: UInt64 + init(_ statistics: JSONOperationStatistics) { + work = statistics[.workUnits]; nodes = statistics[.nodes] + retained = statistics[.retainedBytes]; peak = statistics.peakLogicalRetainedBytes + } +} +private struct CapacitySingles: Encodable { + let workSum: UInt64, nodesSum: UInt64 + let maximumInvocationRetained: UInt64, maximumInvocationPeak: UInt64 +} +private struct CapacityCell: Encodable { + let id: String, operation: String, requests: Int, fixture: String, outcome: String + let report: CapacityCounters?, independentSingles: CapacitySingles? + let requestBoundary: Bool?, workBoundary: Bool?, retentionBoundary: Bool? + let errorMetric: String?, errorLimit: UInt64?, errorAttempted: UInt64?, errorStage: String? +} +private struct CapacityReceipt: Encodable { + let schemaVersion = 1 + let profile = "balanced" + let scope = "Owned-work and logical retention observations; no elapsed-time, allocation, RSS or throughput measurement. Fixture construction, expected-value comparison and caller array copies are caller work." + let cells: [CapacityCell] +} +private enum CapacityInvariant: Error { case result, boundary, overflow } +private func capacitySum(_ lhs: UInt64, _ rhs: UInt64) throws -> UInt64 { + let (result, overflow) = lhs.addingReportingOverflow(rhs) + guard !overflow else { throw CapacityInvariant.overflow }; return result +} +private func capacityProfile() -> JSONStoreProfile { + .init(version: .v1, collectionsAt: [], collections: [ + .init(name: "records", recordsAt: [.key("records")], idKey: "id", referenceFields: [.init(key: "next", presence: .required, nullable: true)]) + ], references: .strict(marker: .v1, validateTargets: true)) +} +private func capacityFixture(_ count: Int) throws -> Data { + var records: [JSON] = [] + for index in 0.. JSONOperationReport) throws -> CapacitySingles { + var work: UInt64 = 0, nodes: UInt64 = 0, peak: UInt64 = 0, retained: UInt64 = 0 + for index in 0.. Void, metric: JSONResourceMetric) throws { + do { try body(); throw CapacityInvariant.boundary } + catch let error as JSONFailure { guard error.resource?.metric == metric else { throw error } } +} +private func capacityBoundaries(_ report: JSONOperationReport, _ body: (JSONLimits) throws -> JSONOperationReport) throws { + var limits = JSONLimits.balanced + limits[.workUnits] = report.statistics[.workUnits] + limits[.retainedBytes] = report.statistics.peakLogicalRetainedBytes + _ = try body(limits) + limits[.workUnits] -= 1 + try capacityResource({ _ = try body(limits) }, metric: .workUnits) + limits = .balanced; limits[.retainedBytes] = report.statistics.peakLogicalRetainedBytes - 1 + try capacityResource({ _ = try body(limits) }, metric: .retainedBytes) +} + +func structuralCapacity(output: URL) throws { + try FileManager.default.createDirectory(at: output, withIntermediateDirectories: true) + let fixtureDirectory = output.appendingPathComponent("fixtures") + try FileManager.default.createDirectory(at: fixtureDirectory, withIntermediateDirectories: true) + let fixedBytes = try capacityFixture(8) + try fixedBytes.write(to: fixtureDirectory.appendingPathComponent("fixed-eight.json")) + let document = try JSONParser().parseDocument(fixedBytes) + let snapshot = try JSONStructuralSnapshotBuilder(document: document).build().result + let store = try JSONStoreBuilder(document: document, profile: capacityProfile()).build().result + var amountOrdinals: [Int] = [] + for ordinal in 0.. JSONOperationReport<[JSON]> { + switch operation { + case "path-batch": return try JSONInspectionBatch(document: document, limits: limits).values(at: paths, maximumRequests: maximum) + case "snapshot-batch": return try snapshot.values(for: ordinals, maximumRequests: maximum, limits: limits) + default: return try store.values(at: positions, maximumRequests: maximum, limits: limits) + } + } + let report = try batch(.balanced, maximum: count) + guard report.result.count == count else { throw CapacityInvariant.result } + for (index, value) in report.result.enumerated() { + let observed = operation == "store-values" ? try value.requiredMember("amount") : value + guard observed == expected[index % 8] else { throw CapacityInvariant.result } + } + let singles = try sumSingles(count) { index in + switch operation { + case "path-batch": return try document.readWithReport { try $0["records"][index % 8]["amount"].requiredValue() } + case "snapshot-batch": return try snapshot.value(for: ordinals[index]) + default: return try store.value(at: positions[index]) + } + } + guard singles.nodesSum == report.statistics[.nodes] * UInt64(count) else { throw CapacityInvariant.result } + do { _ = try batch(.balanced, maximum: count - 1); throw CapacityInvariant.boundary } + catch let error as JSONInspectionBatchFailure { + guard error == .requestLimitExceeded(limit: count - 1, actual: count) else { throw error } + } + try capacityBoundaries(report) { try batch($0, maximum: count) } + cells.append(.init(id: operation + "-" + String(count), operation: operation, requests: count, fixture: "fixed-eight.json", outcome: "completed", + report: .init(report.statistics), independentSingles: singles, requestBoundary: true, workBoundary: true, retentionBoundary: true, + errorMetric: nil, errorLimit: nil, errorAttempted: nil, errorStage: nil)) + } + let name = "chain-" + String(count) + ".json", fixture = try capacityFixture(count) + try fixture.write(to: fixtureDirectory.appendingPathComponent(name)) + func refusal(_ error: JSONFailure, stage: String) throws -> CapacityCell { + guard error.category == .resource, let resource = error.resource else { throw error } + return .init(id: "follow-chain-" + String(count), operation: "follow-chain", requests: count, fixture: name, outcome: "operational_error", + report: nil, independentSingles: nil, requestBoundary: nil, workBoundary: nil, retentionBoundary: nil, + errorMetric: resource.metric.rawValue, errorLimit: resource.limit, errorAttempted: resource.attemptedUsage, errorStage: stage) + } + let chain: JSONConfiguredStore + do { chain = try JSONStoreBuilder(document: JSONParser().parseDocument(fixture), profile: capacityProfile()).build().result } + catch let error as JSONFailure { cells.append(try refusal(error, stage: "construction")); continue } + let start = try chain.record(collection: 0, row: 0).position + var bounds = JSONStoreFollowLimits(); bounds.maximumDepth = count - 1; bounds.maximumFrontier = 1 + bounds.maximumVisited = count; bounds.maximumResults = count + let report: JSONOperationReport<[JSONStorePosition]> + do { report = try chain.follow(from: start, bounds: bounds) } + catch let error as JSONFailure { cells.append(try refusal(error, stage: "follow")); continue } + guard report.result.map(\.row) == Array(0.. 0) + guard let a = try store.lookup(collection: "u", id: "é").result, + let b = try store.lookup(collection: "u", id: "e\u{301}").result else { preconditionFailure() } + witness("scalar-identity", a != b && a.row == 0 && b.row == 1) + witness("missing-id", try store.lookup(collection: "u", id: "absent").result == nil) + let selected = try store.values(at: [b,a,b]) + witness("store-batch", try selected.result[0].requiredMember("id") == .string("e\u{301}") && selected.result[0] == selected.result[2]) + witness("complete-cycle", try store.follow(from: a).result == [a,b]) + let another = try JSONStoreBuilder(document: document, profile: profile).build().result + do { _ = try another.value(at: a); preconditionFailure() } + catch let error as JSONStoreFailure { witness("owner-refusal", error.code == .invalidPosition) } + var bounds = JSONStoreFollowLimits(); bounds.maximumResults = 1 + do { _ = try store.follow(from: a, bounds: bounds); preconditionFailure() } + catch let error as JSONStoreFailure { witness("complete-or-error", error.code == .capacityExceeded && error.capacity == "maximumResults") } + let measured = try store.follow(from: a) + var limits = JSONLimits.balanced; limits[.workUnits] = measured.statistics[.workUnits] + let equal = try store.follow(from: a, limits: limits) + limits[.workUnits] -= 1 + do { _ = try store.follow(from: a, limits: limits); preconditionFailure() } + catch let error as JSONFailure { + witness("cumulative-report", equal.result == [a,b] && equal.statistics[.workUnits] == measured.statistics[.workUnits] && error.resource?.metric == .workUnits) + } + print("BOUNDED_WORKFLOWS passed cases=12 work=\(measured.statistics[.workUnits]) logical_retained=\(measured.statistics[.retainedBytes])") + try structuralAcceptance() + } +} diff --git a/Package.swift b/Package.swift index 164b251..6aa75ee 100644 --- a/Package.swift +++ b/Package.swift @@ -43,7 +43,7 @@ let package = Package( .target(name: "JSONHTML", dependencies: ["JSON"], exclude: ["Assets"]), .target(name: "JSONFilterRuntime", dependencies: ["JSON"]), .target(name: "JSONTransform", dependencies: ["JSON", "JSONFilterRuntime"]), - .testTarget(name: "JSONTransformTests", dependencies: ["JSON", "JSONTransform"]), + .testTarget(name: "JSONTransformTests", dependencies: ["JSON", "JSONTransform", "JSONFilterRuntime"]), .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"]), diff --git a/README.md b/README.md index 9f5117f..35c9ae3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # Swift-JSON +This is **Swift-JSON 1.8.0-dev**, based on the published +[1.7.0 release](https://github.com/ThorFuchs/Swift-JSON/releases/tag/1.7.0) +(`8557badef562cf2f059e964c113d4a7f60ecbe6c`). The next additive wave brings +ordered batch reads and an immutable configured read-only store to JSONInspection. +The optional JSONTransform candidate also adds exact source-definition codecs, +compile reports and [finite indexed selection](Sources/JSONTransform/Documentation.docc/IndexedSelection.md). +It is under qualification; it is not an admitted 1.8 release. +Twelve libraries plus CLI, thirteen DocC catalogs and empty root dependencies remain. +See the [1.8 implementation contract](Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md), +[qualification record](Documentation/Qualification/Release1.8.md) and +[release roadmap](Documentation/ReleaseRoadmap.md). Historical records below retain +their original source and publication dates. + +This feature source reports `1.8.0-dev`. The installation below selects released +1.7.0; the batch/store APIs described at the end require this 1.8 candidate. + **Exact JSON values, offline schema validation, transactional patches, pull streams, HTML exports and bounded queries for modern Swift.** Swift-JSON gives applications one consistent JSON model from input bytes to validated, @@ -766,8 +782,9 @@ Every later member/cell read independently admits the whole captured source with limits and an operation report. These are structural views; no set algebra, matrix 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 is outside the 1.7.0 API. See the [roadmap](Documentation/ReleaseRoadmap.md) -for future workflow proposals. +Batch access is outside the 1.7.0 API. This 1.8 candidate adds aggregate value +reads; see [bounded structural workflows](#bounded-structural-workflows-18-candidate). +Set/matrix algebra remains outside this wave. ## Source-bound directed graphs (available since 1.7.0) @@ -799,3 +816,33 @@ The [published qualification receipt](https://github.com/ThorFuchs/Swift-JSON/re records actual release-source and exact-tag consumer checks, platform scopes and the explicit performance exception. Transform is available from 1.7.0 and is absent from the 1.6 maintenance line. + +## Bounded structural workflows (1.8 candidate) + +Use `JSONInspectionBatch.values(at:maximumRequests:)` for ordered typed paths, +`JSONStructuralSnapshot.values(for:maximumRequests:limits:)` for snapshot ordinals, +and `JSONConfiguredStore.values(at:maximumRequests:limits:)` for record positions. +Each complete batch admits the source once and shares one finite ledger, preserves +request order and repeats, and either returns every result or throws. Missing is +an error; explicit JSON null remains a value. Empty batches still admit the source. + +Build a read-only store with `JSONStoreBuilder` and an explicit `JSONStoreProfile`. +Collection paths, ID fields and reference fields are configured; exact scalar IDs +are never normalized. `lookup(collection:id:limits:)` distinguishes a missing ID +from an unknown collection. `follow(from:bounds:limits:)` returns a complete BFS +closure, with finite depth/frontier/visited/result caps and no truncated success. +Positions are local to one immutable store and cannot be forged or rebound. + +Run the [public SwiftPM example](Examples/BoundedWorkflows) with +`swift run --package-path Examples/BoundedWorkflows BoundedWorkflows`. +The [DocC guide](Sources/JSONInspection/Documentation.docc/BoundedWorkflows.md) +explains separate operation reports, caller work and logical retention. This is +an additive candidate API; no persistence, network resolver, new platform or +performance improvement is claimed. + +The [cross-product guide](Sources/JSONInspection/Documentation.docc/BoundedWorkflowRecipes.md) +and [runnable example](Examples/BoundedWorkflows/README.md) show schema validation, +read-only store interpretation, transformation and checked rebuilds after Patch. +Its separate capacity mode records owned-work/retention counters and fixture hashes; +it makes no timing, RSS or default joint-capacity claim. The existing published +developer kit remains pinned to its own qualified version. diff --git a/Scripts/ci.py b/Scripts/ci.py index 707114f..542f298 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -8,6 +8,8 @@ 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 store_acceptance import check_store_tests, check_store_consumer +from qualify_structural_workflows import check_receipt as check_workflow_consumer +from qualify_structural_acceptance import check_receipt as check_structural_acceptance from projection_acceptance import check_projection_cases, check_public_projection_receipt from test_discovery import DISCOVERY, ROOT_COUNTS, SHARED_COUNTS, PARAMETER_INVOCATIONS, check_restored @@ -26,6 +28,10 @@ SHARED_TESTS += '|graphG30ConcurrentViewsHaveIndependentHealth|graphGP14ConcurrentQueries' SHARED_TESTS += '|dslSharedFinalizationAndPlansAreInvocationOwned' SHARED_TESTS += '|storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth' +SHARED_TESTS += '|selectionSharedImmutablePlanHasIndependentPopulationAndFailureHealth' +SHARED_TESTS += '|bindingSharedDefinitionAndCompilationHaveIndependentHealth|bindingSharedSelectionSpikeReusesOnePlanIndependently' +SHARED_TESTS += '|workflowSharedImmutablePlansHaveIndependentBatchAndStoreHealth' +SHARED_TESTS += '|acceptance18SharedPlansKeepIndependentCrossProductHealth' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -34,6 +40,62 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + if lane in ('transform-bindings', 'transform-bindings-safety'): + def portable(values): + return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in values] + if lane.endswith('safety'): + return portable([ + ['swift', 'test', '--test-product', 'JSONTransformTests', '-c', 'release', '--jobs', '2', '--sanitize', 'address', '--scratch-path', '.build/bindings-address'], + ['swift', 'test', '--test-product', 'JSONTransformTests', '-c', 'release', '--jobs', '2', '--sanitize', 'thread', '--scratch-path', '.build/bindings-thread', '--filter', 'jtxPlanIsSendableAndExecutionsHaveIndependentHealth|bindingShared|selectionShared'], + ['swift', 'build', '--target', 'JSONTransform', '--jobs', '2', '--scratch-path', '.build/bindings-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], + ]) + return portable([ + ['python3', '-B', 'Scripts/public_check.py'], + ['python3', '-B', 'Scripts/generate_cli_identity.py', '--check'], + ['python3', '-B', 'Scripts/test_qualification_harness.py'], + ['swift', 'test', '--test-product', 'JSONTransformTests', '--jobs', '2'], + ['swift', 'test', '--test-product', 'JSONTransformTests', '-c', 'release', '--jobs', '2'], + ['python3', '-B', 'Scripts/qualify_transform_bindings.py', '--output', str(output / 'public-consumer')], + ['python3', '-B', 'Scripts/qualify_transform_selection.py', '--output', str(output / 'selection-consumer')], + ]) + if lane in ('structural-acceptance', 'structural-acceptance-safety'): + if lane.endswith('safety'): + result = [ + ['swift', 'test', '--test-product', 'JSONIntegrationTests', '-c', 'release', '--jobs', '2', '--sanitize', 'address', '--scratch-path', '.build/workflow-address', '--filter', 'JSONIntegrationTests'], + ['swift', 'test', '--test-product', 'JSONIntegrationTests', '-c', 'release', '--jobs', '2', '--sanitize', 'thread', '--scratch-path', '.build/workflow-thread', '--filter', 'acceptance18SharedPlansKeepIndependentCrossProductHealth'], + ['swift', 'build', '--target', 'JSONIntegrationTests', '--jobs', '2', '--scratch-path', '.build/workflow-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], + ] + else: + result = [ + ['python3', '-B', 'Scripts/public_check.py'], + ['python3', '-B', 'Scripts/generate_cli_identity.py', '--check'], + ['python3', '-B', 'Scripts/test_qualification_harness.py'], + ['swift', 'test', '--test-product', 'JSONIntegrationTests', '--jobs', '2', '--filter', 'JSONIntegrationTests'], + ['swift', 'test', '--test-product', 'JSONIntegrationTests', '-c', 'release', '--jobs', '2', '--filter', 'JSONIntegrationTests'], + ['python3', '-B', 'Scripts/qualify_structural_acceptance.py', '--output', str(output / 'public-acceptance')], + ['swift', 'test', '--package-path', 'Tools/Qualification', '--scratch-path', '.build/workflow-consumer', '--jobs', '2'], + ] + return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] + if lane in ('structural-workflows', 'structural-workflows-safety'): + if lane.endswith('safety'): + result = [ + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--jobs', '2', '--sanitize', 'address', '--scratch-path', '.build/workflow-address', '--filter', 'workflow'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--jobs', '2', '--sanitize', 'thread', '--scratch-path', '.build/workflow-thread', '--filter', 'workflowSharedImmutablePlansHaveIndependentBatchAndStoreHealth'], + ['swift', 'build', '--target', 'JSONInspection', '--jobs', '2', '--scratch-path', '.build/workflow-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], + ] + else: + result = [ + ['python3', '-B', 'Scripts/public_check.py'], + ['python3', '-B', 'Scripts/generate_cli_identity.py', '--check'], + ['python3', '-B', 'Scripts/test_qualification_harness.py'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '--jobs', '2', '--filter', 'workflow'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--jobs', '2', '--filter', 'workflow'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '--jobs', '2', '--filter', 'store'], + ['swift', 'test', '--test-product', 'JSONInspectionTests', '-c', 'release', '--jobs', '2', '--filter', 'store'], + ['python3', '-B', 'Scripts/qualify_store.py', '--output', str(output / 'store-boundaries')], + ['python3', '-B', 'Scripts/qualify_structural_workflows.py', '--output', str(output / 'public-consumer')], + ] + return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] if lane == 'store': return [ ['python3', '-B', 'Scripts/public_check.py'], @@ -58,9 +120,9 @@ def commands(lane, output): ] if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], - ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store'], - ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store'], - ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store'], + ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store|workflow'], + ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store|workflow'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store|workflow'], ['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')], @@ -69,6 +131,8 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_public_graph.py', '--output', str(output / 'public-graph-consumer')], ['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_structural_workflows.py', '--output', str(output / 'public-workflow-consumer')], + ['python3', '-B', 'Scripts/qualify_structural_acceptance.py', '--output', str(output / 'structural-acceptance')], ['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')], @@ -201,6 +265,8 @@ def commands(lane, output): ['python3', '-B', 'Scripts/test_transform_ownership.py'], ['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_transform_bindings.py', '--output', str(output / 'transform-binding-consumer')], + ['python3', '-B', 'Scripts/qualify_transform_selection.py', '--output', str(output / 'transform-selection-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'], @@ -229,6 +295,8 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_public_graph.py', '--output', str(output / 'public-graph-consumer')], ['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_structural_workflows.py', '--output', str(output / 'public-workflow-consumer')], + ['python3', '-B', 'Scripts/qualify_structural_acceptance.py', '--output', str(output / 'structural-acceptance')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -279,6 +347,8 @@ def check_result(command, code, log, events=None): if events is not None: counts = events['bundle_counts'] selected_filter = command[command.index('--filter') + 1] if '--filter' in command else None store_test = selected_filter in ('store', 'storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth') + workflow_test = selected_filter in ('workflow', 'workflowSharedImmutablePlansHaveIndependentBatchAndStoreHealth') + acceptance_test = selected_filter in ('acceptance18', 'acceptance18SharedPlansKeepIndependentCrossProductHealth') 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()) @@ -287,11 +357,21 @@ def check_result(command, code, log, events=None): if 'JSONSchemaTests' in command and events is not None: parameters = {name: ROOT_PARAMETER_CASES[name] for name in ['successfulAlternativesCannotSwallowRegexExhaustion(keyword:)', 'URIIdentityUsesSyntaxNormalizationWithoutUnicodeFolding(input:expected:)']} passed = passed and events['module_function_counts'] == {'JSONSchemaTests': 79} and events['parameterized_functions'] == parameters and events['parameter_case_invocations'] == 12 + if 'JSONTransformTests' in command: + ids = set(DISCOVERY['transform_function_ids']) + if 'thread' in command: + ids = {n for n in ids if 'bindingShared' in n or 'selectionShared' in n or 'jtxPlanIsSendableAndExecutionsHaveIndependentHealth' in n} + elif selected_filter == 'selection': + ids = {n for n in ids if '.selection' in n} + elif selected_filter == 'binding': + ids = {n for n in ids if '.binding' in n} + expected = [len(ids)] + passed = passed and events is not None and events['module_function_counts'] == {'JSONTransformTests': len(ids)} and set(events['function_names']) == ids and len(events['function_names']) == len(ids) and events['parameter_case_invocations'] == 0 if 'navigation' in command: expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 - if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store']): + if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store|workflow']): 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'] == 65 and events['parameterized_functions'] == {'storeD1NativeVector(id:)': 65} and check_projection_cases(events) and check_p2_cases(events) and check_exact_cases(events) and check_graph_cases(events) and check_store_tests(events, log) @@ -306,12 +386,28 @@ def check_result(command, code, log, events=None): 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) + if workflow_test: + expected_ids = set(DISCOVERY['workflow_function_ids']) + if selected_filter != 'workflow': + expected_ids = {x for x in expected_ids if 'SharedImmutablePlans' in x} + expected = [len(expected_ids)] + passed = passed and events is not None and events['module_function_counts'] == {'JSONInspectionTests': len(expected_ids)} and set(events['function_names']) == expected_ids and len(events['function_names']) == len(expected_ids) and events['parameter_case_invocations'] == 0 + if selected_filter == 'JSONIntegrationTests': + expected_ids = set(DISCOVERY['integration_function_ids']) + expected = [len(expected_ids)] + passed = passed and events is not None and events['module_function_counts'] == {'JSONIntegrationTests': len(expected_ids)} and set(events['function_names']) == expected_ids and len(events['function_names']) == len(expected_ids) and events['parameter_case_invocations'] == 0 + if acceptance_test: + expected_ids = set(DISCOVERY['structural_acceptance_function_ids']) + if selected_filter != 'acceptance18': + expected_ids = {x for x in expected_ids if 'SharedPlans' in x} + expected = [len(expected_ids)] + passed = passed and events is not None and events['module_function_counts'] == {'JSONIntegrationTests': len(expected_ids)} and set(events['function_names']) == expected_ids and len(events['function_names']) == len(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 not prepared_test and not store_test and events is not None: + if 'thread' in command and 'JSONSchemaExamplesTests' not in command and 'JSONTransformTests' not in command and not prepared_test and not store_test and not workflow_test and not acceptance_test 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) and check_graph_cases(events, shared=True) and events['parameter_case_invocations'] == 0 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: @@ -349,7 +445,7 @@ def check_result(command, code, log, events=None): 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', 'JSONDSLCore', 'JSONValueDSL', 'JSONSchemaDSL', 'JSONDSLTests'} 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['JSONTransform']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONFilterRuntime', None]}] and targets['JSONTransformTests']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONTransform', None]}, {'byName': ['JSONFilterRuntime', 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]}] for name, dependencies in [('JSONDSLCore', ['JSON']), ('JSONValueDSL', ['JSON', 'JSONDSLCore']), ('JSONSchemaDSL', ['JSON', 'JSONSchema', 'JSONDSLCore']), ('JSONDSLTests', ['JSON', 'JSONSchema', 'JSONDSLCore', 'JSONValueDSL', 'JSONSchemaDSL'])]: passed = passed and targets[name]['dependencies'] == [{'byName': [item, None]} for item in dependencies] @@ -388,6 +484,12 @@ def check_result(command, code, log, events=None): 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_structural_acceptance.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_structural_acceptance(data) + if command[:3] == ['python3', '-B', 'Scripts/qualify_structural_workflows.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_workflow_consumer(data) 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) @@ -431,12 +533,18 @@ 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=26' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=35' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_transform_selection.py']: + data = json.loads(log.strip().splitlines()[-1]) + passed = passed and data['status'] == 'passed' and len(data['commands']) == 20 and all(x['passed'] for x in data['commands']) and data['witnesses_per_configuration'] == 10 and data['negative_fixture_invocations'] == 14 and data['compiled_guide_fences'] == 1 + if command[:3] == ['python3', '-B', 'Scripts/qualify_transform_bindings.py']: + data = json.loads(log.strip().splitlines()[-1]) + passed = passed and data['status'] == 'passed' and len(data['commands']) == 8 and all(x['passed'] for x in data['commands']) and data['witnesses_per_configuration'] == 6 and data['negative_fixture_invocations'] == 4 and data['compiled_guide_fences'] == 1 return passed, counts def main(): parser = argparse.ArgumentParser(description=__doc__) - 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('--lane', choices=['transform-bindings', 'transform-bindings-safety', 'structural-acceptance', 'structural-acceptance-safety', 'structural-workflows', 'structural-workflows-safety', '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') parser.add_argument('--skip-host-measurements', action='store_true', help='For WASI runtime qualification only: record optional host timings as NOT RUN.') args = parser.parse_args() @@ -448,7 +556,7 @@ def main(): repository = subprocess.run(['git', 'rev-parse', '--show-toplevel'], cwd=ROOT, capture_output=True, text=True) is_checkout = repository.returncode == 0 and pathlib.Path(repository.stdout.strip()).resolve() == ROOT.resolve() commit = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=ROOT, capture_output=True, text=True) if is_checkout else None - environment = collect_linux() if args.lane.startswith('linux') or (args.lane in ('composition', 'inspection') and sys.platform == 'linux') else collect() + environment = collect_linux() if args.lane.startswith('linux') or (args.lane in ('transform-bindings', 'transform-bindings-safety', 'composition', 'inspection', 'structural-workflows', 'structural-workflows-safety', 'structural-acceptance', 'structural-acceptance-safety') and sys.platform == 'linux') else collect() receipt = {'schema_version': 1, 'lane': args.lane, 'status': 'blocked', 'environment': environment, 'started_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(), 'git_commit': commit.stdout.strip() if commit is not None and commit.returncode == 0 else None, 'github_run_id': os.environ.get('GITHUB_RUN_ID'), 'source_inputs': inputs, 'source_manifest_sha256': hashlib.sha256(json.dumps(inputs, sort_keys=True).encode()).hexdigest(), 'commands': []} def save(): (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') diff --git a/Scripts/qualify_store.py b/Scripts/qualify_store.py index 950b137..fbca82e 100644 --- a/Scripts/qualify_store.py +++ b/Scripts/qualify_store.py @@ -31,7 +31,7 @@ '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"), + 'external-scope':('func illegal(_ x: JSONReadScope) {}',"cannot find type 'JSONReadScope' 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"), } diff --git a/Scripts/qualify_structural_acceptance.py b/Scripts/qualify_structural_acceptance.py new file mode 100644 index 0000000..5dbc01f --- /dev/null +++ b/Scripts/qualify_structural_acceptance.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Public cross-product acceptance and exact owned-counter/capacity observations.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +from qualify_structural_workflows import check_receipt as check_original_consumer + +ROOT = pathlib.Path(__file__).resolve().parents[1] +MATRIX_PATH = ROOT / 'Documentation/Qualification/StructuralWorkflows1.8Matrix.json' +MATRIX = json.loads(MATRIX_PATH.read_text()) +METRICS = {m['metric'] for m in json.loads((ROOT/'Documentation/Qualification/LimitsMatrix.json').read_text())['metrics']} +CASES = {'schema-store-transform', 'patched-owner', 'source-map-reset', + 'schema-refusal', 'root-removal', 'terminal-transform'} + +def finite(value, positive=False): + return type(value) is int and (0 < value if positive else 0 <= value) and value < 2**64 - 1 + +def validate_capacity(data): + if not isinstance(data, dict): return False + rows = data.get('cells', []) + if not isinstance(rows, list) or any(not isinstance(r, dict) for r in rows): return False + if data.get('schemaVersion') != 1 or data.get('profile') != MATRIX['profile'] or [r.get('id') for r in rows] != MATRIX['cells']: + return False + for row in rows: + operation, size = row['id'].rsplit('-', 1); size = int(size) + expected_fixture = 'chain-%d.json' % size if operation == 'follow-chain' else 'fixed-eight.json' + if type(row.get('requests')) is not int or row['requests'] != size or row.get('operation') != operation or row.get('fixture') != expected_fixture: + return False + if row.get('outcome') == 'completed': + report = row.get('report', {}) + if not (isinstance(report, dict) and all(finite(report.get(k), positive=k in ('work','nodes','peak')) for k in ['work','nodes','retained','peak']) + and report['peak'] >= report['retained'] and all(row.get(k) is True for k in ['requestBoundary','workBoundary','retentionBoundary']) + and all(row.get(k) is None for k in ['errorMetric','errorLimit','errorAttempted','errorStage'])): + return False + singles = row.get('independentSingles') + if operation == 'follow-chain': + if singles is not None: return False + elif not (isinstance(singles, dict) and all(finite(singles.get(k), positive=k in ('workSum','nodesSum','maximumInvocationPeak')) + for k in ['workSum','nodesSum','maximumInvocationRetained','maximumInvocationPeak']) + and singles['nodesSum'] == size * report['nodes'] and singles['maximumInvocationPeak'] >= singles['maximumInvocationRetained']): + return False + elif row.get('outcome') == 'operational_error' and row['id'] in MATRIX['diagnostic_capacity']: + if not (row.get('report') is None and row.get('independentSingles') is None + and all(row.get(k) is None for k in ['requestBoundary','workBoundary','retentionBoundary']) + and row.get('errorMetric') in METRICS + and finite(row.get('errorLimit')) and finite(row.get('errorAttempted'), positive=True) + and row['errorAttempted'] > row['errorLimit'] and row.get('errorStage') in ('construction','follow')): + return False + else: + return False + return all(next(r for r in rows if r['id'] == name)['outcome'] == 'completed' for name in MATRIX['required_complete']) + +def validate_fixtures(directory): + expected = {x['name']: x for x in MATRIX['fixtures']} + actual = sorted(directory.glob('*.json')) + if {p.name for p in actual} != set(expected) or any(p.is_symlink() for p in actual): + raise ValueError('Changed capacity fixture inventory') + records = [] + for path in actual: + data = path.read_bytes(); record = dict(name=path.name, bytes=len(data), sha256=hashlib.sha256(data).hexdigest()) + if any(record[k] != expected[path.name][k] for k in ('bytes','sha256')): + raise ValueError('Changed capacity fixture bytes: ' + path.name) + records.append(record) + return records + +def check_receipt(data): + commands = data.get('commands', []) + expected = {'consumer','debug-binpath','debug-guide','release-binpath','release-guide','capacity'} + fixtures = data.get('fixtures', []) + return (data.get('status') == 'passed' and data.get('source_inputs_unchanged') is True + and data.get('matrix_sha256') == hashlib.sha256(MATRIX_PATH.read_bytes()).hexdigest() + and len(commands) == len(expected) and {c.get('name') for c in commands} == expected + and all(c.get('exit') == 0 and c.get('compiler_crash') is False for c in commands) + and set(data.get('consumer_results', {})) == {'debug','release'} + and all(len(v) == len(CASES) and set(v) == CASES for v in data['consumer_results'].values()) + and validate_capacity(data.get('capacity', {})) + and data.get('completed_cells') == sum(r['outcome'] == 'completed' for r in data['capacity']['cells']) + and data.get('operational_refusals') == sum(r['outcome'] == 'operational_error' for r in data['capacity']['cells']) + and data.get('performance_status', '').startswith('NOT MEASURED:') + and len(fixtures) == len(MATRIX['fixtures']) and sorted(fixtures, key=lambda x:x['name']) == sorted( + [{k:x[k] for k in ('name','bytes','sha256')} for x in MATRIX['fixtures']], key=lambda x:x['name'])) + +def main(): + parser = argparse.ArgumentParser(); parser.add_argument('--output', required=True) + args = parser.parse_args(); out = ROOT / args.output; out.mkdir(parents=True, exist_ok=True) + receipt = dict(status='running', source_inputs=[dict(path=p.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files()], + matrix_sha256=hashlib.sha256(MATRIX_PATH.read_bytes()).hexdigest(), commands=[], consumer_results={}) + def save(): (out/'receipt.json').write_text(json.dumps(receipt, indent=2)+'\n') + def run(name, command): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=1200) + log = out/(name+'.log'); log.write_text(result.stdout+result.stderr) + crash = (result.returncode < 0 and command[0] == 'swiftc') or any(x in log.read_text() for x in ['Stack dump:', 'Assertion failed', 'Found ownership error', 'fatal error encountered during compilation']) + receipt['commands'].append(dict(name=name, command=command, exit=result.returncode, compiler_crash=crash, log=str(log.relative_to(ROOT)), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save(); print(name, result.returncode, flush=True) + if result.returncode != 0 or crash: raise ValueError(name) + return result.stdout + try: + child = out/'public-consumer' + run('consumer', ['python3','-B','Scripts/qualify_structural_workflows.py','--output',str(child.relative_to(ROOT))]) + original = json.loads((child/'receipt.json').read_text()) + if not check_original_consumer(original): raise ValueError('Original public consumer/compiler boundary failed') + for config in ['debug','release']: + log = child/(config+'-consumer.log') + cases = re.findall(r'^ACCEPTANCE18_CASE (\S+) passed$', log.read_text(), re.M) + if len(cases) != 6 or set(cases) != CASES or len(re.findall(r'^ACCEPTANCE18 passed cases=6$', log.read_text(), re.M)) != 1: + raise ValueError('Incomplete public cross-product witnesses') + receipt['consumer_results'][config] = cases + bp = pathlib.Path(run(config+'-binpath', ['swift','build','-c',config,'--package-path','Examples/BoundedWorkflows','--scratch-path','.build/workflow-consumer','--show-bin-path']).strip()) + modules = bp/'Modules' if (bp/'Modules').exists() else bp + guide = ROOT/'Sources/JSONInspection/Documentation.docc/BoundedWorkflowRecipes.md' + blocks = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(blocks) != 1: raise ValueError('Changed recipe snippet inventory') + source = out/'guide.swift'; source.write_text(blocks[0]+'\n') + run(config+'-guide', ['swiftc','-swift-version','6','-warnings-as-errors','-I',str(modules),'-Onone' if config == 'debug' else '-O', + '-parse-as-library','-c',str(source),'-o',str(out/(config+'-guide.o'))]) + capacity = out/'capacity' + run('capacity', ['swift','run','-c','release','--jobs','2','--package-path','Examples/BoundedWorkflows','--scratch-path','.build/workflow-consumer','BoundedWorkflows','--capacity',str(capacity)]) + data = json.loads((capacity/'cells.json').read_text()) + if not validate_capacity(data): raise ValueError('Changed/incomplete capacity observations or boundary proof') + receipt['fixtures'] = validate_fixtures(capacity/'fixtures') + receipt['capacity'] = data + receipt['completed_cells'] = sum(r['outcome'] == 'completed' for r in data['cells']) + receipt['operational_refusals'] = sum(r['outcome'] == 'operational_error' for r in data['cells']) + receipt['capacity_status'] = 'observed_fixture_grid_complete' if not receipt['operational_refusals'] else 'partial_diagnostic_capacity' + receipt['performance_status'] = 'NOT MEASURED: owned counters are not timings, heap allocations, RSS or throughput' + receipt['status'] = 'passed' + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + receipt.update(status='failed', error=str(error)) + receipt['source_inputs_unchanged'] = receipt['source_inputs'] == [dict(path=p.relative_to(ROOT).as_posix(),sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files()] + if not receipt['source_inputs_unchanged']: receipt['status'] = 'failed' + if receipt['status'] == 'passed' and not check_receipt(receipt): receipt.update(status='failed', error='Incomplete final acceptance receipt') + save(); return 0 if receipt['status'] == 'passed' else 1 + +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_structural_workflows.py b/Scripts/qualify_structural_workflows.py new file mode 100644 index 0000000..c6908ed --- /dev/null +++ b/Scripts/qualify_structural_workflows.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Real external SwiftPM consumer plus domain-specific public-boundary codegen.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT = pathlib.Path(__file__).resolve().parents[1] +CASES = {'batch-exact-order', 'missing-is-error', 'batch-capacity', 'snapshot-root', + 'checked-construction', 'scalar-identity', 'missing-id', 'store-batch', + 'complete-cycle', 'owner-refusal', 'complete-or-error', 'cumulative-report'} +PROBES = { + 'position-forge': ('func invalid() { _ = JSONStorePosition() }', 'initializer is inaccessible'), + 'store-forge': ('func invalid() { _ = JSONConfiguredStore() }', 'initializer is inaccessible'), + 'private-context': ('func invalid(_ context: JSONTraversalContext) {}', "cannot find type 'JSONTraversalContext' in scope"), + 'version': ('func invalid() { _ = JSONStoreProfileVersion.v2 }', "has no member 'v2'"), + 'nullable': ('func invalid() { _ = JSONStoreReferenceField(key:"f",presence:.required,nullable:"yes") }', "cannot convert value of type 'String' to expected argument type 'Bool'"), + 'mutation': ('func invalid(_ store: JSONConfiguredStore) { store.document = JSONDocument(root:.null) }', "'document' is a 'let' constant"), +} +def check_receipt(data): + configs = {'debug', 'release'} + expected_names = {f'{c}-{name}' for c in configs for name in ['consumer','binpath','guide',*PROBES]} + commands = data.get('commands', []) + return (data.get('status') == 'passed' and len(commands) == len(expected_names) + and {c['name'] for c in commands} == expected_names + and all(c.get('passed') and c.get('compiler_crash') is False and + (c['exit'] > 0 and any(c['expected'].lower() in d.lower() for d in c['primary_diagnostics']) + if c.get('expected') else c['exit'] == 0) for c in commands) + and set(data.get('consumer_results', {})) == configs + and all(set(x['cases']) == CASES and len(x['cases']) == len(CASES) + and x['work_units'] > 0 and x['logical_retained_bytes'] > 0 + for x in data['consumer_results'].values())) +def main(): + parser = argparse.ArgumentParser(); parser.add_argument('--output', required=True) + args = parser.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':[], 'consumer_results':{}} + 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=1200) + log = result.stdout + result.stderr + signal = result.returncode < 0 + crash = (signal and command[0] == 'swiftc') or any(x in log for x in ['Stack dump:', 'Assertion failed', 'Found ownership error', 'fatal error encountered during compilation']) + diagnostics = re.findall(r': error: ([^\n]+)', log) + passed = not signal and 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)) + path = out/(name+'.log'); path.write_text(log) + receipt['commands'].append(dict(name=name, command=command, exit=result.returncode, passed=passed, + compiler_crash=crash, signal_termination=signal, expected=expected, primary_diagnostics=diagnostics, + log=str(path.relative_to(ROOT)), log_sha256=hashlib.sha256(path.read_bytes()).hexdigest())) + save(); print(name, result.returncode, 'PASS' if passed else 'FAIL', flush=True) + if not passed: raise ValueError(name) + return result.stdout + try: + for config in ['debug','release']: + common = ['swift', 'run', '-c', config, '--jobs', '2', '--package-path', 'Examples/BoundedWorkflows', '--scratch-path', '.build/workflow-consumer'] + text = run(config+'-consumer', common+['BoundedWorkflows']) + cases = re.findall(r'^WORKFLOW_CASE (\S+) passed$', text, re.M) + summary = re.findall(r'^BOUNDED_WORKFLOWS passed cases=12 work=([1-9]\d*) logical_retained=([1-9]\d*)$', text, re.M) + if len(cases) != 12 or set(cases) != CASES or len(summary) != 1: raise ValueError('Incomplete actual consumer witnesses') + receipt['consumer_results'][config] = dict(cases=cases, work_units=int(summary[0][0]), logical_retained_bytes=int(summary[0][1])) + bp = pathlib.Path(run(config+'-binpath', ['swift','build','-c',config,'--package-path','Examples/BoundedWorkflows','--scratch-path','.build/workflow-consumer','--show-bin-path']).strip()) + modules = bp/'Modules' if (bp/'Modules').exists() else bp + guide = ROOT/'Sources/JSONInspection/Documentation.docc/BoundedWorkflows.md' + blocks = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(blocks) != 1: raise ValueError('Changed DocC example inventory') + source = out/'guide.swift'; source.write_text(blocks[0]+'\n') + run(config+'-guide', ['swiftc','-swift-version','6','-warnings-as-errors','-I',str(modules), + '-Onone' if config == 'debug' else '-O', '-parse-as-library','-c',str(source),'-o',str(out/(config+'-guide.o'))]) + for name, (body, expected) in PROBES.items(): + source = out/(name+'.swift'); source.write_text('import JSON\nimport JSONNavigation\nimport JSONInspection\n'+body+'\n') + run(config+'-'+name, ['swiftc','-swift-version','6','-warnings-as-errors','-I',str(modules), + '-Onone' if config == 'debug' else '-O', '-parse-as-library','-c',str(source),'-o',str(out/(config+'-'+name+'.o'))], expected) + receipt['status'] = 'passed' + if not check_receipt(receipt): raise ValueError('Incomplete receipt') + 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/qualify_transform_bindings.py b/Scripts/qualify_transform_bindings.py new file mode 100644 index 0000000..53098c8 --- /dev/null +++ b/Scripts/qualify_transform_bindings.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Public-only definition consumer and ordinary full-codegen constructor refusals.""" +import argparse, hashlib, json, pathlib, platform, re, shutil, subprocess, sys, tempfile, textwrap +from qualify_transform_ownership import ordinary_rejection +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) + guide=(ROOT/'Sources/JSONTransform/Documentation.docc/SourceDefinitions.md').read_text() + fences=re.findall(r'```swift\n(.*?)\n```',guide,re.S) + consumer_text=(ROOT/'Tools/TransformBindingQualification/Sources/TransformBindingConsumer/Main.swift').read_text() + body=re.search(r'func guideExample\(\) throws \{\n(.*?)\n\}\n@main',consumer_text,re.S).group(1) + assert len(fences)==1 and textwrap.dedent(body).strip()=='\n'.join(line for line in fences[0].splitlines() if not line.startswith('import ')).strip() + commands = [] + def run(command, name): + result = subprocess.run(command,cwd=ROOT,capture_output=True,text=True) + data=result.stdout+result.stderr; path=output/(name+'.log');path.write_text(data) + row=dict(command=command,exit_code=result.returncode,log=path.name,sha256=hashlib.sha256(path.read_bytes()).hexdigest()) + commands.append(row); return result,row + with tempfile.TemporaryDirectory(prefix='swift-json-binding-consumer-') as temporary: + consumer=pathlib.Path(temporary) + for p in (ROOT/'Tools/TransformBindingQualification').rglob('*'): + if p.is_file() and not any(x in p.parts for x in ('.build','.swiftpm')): + target=consumer/p.relative_to(ROOT/'Tools/TransformBindingQualification');target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(p.read_bytes()) + manifest=consumer/'Package.swift';manifest.write_text(manifest.read_text().replace('path: "../.."','path: '+json.dumps(str(ROOT)))) + inputs=[dict(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 config in ['debug','release']: + result,row=run(['swift','run','--package-path',str(consumer),'-c',config,'--jobs','2','TransformBindingConsumer'],config+'-consumer') + row['passed']=result.returncode==0 and result.stdout.splitlines()==['TRANSFORM_BINDINGS passed witnesses=6'] + if not row['passed']:break + result,row=run(['swift','build','--package-path',str(consumer),'-c',config,'--show-bin-path'],config+'-binpath') + row['passed']=result.returncode==0 + if not row['passed']:break + modules=pathlib.Path(result.stdout.strip()); binary=modules/'TransformBindingConsumer' + target=output/(config+'-TransformBindingConsumer');shutil.copyfile(binary,target) + row['binary_sha256']=hashlib.sha256(target.read_bytes()).hexdigest() + base=['swiftc','-swift-version','6','-I',str(modules),'-module-cache-path',str(output/'ModuleCache'),'-c'] + if sys.platform=='darwin': + sdk=subprocess.check_output(['xcrun','--sdk','macosx','--show-sdk-path'],text=True).strip() + base+=['-sdk',sdk,'-target',platform.machine()+'-apple-macos27.0','-F',str(modules/'PackageFrameworks')] + for name,source,diagnostic in [ + ('immutable','import JSONTransform\nfunc check(_ d: JSONTransformProgramDefinition) { d.source = "changed" }\n','cannot assign to property'), + ('constructor','import JSONTransform\nlet d = JSONTransformProgramDefinition(validatedSource: ".", canonicalNames: [])\n',"inaccessible due to 'private'")]: + path=output/(config+'-'+name+'.swift');path.write_text(source) + result,row=run(base+[str(path),'-o',str(output/(config+'-'+name+'.o'))],config+'-'+name) + row['expected_diagnostic']=diagnostic;row['passed']=ordinary_rejection(result.returncode,result.stderr,diagnostic) + passed=len(commands)==8 and all(x['passed'] for x in commands) + receipt=dict(status='passed' if passed else 'failed',commands=commands,source_inputs=inputs,compiled_guide_fences=1, + configurations=2,witnesses_per_configuration=6,negative_fixture_invocations=4, + scope='Outside-Git public-only path consumer; no private imports or released-version resolution.') + (output/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + print(json.dumps(receipt));return 0 if passed else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/qualify_transform_selection.py b/Scripts/qualify_transform_selection.py new file mode 100644 index 0000000..175154d --- /dev/null +++ b/Scripts/qualify_transform_selection.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Public-only selection consumer and ownership/completion compiler boundaries.""" +import argparse, hashlib, json, pathlib, platform, re, shutil, subprocess, sys, tempfile, textwrap +from qualify_transform_ownership import ordinary_rejection +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) + guide=(ROOT/'Sources/JSONTransform/Documentation.docc/IndexedSelection.md').read_text() + fences=re.findall(r'```swift\n(.*?)\n```',guide,re.S) + consumer_text=(ROOT/'Tools/TransformSelectionQualification/Sources/TransformSelectionConsumer/Main.swift').read_text() + body=re.search(r'func selectionGuideExample\(\) throws \{\n(.*?)\n\}\nprivate',consumer_text,re.S).group(1) + assert len(fences)==1 and textwrap.dedent(body).strip()=='\n'.join(line for line in fences[0].splitlines() if not line.startswith('import ')).strip() + commands = [] + def run(command, name): + result = subprocess.run(command,cwd=ROOT,capture_output=True,text=True) + data=result.stdout+result.stderr; path=output/(name+'.log');path.write_text(data) + row=dict(command=command,exit_code=result.returncode,log=path.name,sha256=hashlib.sha256(path.read_bytes()).hexdigest()) + commands.append(row); return result,row + with tempfile.TemporaryDirectory(prefix='swift-json-selection-consumer-') as temporary: + consumer=pathlib.Path(temporary) + for p in (ROOT/'Tools/TransformSelectionQualification').rglob('*'): + if p.is_file() and not any(x in p.parts for x in ('.build','.swiftpm')): + target=consumer/p.relative_to(ROOT/'Tools/TransformSelectionQualification');target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(p.read_bytes()) + manifest=consumer/'Package.swift';manifest.write_text(manifest.read_text().replace('path: "../.."','path: '+json.dumps(str(ROOT)))) + inputs=[dict(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 config in ['debug','release']: + result,row=run(['swift','run','--package-path',str(consumer),'-c',config,'--jobs','2','TransformSelectionConsumer'],config+'-consumer') + row['passed']=result.returncode==0 and result.stdout.splitlines()==['TRANSFORM_SELECTION passed witnesses=10'] + if not row['passed']:break + result,row=run(['swift','build','--package-path',str(consumer),'-c',config,'--show-bin-path'],config+'-binpath') + row['passed']=result.returncode==0 + if not row['passed']:break + modules=pathlib.Path(result.stdout.strip()); binary=modules/'TransformSelectionConsumer' + target=output/(config+'-TransformSelectionConsumer');shutil.copyfile(binary,target) + row['binary_sha256']=hashlib.sha256(target.read_bytes()).hexdigest() + base=['swiftc','-swift-version','6','-I',str(modules),'-module-cache-path',str(output/'ModuleCache'),'-c'] + if sys.platform=='darwin': + sdk=subprocess.check_output(['xcrun','--sdk','macosx','--show-sdk-path'],text=True).strip() + base+=['-sdk',sdk,'-target',platform.machine()+'-apple-macos27.0','-F',str(modules/'PackageFrameworks')] + fixtures = [ + ('position','import JSONTransform\nlet x = JSONTransformSelectionPosition()\n',False,'inaccessible'), + ('completion','import JSONTransform\nlet x = JSONTransformSelectionCompletion()\n',False,'inaccessible'), + ('report','import JSONTransform\nfunc forge(_ c: JSONTransformSelectionCompletion) { _ = JSONTransformSelectionReport(result: (), completion: c) }\n',False,'inaccessible'), + ('scope','import JSONTransform\nlet x = JSONTransformSelectionScope()\n',False,'inaccessible'), + ('send-scope','import JSONTransform\nfunc send(_: T) {}\nfunc check(_ s: JSONTransformSelectionScope) { send(s) }\n',False,'does not conform to'), + ('send-position','import JSONTransform\nfunc send(_: T) {}\nfunc check(_ p: JSONTransformSelectionPosition) { send(p) }\n',False,'does not conform to'), + ('send-completion','import JSONTransform\nfunc send(_: T) {}\nfunc check(_ c: JSONTransformSelectionCompletion) { send(c) }\n',False,'does not conform to'), + ('guarded-alias','import JSONTransform\nfunc check(_ p: JSONTransformProgram) throws -> (() throws -> JSONTransformSelectionPosition?)? { var escaped: (() throws -> JSONTransformSelectionPosition?)?; _ = try p.withSelection(population: []) { s in escaped = { try s.next() }; let eof = try s.next(); precondition(eof == nil) }; return escaped }\n',True,None), + ] + for name,source,positive,diagnostic in fixtures: + path=output/(config+'-'+name+'.swift');path.write_text(source) + result,row=run(base+[str(path),'-o',str(output/(config+'-'+name+'.o'))],config+'-'+name) + row['positive']=positive;row['expected_diagnostic']=diagnostic;row['passed']=result.returncode==0 if positive else ordinary_rejection(result.returncode,result.stderr,diagnostic) + passed=len(commands)==20 and all(x['passed'] for x in commands) + receipt=dict(status='passed' if passed else 'failed',commands=commands,source_inputs=inputs,compiled_guide_fences=1, + configurations=2,witnesses_per_configuration=10,negative_fixture_invocations=14, + scope='Outside-Git public-only path consumer; no private imports or released-version resolution.') + (output/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + print(json.dumps(receipt));return 0 if passed else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 4662219..2a71616 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -4,6 +4,8 @@ import copy import json import pathlib +import os +import re import subprocess from qualify_release_1_1 import checkout_head from generate_cli_identity import rendered @@ -28,6 +30,165 @@ def store_markers(): 'STORE_RUNTIME R%02d passed' % n for n in range(1,11)]) class HarnessTests(unittest.TestCase): + def workflow_jobs(self, filename): + text = (pathlib.Path(__file__).resolve().parents[1]/'.github/workflows'/filename).read_text() + # Inspect this repository's authored job blocks, not a general YAML parser. + jobs = dict(re.findall(r'^ (\w+):\n(.*?)(?=^ \w+:\n|\Z)', text.split('jobs:\n',1)[1], re.M|re.S)) + return text, jobs + + def test_required_workflow_contexts_wait_for_complete_execution_matrices(self): + for filename, execution, required, name in [ + ('native.yml','[native, safety, docs, archive, composition]','[native, safety, docs, archive]','Native / ${{ matrix.lane }}'), + ('linux.yml','[linux, linux-safety, composition]','[linux, linux-safety]','Linux / ${{ matrix.architecture }} / ${{ matrix.lane }}'), + ]: + with self.subTest(workflow=filename): + _, jobs = self.workflow_jobs(filename) + self.assertIn('lane: '+execution, jobs['qualification']) + self.assertIn("run: python3 -B Scripts/ci.py --lane '${{ matrix.lane }}'", jobs['qualification']) + self.assertNotIn("if: matrix.lane == 'composition'", jobs['qualification']) + self.assertNotIn('--lane composition', jobs['qualification']) + gate = jobs['required'] + self.assertIn('name: '+name+'\n', gate) + self.assertIn('needs: qualification\n', gate) + self.assertIn('if: ${{ always() }}\n', gate) + self.assertIn('lane: '+required, gate) + if filename=='linux.yml': + for block in [jobs['qualification'], gate]: + self.assertIn('architecture: [x86_64, arm64]', block) + + def test_workflow_gates_reject_failed_cancelled_skipped_and_missing_results(self): + for filename in ['native.yml','linux.yml']: + text, jobs = self.workflow_jobs(filename) + self.assertNotIn('continue-on-error', text) + gate = jobs['required'] + self.assertIn('QUALIFICATION_RESULT: ${{ needs.qualification.result }}', gate) + script = re.findall(r'^ run: (.+)$', gate, re.M) + self.assertEqual(script, ['test "$QUALIFICATION_RESULT" = success']) + for result in ['success','failure','cancelled','skipped','', 'Success','success failure']: + run = subprocess.run(['bash','-c',script[0]],env=dict(os.environ,QUALIFICATION_RESULT=result),capture_output=True) + self.assertEqual(run.returncode==0, result=='success', (filename,result)) + env = dict(os.environ); env.pop('QUALIFICATION_RESULT',None) + self.assertNotEqual(subprocess.run(['bash','-c',script[0]],env=env,capture_output=True).returncode,0) + + def test_composition_scheduling_preserves_timeouts_pins_artifacts_and_pages_guard(self): + for filename in ['native.yml','linux.yml']: + text, jobs = self.workflow_jobs(filename) + self.assertIn('timeout-minutes: 60', jobs['qualification']) + self.assertIn('fail-fast: false', jobs['qualification']) + self.assertIn('persist-credentials: false', jobs['qualification']) + self.assertIn('retention-days: 30', jobs['qualification']) + self.assertIn('if-no-files-found: error', jobs['qualification']) + self.assertIn('actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803', text) + self.assertIn('actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f', text) + self.assertIn("branches: [main, 'release/1.6']", text) + _, native = self.workflow_jobs('native.yml') + self.assertIn("if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'", native['publish']) + self.assertIn('needs: [qualification, required]', native['publish']) + _, linux = self.workflow_jobs('linux.yml') + self.assertNotIn('publish', linux) + self.assertIn('swift@sha256:31d14d727f4451f25e29bac9c42563a098c39ac66415e8bad970f48038e38aca', linux['qualification']) + + def test_selection_filter_preserves_exact_identity_and_completion_evidence(self): + ids = [n for n in DISCOVERY['transform_function_ids'] if '.selection' in n] + self.assertEqual(len(ids), 16) + command = ['swift','test','--test-product','JSONTransformTests','--filter','selection'] + events = dict(bundle_counts=[16],module_function_counts={'JSONTransformTests':16},function_names=ids,parameter_case_invocations=0) + self.assertTrue(check_result(command,0,'',events)[0]) + self.assertFalse(check_result(command,0,'',dict(events,function_names=ids[:-1]))[0]) + consumer = ['python3','-B','Scripts/qualify_transform_selection.py'] + receipt = dict(status='passed',commands=[dict(passed=True)]*20,witnesses_per_configuration=10,negative_fixture_invocations=14,compiled_guide_fences=1) + self.assertTrue(check_result(consumer,0,json.dumps(receipt))[0]) + for broken in [dict(receipt,commands=receipt['commands'][:-1]),dict(receipt,negative_fixture_invocations=0),dict(receipt,compiled_guide_fences=0)]: + self.assertFalse(check_result(consumer,0,json.dumps(broken))[0]) + + def test_transform_binding_discovery_rejects_incomplete_duplicate_and_foreign_events(self): + ids = DISCOVERY['transform_function_ids'] + command = ['swift', 'test', '--test-product', 'JSONTransformTests'] + events = dict(bundle_counts=[57], module_function_counts={'JSONTransformTests':57}, + function_names=ids, parameter_case_invocations=0) + self.assertTrue(check_result(command, 0, '', events)[0]) + for broken in [dict(events,function_names=ids[:-1]),dict(events,function_names=ids+ids[:1]), + dict(events,module_function_counts={'JSONTests':57}),dict(events,parameter_case_invocations=1)]: + self.assertFalse(check_result(command, 0, '', broken)[0]) + shared = [n for n in ids if 'bindingShared' in n or 'selectionShared' in n or 'jtxPlanIsSendableAndExecutionsHaveIndependentHealth' in n] + thread = command + ['--sanitize', 'thread'] + self.assertTrue(check_result(thread, 0, '', dict(events,bundle_counts=[4],module_function_counts={'JSONTransformTests':4},function_names=shared))[0]) + + def test_acceptance18_discovery_preserves_exact_public_pipeline_identities(self): + names = DISCOVERY['structural_acceptance_function_ids'] + self.assertEqual(len(names), 9) + self.assertEqual(len(DISCOVERY['integration_function_ids']), 43) + events = dict(bundle_counts=[9], module_function_counts={'JSONIntegrationTests':9}, + function_names=names, parameter_case_invocations=0) + command = ['swift','test','--test-product','JSONIntegrationTests','--filter','acceptance18'] + self.assertTrue(check_result(command, 0, '', events)[0]) + full = dict(events, bundle_counts=[43], module_function_counts={'JSONIntegrationTests':43}, function_names=DISCOVERY['integration_function_ids']) + full_command = ['swift','test','--test-product','JSONIntegrationTests','--filter','JSONIntegrationTests'] + self.assertTrue(check_result(full_command, 0, '', full)[0]) + self.assertFalse(check_result(full_command, 0, '', dict(full,function_names=full['function_names'][:-1]))[0]) + for broken in [dict(events, function_names=names[:-1]), dict(events, function_names=names+names[:1]), + dict(events, module_function_counts={'JSONInspectionTests':9}), dict(events, parameter_case_invocations=1)]: + self.assertFalse(check_result(command, 0, '', broken)[0]) + + def test_capacity18_does_not_convert_refusals_or_incomplete_boundaries_to_passes(self): + from qualify_structural_acceptance import MATRIX, validate_capacity, validate_fixtures + cells = [] + for identifier in MATRIX['cells']: + operation, count = identifier.rsplit('-',1); count=int(count) + cells.append(dict(id=identifier, operation=operation, requests=count, + fixture='chain-%d.json' % count if operation=='follow-chain' else 'fixed-eight.json', + outcome='completed', report=dict(work=1,nodes=1,retained=32,peak=32), + independentSingles=None if operation=='follow-chain' else dict(workSum=count,nodesSum=count,maximumInvocationRetained=32,maximumInvocationPeak=32), + requestBoundary=True,workBoundary=True,retentionBoundary=True)) + data=dict(schemaVersion=1,profile='balanced',cells=cells) + self.assertTrue(validate_capacity(data)) + refused=copy.deepcopy(data);refused['cells'][-1]=dict(refused['cells'][-1],outcome='operational_error',report=None, + requestBoundary=None,workBoundary=None,retentionBoundary=None,errorMetric='workUnits',errorLimit=1,errorAttempted=2,errorStage='construction') + self.assertTrue(validate_capacity(refused)) + for mode in ['missing','duplicate','fake-boundary','bad-single-count','required-refusal','fake-metric','refusal-as-success','missing-report']: + bad=copy.deepcopy(refused) + if mode=='missing':bad['cells'].pop() + elif mode=='duplicate':bad['cells'][-1]=bad['cells'][0] + elif mode=='fake-boundary':bad['cells'][0]['workBoundary']=False + elif mode=='bad-single-count':bad['cells'][0]['independentSingles']['nodesSum']=2 + elif mode=='required-refusal':bad['cells'][3]=dict(bad['cells'][-1],id='follow-chain-1',requests=1,fixture='chain-1.json') + elif mode=='fake-metric':bad['cells'][-1]['errorMetric']='unrelated' + elif mode=='refusal-as-success':bad['cells'][-1]['outcome']='completed' + else:bad['cells'][0]['report']=None + self.assertFalse(validate_capacity(bad)) + with tempfile.TemporaryDirectory() as temporary: + directory=pathlib.Path(temporary) + with self.assertRaisesRegex(ValueError,'inventory'):validate_fixtures(directory) + for fixture in MATRIX['fixtures']:(directory/fixture['name']).write_bytes(b'corrupt') + with self.assertRaisesRegex(ValueError,'bytes'):validate_fixtures(directory) + + def test_workflow_discovery_requires_all_exact_identities(self): + names = DISCOVERY['workflow_function_ids'] + self.assertEqual(len(names), 24) + events = dict(bundle_counts=[24], module_function_counts={'JSONInspectionTests':24}, + function_names=names, parameter_case_invocations=0) + command = ['swift','test','--test-product','JSONInspectionTests','--filter','workflow'] + self.assertTrue(check_result(command, 0, '', events)[0]) + for broken in [dict(events, function_names=names[:-1]), dict(events, function_names=names+names[:1]), + dict(events, parameter_case_invocations=1), dict(events, bundle_counts=[21])]: + self.assertFalse(check_result(command, 0, '', broken)[0]) + + def test_workflow_consumer_rejects_missing_witnesses_or_incidental_diagnostics(self): + from qualify_structural_workflows import CASES, PROBES, check_receipt + data = dict(status='passed', commands=[dict(name=c+'-'+name, passed=True, compiler_crash=False, + exit=1 if name in PROBES else 0, expected=PROBES[name][1] if name in PROBES else None, + primary_diagnostics=[PROBES[name][1]] if name in PROBES else []) + for c in ['debug','release'] for name in ['consumer','binpath','guide',*PROBES]], + consumer_results={c:dict(cases=sorted(CASES),work_units=1,logical_retained_bytes=1) for c in ['debug','release']}) + self.assertTrue(check_receipt(data)) + for mode in ['missing-command','missing-case','incidental','crash']: + bad=copy.deepcopy(data) + if mode == 'missing-command': bad['commands'].pop() + elif mode == 'missing-case': bad['consumer_results']['release']['cases'].pop() + elif mode == 'incidental': bad['commands'][3]['primary_diagnostics']=['unrelated error'] + else: bad['commands'][3]['compiler_crash']=True + self.assertFalse(check_receipt(bad)) + def test_scoped_catalog_check_cannot_weaken_the_full_site_inventory(self): with tempfile.TemporaryDirectory() as temporary: root = pathlib.Path(temporary).resolve() @@ -153,7 +314,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', 57, ['swift', 'test', '--filter', 'navigation']), - ('JSONInspectionTests', 190, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store']), + ('JSONInspectionTests', 214, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|p2|exactProjectionPUB|graphG|store|workflow']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, @@ -167,13 +328,13 @@ 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'], 190) - self.assertEqual(policy[key]['JSONIntegrationTests'], 34) + self.assertEqual(policy[key]['JSONInspectionTests'], 214) + self.assertEqual(policy[key]['JSONIntegrationTests'], 43) 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'], 10) - self.assertEqual(policy[key]['JSONIntegrationTests'], 11) + self.assertEqual(policy[key]['JSONInspectionTests'], 11) + self.assertEqual(policy[key]['JSONIntegrationTests'], 12) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): names = sorted(PROJECTION_FUNCTIONS) @@ -364,8 +525,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=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.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=35')[0]) + for stale in ['HARNESS_TESTS passed cases=32', 'HARNESS_TESTS passed cases=31', 'HARNESS_TESTS passed cases=30', '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', 'HARNESS_TESTS passed cases=26', 'HARNESS_TESTS passed cases=28', '']: 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' diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index a659e0c..ab25073 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:138ae7e8bc2f4392db7aafaa000f60db5915658a1b34a55f12a9c27958da1312" + package static let revision = "sha256:0a49346d574588626ee993fab93ac2fb38b4a392ea7bacb750e04e9e89855f61" package static let kind = "source-sha256" } diff --git a/Sources/JSONCLIEngine/CLIReleaseVersion.swift b/Sources/JSONCLIEngine/CLIReleaseVersion.swift index 6ed3782..9927fbf 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" + package static let value = "1.8.0-dev" } diff --git a/Sources/JSONInspection/Documentation.docc/BoundedWorkflowRecipes.md b/Sources/JSONInspection/Documentation.docc/BoundedWorkflowRecipes.md new file mode 100644 index 0000000..f559e8f --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/BoundedWorkflowRecipes.md @@ -0,0 +1,67 @@ +# Combine checked structural workflows + +Compose existing authorities while preserving exact values and each operation's limits. + +## Validate, then interpret records + +JSONSchema owns schema semantics. A valid verdict does not construct a store; +JSONInspection separately checks the explicit collection, ID and link profile. +An ordinary false verdict is a completed evaluation. A thrown operational error +must not be converted to false or an empty store. + +```swift +import JSON +import JSONSchema +import JSONInspection + +func validatedStore(document: JSONDocument, schema: CompiledJSONSchema, + profile: JSONStoreProfile) throws -> JSONConfiguredStore? { + let validation = try JSONSchemaValidator().validateWithReport(document.root, against: schema) + guard validation.result.isValid else { return nil } // Application's explicit invalid-input result. + return try JSONStoreBuilder(document: document, profile: profile).build().result +} +``` + +Compile the schema against an explicit immutable offline registry. If a schema +contains only definitions, select the intended definition before validation; +the store does not invent a schema root or infer required properties. + +## Read, transform and write + +The executable `Examples/BoundedWorkflows` validates an exact source, constructs +a store, follows declared references, batch-reads the closure, runs the existing +JSONTransform compiler/VM and writes an independent byte-level golden result. +`9007199254740993` and `0.10000000000000001` never pass through Double. +Caller collection assembly between stages is ordinary caller work. Each public +stage has its own bounded report; this recipe does not create one pipeline-wide +ledger or a caller-instruction/RSS bound. Within a batch/follow/transform invocation, +all nested owned work still uses the original cumulative ledger. + +A caught transform resource failure remains terminal for that execution. It is +not EOF, JSON null or schema invalidity. A later operation on the immutable source +has independent health. With an awaited streaming sink, already accepted bytes +remain accepted after cancellation; stage output in the caller if publication +must be atomic. + +## Patch and rebuild deliberately + +Apply JSONPatch to the original canonical value, then use +`JSONDocument.replacingRoot(_:)` to capture the result. The replacement has no +invented original source bytes/spans. Rebuild its store and obtain new positions; +old owner-bound tokens are refused even when most records are unchanged. A patch +which creates a dangling declared link cannot publish a successful checked store. +Root removal is `JSONPatchResult.removed`, distinct from a present JSON null. + +## Interpret capacity evidence + +The public consumer's `--capacity OUTPUT_DIRECTORY` mode uses fixed 1/8/64/512 +request cells and small growing chains, unchanged balanced native limits and +explicit finite follow bounds. It checks exact work/retention and request boundaries, +compares independently admitted reads and writes fixture bytes for independent +hash verification. Logical counters are not heap allocations, RSS or elapsed time. +A diagnostic resource refusal remains an incomplete capacity observation. These +fixtures do not prove the default maxima jointly achievable or a speed improvement. + +The existing developer kit stays pinned to its published library version. This +1.8 candidate recipe is compiled separately; it does not silently change that kit's +compatibility or distribution claims. diff --git a/Sources/JSONInspection/Documentation.docc/BoundedWorkflows.md b/Sources/JSONInspection/Documentation.docc/BoundedWorkflows.md new file mode 100644 index 0000000..081d7b4 --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/BoundedWorkflows.md @@ -0,0 +1,73 @@ +# Read batches and configured stores + +Read ordered exact values, or follow explicitly declared links in an immutable source. + +## Choose the smallest operation + +Use ``JSONInspectionBatch`` for typed path requests, or +``JSONStructuralSnapshot/values(for:maximumRequests:limits:)`` when you already +have snapshot-local ordinals. Repeated requests remain repeated outputs. Missing +selections throw; explicit JSON null is a value. Empty batches still admit the +whole captured source, including its source map. Each batch has one cumulative +native admission/work/retention ledger, rather than one independent read per item. + +The default request ceiling is 1,024. Limits are inclusive. Invalid configurations, +resource exhaustion and cancellation throw without publishing a partial array. +Returned JSON values retain ordinary native value semantics. Logical alias bytes +are finite accounting policy, not a process RSS or caller-instruction guarantee. + +## Declare a read-only store + +``JSONStoreBuilder`` promotes a source to ``JSONConfiguredStore`` only after the +existing complete eight-phase checked construction. Names, ID fields, paths and +reference fields are explicit. There is no id/_id inference, Unicode normalization, +database, file loader or network resolution. This API is a candidate for 1.8; +released 1.7 does not contain it. + +```swift +import Foundation +import JSON +import JSONInspection + +func boundedStoreExample() throws -> [JSON] { + let document = try JSONParser().parseDocument(Data(#"{"users":[{"id":"a","friend":{"collection":"users","id":"b"}},{"id":"b","friend":null}]}"#.utf8)) + let profile = JSONStoreProfile(version: .v1, collectionsAt: [], collections: [ + .init(name: "users", recordsAt: [.key("users")], idKey: "id", referenceFields: [ + .init(key: "friend", presence: .required, nullable: true)]) + ], references: .strict(marker: .v1, validateTargets: true)) + let store = try JSONStoreBuilder(document: document, profile: profile).build().result + guard let first = try store.lookup(collection: "users", id: "a").result else { return [] } + let closure = try store.follow(from: first) + // Construction, lookup, follow and value reads are separate reported operations. + // Every selection within this one batch shares its original native ledger. + let values = try store.values(at: closure.result) + return values.result +} +``` + +The runnable [public consumer](https://github.com/ThorFuchs/Swift-JSON/tree/main/Examples/BoundedWorkflows) +exercises exact scalar IDs, missing versus null, repeated selections, cycles, +owner refusals and cumulative reports. Use this feature branch for the 1.8 API; +a compatible dependency on 1.7 cannot supply it. + +## Understand ownership and errors + +Positions are store-instance-local tokens. They cannot be constructed publicly +or reused against another build, even if that build has byte-identical input. +Fixed collection/record/reference metadata access is ordinary trusted caller work; +scanning lookup and value/follow operations have explicit reports and finite quotas. +A missing ID in a known collection returns nil. An unknown collection throws +``JSONStoreFailure`` with ``JSONStoreFailureCode/unknownCollection``. Declared +reference slots distinguish absent, null and validated links. Marker-shaped values +outside declared fields remain ordinary JSON. + +``JSONStoreFollowLimits`` defaults to depth64 and frontier/visited/results10,000. +Follow includes the start exactly once and visits new positions breadth-first in +configured field order. A self-cycle does not consume another depth. Any reachable +new node beyond a bound throws: depth is a complete-search capacity, not permission +to return a truncated closure. Cancellation invalidates the invocation; another +operation on the same immutable store starts with independent health. + +The original twelve store construction ceilings and 49 core metrics are unchanged. +They are finite policies, not a demonstrated joint capacity or heap bound. No +persistence, mutable store, parallel batches or new performance claim is supplied. diff --git a/Sources/JSONInspection/Documentation.docc/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md index e4a99f4..405526a 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -57,3 +57,17 @@ or numeric matrix. Sampling does not exempt any source from admission. - ``JSONGraphProfile`` - ``JSONGraphProjection`` - ``JSONGraphFailure`` + +### Bounded structural workflows (1.8 candidate) + +- +- +- ``JSONInspectionBatch`` +- ``JSONInspectionBatchFailure`` +- ``JSONStoreBuilder`` +- ``JSONConfiguredStore`` +- ``JSONStoreProfile`` +- ``JSONStoreCaps`` +- ``JSONStoreFollowLimits`` +- ``JSONStorePosition`` +- ``JSONStoreFailure`` diff --git a/Sources/JSONInspection/JSONInspectionBatch.swift b/Sources/JSONInspection/JSONInspectionBatch.swift new file mode 100644 index 0000000..5ccbd63 --- /dev/null +++ b/Sources/JSONInspection/JSONInspectionBatch.swift @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// A finite batch request policy refusal; ordinary selection errors retain core paths. +public enum JSONInspectionBatchFailure: Error, Sendable, Equatable { + /// The requested result count exceeds the inclusive batch bound. + /// - Parameters: + /// - limit: Configured finite maximum. + /// - actual: Number of caller requests, including repeated requests. + case requestLimitExceeded(limit: Int, actual: Int) +} + +internal func checkInspectionBatch(_ count: Int, maximum: Int) throws { + try Task.checkCancellation() + guard maximum >= 0, maximum < Int.max else { throw JSONFailure(.configuration, code: .invalidLimits) } + guard count <= maximum else { throw JSONInspectionBatchFailure.requestLimitExceeded(limit: maximum, actual: count) } +} + +/// Ordered exact value reads with one complete source admission and cumulative ledger. +/// Caller-authored paths and later caller copies retain their ordinary value semantics. +public struct JSONInspectionBatch: Sendable { + /// Canonical immutable source used by every request. + public let document: JSONDocument + /// Finite original native quotas for each batch, not per selection. + public let limits: JSONLimits + package var checkpointObserver: (@Sendable (UInt64) -> Void)? + package var outputObserver: (@Sendable (Int) -> Void)? + package var lifetimeObserver: (@Sendable (Bool, Bool) -> Void)? + + /// Captures immutable input without opening an operation or scanning it. + /// - Parameters: + /// - document: Canonical source, optionally with coherent source mapping. + /// - limits: Finite cumulative admission, work, path and retention quotas. + public init(document: JSONDocument, limits: JSONLimits = .balanced) { + self.document = document; self.limits = limits + } + + /// Reads all requested values or throws, preserving caller order and repeats. + /// Even an empty batch admits the entire source. JSON null is a value; missing + /// paths throw. The result owns32 logical header bytes and32 per alias slot; + /// JSON aliases follow existing read semantics, not an RSS guarantee. + /// - Parameters: + /// - paths: Absolute typed selections; an empty path selects the root. + /// - maximumRequests: Inclusive request cap, default1024; zero admits only []. + /// - Returns: All exact values and one cumulative operation report. + /// - Throws: Batch capacity, core access/resource/configuration errors or cancellation. + public func values(at paths: [[JSONLocationComponent]], maximumRequests: Int = 1_024) + throws -> JSONOperationReport<[JSON]> { + try checkInspectionBatch(paths.count, maximum: maximumRequests) + var reducer = PathBatchReducer(outputObserver: outputObserver) + var traversal = JSONTraversal(document: document, limits: limits) + traversal.checkpointObserver = checkpointObserver + traversal.lifetimeObserver = lifetimeObserver + return try traversal.inspectPreparedOwned(scope: .virtual(roots: paths, overlap: .orderedOccurrences), + options: .init(depthLimit: 0), reducer: &reducer) + } +} + +private struct PathBatchReducer: JSONReadPreparation, JSONTraversalOwnedReducer { + var values: [JSON] = [] + let outputObserver: (@Sendable (Int) -> Void)? + mutating func prepare(context: inout JSONReadPreparationContext) throws { try context.retain(32) } + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work(); try context.retain(32); values.append(value) + outputObserver?(values.count) + return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> [JSON] { + try context.checkpoint() + let result = values; values = []; return result + } +} + +extension JSONStructuralSnapshot { + /// Resolves ordered snapshot positions with one source admission and ledger. + /// This does not invoke singular public reads. Invalid positions throw before + /// admission; repeats are preserved, billed and never silently deduplicated. + /// - Parameters: + /// - ordinals: Positions local to this complete immutable snapshot. + /// - maximumRequests: Inclusive finite request/output count, default1024. + /// - limits: Cumulative native quotas for this entire independent batch. + /// - Returns: Exact values in request order and cumulative statistics. + /// - Throws: Position/capacity, core resource/access/configuration errors or cancellation. + public func values(for ordinals: [Int], maximumRequests: Int = 1_024, + limits: JSONLimits = .balanced) throws -> JSONOperationReport<[JSON]> { + try checkInspectionBatch(ordinals.count, maximum: maximumRequests) + return try readValues(for: ordinals, limits: limits) + } + package func valuesForQualification(for ordinals: [Int], limits: JSONLimits, + outputObserver: @Sendable @escaping (Int) -> Void, + lifetimeObserver: @Sendable @escaping (Bool, Bool) -> Void) throws -> JSONOperationReport<[JSON]> { + try checkInspectionBatch(ordinals.count, maximum: 1_024) + return try readValues(for: ordinals, limits: limits, outputObserver: outputObserver, lifetimeObserver: lifetimeObserver) + } + private func readValues(for ordinals: [Int], limits: JSONLimits, + outputObserver: (@Sendable (Int) -> Void)? = nil, + lifetimeObserver: (@Sendable (Bool, Bool) -> Void)? = nil) throws -> JSONOperationReport<[JSON]> { + var reducer = SnapshotBatchReducer(snapshot: self, ordinals: ordinals, outputObserver: outputObserver) + var traversal = JSONTraversal(document: document, limits: limits) + traversal.lifetimeObserver = lifetimeObserver + return try traversal.inspectPreparedOwned(options: .init(depthLimit: 0), reducer: &reducer) + } +} + +private struct SnapshotBatchReducer: JSONReadPreparation, JSONTraversalOwnedReducer { + let snapshot: JSONStructuralSnapshot + let ordinals: [Int] + let outputObserver: (@Sendable (Int) -> Void)? + private var requestAliases: UInt64 = 0 + init(snapshot: JSONStructuralSnapshot, ordinals: [Int], outputObserver: (@Sendable (Int) -> Void)?) { + self.snapshot = snapshot; self.ordinals = ordinals; self.outputObserver = outputObserver + } + mutating func prepare(context: inout JSONReadPreparationContext) throws { + try context.retain(32) + for ordinal in ordinals { + try context.work(); _ = try snapshot.record(at: ordinal) + try context.retain(8); requestAliases += 8 // count was capped before preparation + } + } + 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 -> [JSON] { + var result: [JSON] = [] + for ordinal in ordinals { + try context.work() + var reversed: [JSONLocationComponent] = [], current = ordinal, scratch: UInt64 = 0 + while let parent = try snapshot.record(at: current).parentOrdinal { + try context.work() + guard parent >= 0, parent < current, + let component = try snapshot.record(at: current).componentFromParent else { + throw JSONFailure(.configuration, code: .invalidOperation) + } + try context.retain(32); scratch += 32 + if case .key(let key) = component { + for _ in key.string.utf8 { try context.work(); try context.retain(1); scratch += 1 } + } + reversed.append(component); current = parent + } + // Reuse the reserved reverse buffer; do not accumulate owned prefix copies. + for index in 0..<(reversed.count / 2) { + try context.work(); reversed.swapAt(index, reversed.count - index - 1) + } + let value = try context.selectedValue(at: reversed) + reversed = []; try context.release(scratch) + try context.retain(32); result.append(value); outputObserver?(result.count) + } + try context.checkpoint(); try context.release(requestAliases); return result + } +} diff --git a/Sources/JSONInspection/Store/JSONConfiguredStore.swift b/Sources/JSONInspection/Store/JSONConfiguredStore.swift index b58a220..5c0428b 100644 --- a/Sources/JSONInspection/Store/JSONConfiguredStore.swift +++ b/Sources/JSONInspection/Store/JSONConfiguredStore.swift @@ -3,27 +3,53 @@ import JSON import JSONNavigation private final class JSONStoreOwner: Sendable {} -package struct JSONStorePosition: Sendable { +/// Unforgeable position bound to one immutable store instance, not a persistent ID. +public struct JSONStorePosition: Sendable, Equatable { fileprivate let owner: JSONStoreOwner - package let collection: Int - package let row: Int + /// Zero-based physical source collection position. + public let collection: Int + /// Zero-based physical row in that collection. + public let row: Int + /// Compares exact owner identity and fixed local ordinals; no JSON scan. + /// - Parameters: + /// - lhs: First owner-bound position. + /// - rhs: Second owner-bound position. + /// - Returns: True only for the same source instance and physical record. + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.owner === rhs.owner && lhs.collection == rhs.collection && lhs.row == rhs.row + } } -package enum JSONStoreReferenceState: Sendable { +/// A declared reference slot; missing and explicit null remain distinct. +public enum JSONStoreReferenceState: Sendable { + /// Optional field absent from the original record. case absent + /// Explicit JSON null accepted by the field policy. case null + /// A validated existing target in this same store. + /// - Parameter position: Exact owner-bound target position. 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 +/// Immutable source metadata; value resolution is a separately metered operation. +public struct JSONStoreRecordMetadata: Sendable { + /// Exact instance-bound physical position. + public let position: JSONStorePosition + /// Original decoded-scalar string ID. + public let id: String + /// Absolute physical path of the record. + public let path: [JSONLocationComponent] + /// Absolute path of its configured ID member. + public let idPath: [JSONLocationComponent] + /// Half-open range into the containing store references array. + public let referenceRange: Range } -package struct JSONStoreReferenceMetadata: Sendable { - package let field: Int - package let path: [JSONLocationComponent] - package let state: JSONStoreReferenceState +/// Immutable configured-slot metadata in physical record and declaration order. +public struct JSONStoreReferenceMetadata: Sendable { + /// Zero-based field declaration in the record collection profile. + public let field: Int + /// Exact absolute field path, including absent fields. + public let path: [JSONLocationComponent] + /// Absent, explicit null or a validated same-store link. + public let state: JSONStoreReferenceState } internal struct StoreCollectionMetadata: Sendable { let profile: Int @@ -31,25 +57,46 @@ internal struct StoreCollectionMetadata: Sendable { 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 +/// Immutable completely admitted source, records and explicitly validated references. +/// Construct through JSONStoreBuilder. No mutable store, I/O or persistent identity. +public struct JSONConfiguredStore: Sendable { + /// Original complete canonical source, optionally with coherent source mapping. + public let document: JSONDocument + /// Immutable explicit collection/reference interpretation. + public let profile: JSONStoreProfile private let owner: JSONStoreOwner private let collections: [StoreCollectionMetadata] private let records: [JSONStoreRecordMetadata] - package let references: [JSONStoreReferenceMetadata] + /// All immutable configured slots, in physical record and field order. + public 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 { + /// Number of complete physical collections. + public var collectionCount: Int { collections.count } + /// Total complete physical records. + public var recordCount: Int { records.count } + /// Fixed metadata access without a new owned operation. + /// - Parameter collection: Zero-based physical collection position. + /// - Returns: Complete record count in that collection. + /// - Throws: Invalid local collection position. + public 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 } + /// Reads a physical collection's exact name without scanning or opening a ledger. + /// - Parameter collection: Zero-based physical source collection position. + /// - Returns: Exact configured source name. + /// - Throws: Invalid collection position. + public func collectionName(at collection: Int) throws -> JSONKey { + guard collection >= 0, collection < collections.count else { + throw JSONStoreFailure(.invalidPosition, phase: 0, domain: .operation) + } + return profile.collections[collections[collection].profile].name + } + fileprivate init(document: JSONDocument, profile: JSONStoreProfile, owner: JSONStoreOwner, collections: [StoreCollectionMetadata], records: [JSONStoreRecordMetadata], references: [JSONStoreReferenceMetadata], overlay: UInt64, aliases: UInt64, @@ -60,14 +107,23 @@ package struct JSONConfiguredStore: Sendable { profileTextBytes = profileText } /// Checked fixed metadata access; positions are bound to this exact owner instance. - package func record(collection: Int, row: Int) throws -> JSONStoreRecordMetadata { + /// - Parameters: + /// - collection: Physical collection position. + /// - row: Physical row position. + /// - Returns: Immutable metadata from this exact store. + /// - Throws: Invalid local position. + public 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 { + /// Reads fixed metadata with exact owner validation, without scanning JSON. + /// - Parameter position: Unforgeable position from this exact store instance. + /// - Returns: Original immutable record metadata. + /// - Throws: Foreign-owner or out-of-bounds position; never a null substitute. + public func record(at position: JSONStorePosition) throws -> JSONStoreRecordMetadata { guard position.owner === owner else { throw JSONStoreFailure(.invalidPosition, phase: 0, domain: .operation) } @@ -75,20 +131,35 @@ package struct JSONConfiguredStore: Sendable { } } -package struct JSONStoreBuilder: Sendable { - package let document: JSONDocument - package let profile: JSONStoreProfile - package let limits: JSONLimits - package let caps: JSONStoreCaps +/// Builds a complete immutable configured store using the original native ledger. +/// Profile preparation, admission, all construction phases and publication share it. +public struct JSONStoreBuilder: Sendable { + /// Original canonical source; admitted once during build. + public let document: JSONDocument + /// Closed typed configuration checked before source admission. + public let profile: JSONStoreProfile + /// Native cumulative operation quotas. + public let limits: JSONLimits + /// Additional inclusive store construction policies. + public 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, + /// Captures trusted immutable inputs; validation occurs inside build. + /// - Parameters: + /// - document: Complete canonical source, optionally with mapping. + /// - profile: Explicit exact collection/ID/reference declarations. + /// - limits: Finite cumulative native quotas. + /// - caps: Additional finite store policies, not physical capacity promises. + public 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 { + /// Completes all construction phases atomically or throws, with no partial store. + /// - Returns: Immutable admitted store and the one original operation report. + /// - Throws: Profile/source/capacity/core resource errors or cancellation. + public func build() throws -> JSONOperationReport { try Task.checkCancellation() try limits.validate() try caps.validate() diff --git a/Sources/JSONInspection/Store/JSONStoreProfile.swift b/Sources/JSONInspection/Store/JSONStoreProfile.swift index d581aa3..0d5ffef 100644 --- a/Sources/JSONInspection/Store/JSONStoreProfile.swift +++ b/Sources/JSONInspection/Store/JSONStoreProfile.swift @@ -1,62 +1,126 @@ // SPDX-License-Identifier: Apache-2.0 import JSON -package enum JSONStoreProfileVersion: String, Sendable { case v1 = "nav12.store-profile/1" } -package enum JSONStoreMarkerVersion: String, Sendable { case v1 = "collection-id/1" } -package enum JSONStoreFieldPresence: String, Sendable { case required, optional } -package enum JSONStoreReferences: Sendable { +/// Closed version of the typed configured-store construction contract. +public enum JSONStoreProfileVersion: String, Sendable { + /// Exact collection arrays, explicit string identifiers and declared reference slots. + case v1 = "nav12.store-profile/1" +} +/// Closed reference-marker interpretation, independent of JSON Schema $ref. +public enum JSONStoreMarkerVersion: String, Sendable { + /// An object containing exactly the string members collection and id. + case v1 = "collection-id/1" +} +/// Requiredness of one explicitly declared reference field. +public enum JSONStoreFieldPresence: String, Sendable { + /// A missing member is a construction error. + case required + /// A missing member is retained as an absent slot. + case optional +} +/// Explicit reference interpretation; undeclared fields always remain ordinary JSON. +public enum JSONStoreReferences: Sendable { + /// Interpret no fields as references. case disabled + /// Validate exact markers and their existing targets before publishing a store. + /// - Parameters: + /// - marker: Closed version of the two-member marker grammar. + /// - validateTargets: Must be true; false is a typed profile refusal. case strict(marker: JSONStoreMarkerVersion, validateTargets: Bool) } -package struct JSONStoreReferenceField: Sendable { - package let key: JSONKey - package let presence: JSONStoreFieldPresence - package let nullable: Bool - package init(key: JSONKey, presence: JSONStoreFieldPresence, nullable: Bool) { +/// One configured direct reference field; no name or null policy is inferred. +public struct JSONStoreReferenceField: Sendable { + /// Exact decoded-scalar field name. + public let key: JSONKey + /// Whether a missing member is accepted as an absent slot. + public let presence: JSONStoreFieldPresence + /// Whether explicit JSON null is accepted as a null slot. + public let nullable: Bool + /// Captures trusted typed configuration, checked on construction's one ledger. + /// - Parameters: + /// - key: Literal exact member name. + /// - presence: Required or optional field policy. + /// - nullable: Explicit null policy, independent of absence. + public init(key: JSONKey, presence: JSONStoreFieldPresence, nullable: Bool) { self.key = key; self.presence = presence; self.nullable = nullable } } -package struct JSONStoreCollectionProfile: Sendable { - package let name: JSONKey - package let recordsAt: [JSONLocationComponent] - package let idKey: JSONKey - package let referenceFields: [JSONStoreReferenceField] - package init(name: JSONKey, recordsAt: [JSONLocationComponent], idKey: JSONKey, - referenceFields: [JSONStoreReferenceField]) { +/// A named direct array collection with an explicit exact string-ID field. +public struct JSONStoreCollectionProfile: Sendable { + /// Exact collection name; canonical Unicode equivalence does not merge it. + public let name: JSONKey + /// Must be exactly one key component matching name inside collectionsAt. + public let recordsAt: [JSONLocationComponent] + /// Exact direct record member holding its string ID; no id/_id fallback. + public let idKey: JSONKey + /// Declared reference slots, interpreted in this order. + public let referenceFields: [JSONStoreReferenceField] + /// Captures an explicit collection declaration without scanning source data. + /// - Parameters: + /// - name: Exact direct collection name. + /// - recordsAt: One direct key equal to name; checked during preparation. + /// - idKey: Required exact string identifier member. + /// - referenceFields: Ordered explicit reference declarations, or []. + public init(name: JSONKey, recordsAt: [JSONLocationComponent], idKey: JSONKey, + referenceFields: [JSONStoreReferenceField]) { self.name = name; self.recordsAt = recordsAt; self.idKey = idKey self.referenceFields = referenceFields } } -/// Closed native input, not a new JSON wire decoder or public configuration API. -package struct JSONStoreProfile: Sendable { - package let version: JSONStoreProfileVersion - package let collectionsAt: [JSONLocationComponent] - package let collections: [JSONStoreCollectionProfile] - package let references: JSONStoreReferences - package init(version: JSONStoreProfileVersion, collectionsAt: [JSONLocationComponent], - collections: [JSONStoreCollectionProfile], references: JSONStoreReferences) { +/// Closed native configuration, checked before source admission on the same ledger. +/// It is neither a JSON wire-profile decoder nor an online resource resolver. +public struct JSONStoreProfile: Sendable { + /// Closed construction contract version. + public let version: JSONStoreProfileVersion + /// Absolute exact typed selection of the collections object. + public let collectionsAt: [JSONLocationComponent] + /// Explicit collection declarations; unconfigured source collections are refused. + public let collections: [JSONStoreCollectionProfile] + /// Disabled or strict reference interpretation; no ambient reference behavior. + public let references: JSONStoreReferences + /// Captures immutable typed policy; construction performs bounded validation. + /// - Parameters: + /// - version: Explicit closed profile version. + /// - collectionsAt: Absolute collections-object selection, including []. + /// - collections: Exact declared direct collections. + /// - references: Explicit reference mode. + public init(version: JSONStoreProfileVersion, collectionsAt: [JSONLocationComponent], + collections: [JSONStoreCollectionProfile], references: JSONStoreReferences) { self.version = version; self.collectionsAt = collectionsAt self.collections = collections; self.references = references } } -/// Finite prototype policy only. All maxima inclusive, inside unchanged core limits. -package struct JSONStoreCaps: Sendable { - package var collections = 256 - package var recordsPerCollection = 10_000 - package var records = 10_000 - package var referenceFieldsPerCollection = 16 - package var referenceFields = 4_096 - package var referenceSlots = 100_000 - package var links = 100_000 - package var identityBytes = 4_096 - package var profileTextBytes = 1_048_576 - package var aliasBytes = 16_777_216 - package var pathComponents = 128 - package var overlayBytes = 33_554_432 - package init() {} +/// Finite inclusive construction policies within unchanged native limits. +/// Maxima are not jointly achievable capacity or RSS/throughput guarantees. +public struct JSONStoreCaps: Sendable { + /// Maximum configured/source collections. + public var collections = 256 + /// Maximum records in each complete collection. + public var recordsPerCollection = 10_000 + /// Maximum records across all collections. + public var records = 10_000 + /// Maximum configured reference fields in each collection. + public var referenceFieldsPerCollection = 16 + /// Maximum configured fields across all collections. + public var referenceFields = 4_096 + /// Maximum interpreted slots, including absent and null slots. + public var referenceSlots = 100_000 + /// Maximum staged links; repeated targets remain distinct slots. + public var links = 100_000 + /// Maximum UTF-8 bytes in a collection or record identity. + public var identityBytes = 4_096 + /// Maximum cumulative profile UTF-8 text bytes. + public var profileTextBytes = 1_048_576 + /// Maximum live logical ID/marker text-alias bytes. + public var aliasBytes = 16_777_216 + /// Maximum typed components in generated/configured paths. + public var pathComponents = 128 + /// Maximum conservative logical metadata/scratch overlay bytes. + public var overlayBytes = 33_554_432 + /// Uses finite policy defaults; zero is finite, negatives and Int.max invalid. + public init() {} internal func validate() throws { - // Fixed bootstrap size; no source/profile text scanned before native health. for value in [collections, recordsPerCollection, records, referenceFieldsPerCollection, referenceFields, referenceSlots, links, identityBytes, profileTextBytes, aliasBytes, pathComponents, overlayBytes] { @@ -65,28 +129,98 @@ package struct JSONStoreCaps: Sendable { } } -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 +/// Stable typed construction/selection refusals, separate from native quota errors. +public enum JSONStoreFailureCode: String, Sendable { + /// A configured typed index is negative. + case invalidTypedComponent + /// Two profile collection names have identical decoded scalars. + case duplicateCollectionDeclaration + /// A collection selector is not its one exact direct name. + case invalidRecordsAt + /// Two configured fields have identical decoded scalars. + case duplicateReferenceField + /// An ID member is also declared as a reference. + case idReferenceFieldCollision + /// Reference declarations conflict with disabled references. + case disabledReferencesConflict + /// Strict mode attempted to disable target validation. + case invalidReferenceMode + /// The selected collections value is not an object. + case collectionsNotObject + /// A source collection has no explicit declaration. + case unconfiguredCollection + /// A declared collection value is not an array. + case collectionNotArray + /// A declared collection is absent from the source. + case missingConfiguredCollection + /// A physical record is not an object. + case recordWrongKind + /// The required ID member is absent. + case recordIDMissing + /// The required ID is explicit null. + case recordIDNull + /// The ID is present with a non-string kind. + case recordIDWrongKind + /// The second exact duplicate ID retains its first ID path. + case duplicateRecordID + /// A required declared reference field is absent. + case referenceMissing + /// A declared reference field forbids explicit null. + case referenceNull + /// A non-null marker is not an object. + case referenceMarkerWrongKind + /// An exact collection/id marker member is missing. + case referenceMarkerMissing + /// A marker member has a non-string kind. + case referenceMarkerMemberWrongKind + /// A marker has an undeclared extra member. + case referenceMarkerExtra + /// A marker names an unknown exact collection. + case referenceUnknownCollection + /// A marker names a missing ID in an existing collection. + case referenceDanglingID + /// The collections selection is missing; coreCause retains the original error. + case selectionMissing + /// The collections selection crosses a wrong kind. + case selectionWrongKind + /// A finite store construction or follow policy was exceeded. + case capacityExceeded + /// Checked size/counter arithmetic overflowed. + case counterOverflow + /// A local position is out of bounds or belongs to another store instance. + case invalidPosition + /// A lookup request names no declared physical collection. + case unknownCollection +} +/// The authority that refused construction or a read-only operation. +public enum JSONStoreFailureDomain: String, Sendable { + /// Invalid typed configuration, before source admission. + case profile + /// Invalid source structure or declared references. + case source + /// Invalid local position, query policy or checked capacity. + case operation } -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? +/// Precise checked refusal; no partial store or null substitute is published. +public struct JSONStoreFailure: Error, Sendable { + /// Stable refusal identity. + public let code: JSONStoreFailureCode + /// Construction phases1...8; ordinary metadata0 and owned queries9. + public let phase: Int + /// Profile, source or operation authority. + public let domain: JSONStoreFailureDomain + /// Exact source/request path, empty when no location applies. + public let path: [JSONLocationComponent] + /// First occurrence for a duplicate ID; never a normalized key. + public let firstPath: [JSONLocationComponent]? + /// Coherent original span, or nil when unavailable. + public let sourceSpan: JSONSourceSpan? + /// Original core selection failure without loss of its resource/location metadata. + public let coreCause: JSONFailure? + /// Named construction/follow capacity for a capacity refusal. + public let capacity: String? + /// Inclusive configured capacity, distinct from physical memory. + public let limit: Int? internal init(_ code: JSONStoreFailureCode, phase: Int, domain: JSONStoreFailureDomain, path: [JSONLocationComponent] = [], firstPath: [JSONLocationComponent]? = nil, sourceSpan: JSONSourceSpan? = nil, coreCause: JSONFailure? = nil, diff --git a/Sources/JSONInspection/Store/JSONStoreQueries.swift b/Sources/JSONInspection/Store/JSONStoreQueries.swift new file mode 100644 index 0000000..0055990 --- /dev/null +++ b/Sources/JSONInspection/Store/JSONStoreQueries.swift @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Inclusive finite policies for a complete reachable closure, not truncation. +public struct JSONStoreFollowLimits: Sendable { + /// Maximum shortest link distance from the start; start has depth zero. + public var maximumDepth: Int = 64 + /// Maximum pending BFS positions, excluding already consumed queue entries. + public var maximumFrontier: Int = 10_000 + /// Maximum distinct visited records, including the start. + public var maximumVisited: Int = 10_000 + /// Maximum returned positions, including the start. + public var maximumResults: Int = 10_000 + /// Uses finite defaults; zero is valid, negative values and Int.max are invalid. + public init() {} + internal func validate() throws { + for value in [maximumDepth, maximumFrontier, maximumVisited, maximumResults] { + guard value >= 0, value < Int.max else { throw JSONFailure(.configuration, code: .invalidLimits) } + } + } +} + +extension JSONConfiguredStore { + /// Looks up an exact collection/string ID in one independent admitted operation. + /// No canonical normalization or id/_id fallback. Missing IDs return nil; + /// an unknown collection is a distinct typed refusal. Metadata scans and text + /// comparisons use the same source-admission ledger; no index speed is promised. + /// - Parameters: + /// - collection: Exact decoded-scalar configured collection name. + /// - id: Exact decoded-scalar string record identifier. + /// - limits: Finite cumulative native quotas for this lookup. + /// - Returns: Owner-bound position or nil, plus cumulative statistics. + /// - Throws: Unknown collection, native configuration/resource failure or cancellation. + public func lookup(collection: JSONKey, id: String, limits: JSONLimits = .balanced) + throws -> JSONOperationReport { + let report = try query(.lookup(collection, id), limits: limits) + guard case .position(let result) = report.result else { throw JSONFailure(.configuration, code: .invalidOperation) } + return .init(result: result, statistics: report.statistics) + } + + /// Reads exact values in request order, preserving repeated positions. + /// One source admission, metadata reservation and ledger cover the entire batch. + /// Empty requests still admit the whole source; foreign tokens never become null. + /// Result aliases follow ordinary native read semantics and caller-copy boundaries. + /// - Parameters: + /// - positions: Unforgeable same-store record positions. + /// - maximumRequests: Inclusive finite request/result cap, default1024. + /// - limits: Finite native quotas for the whole independent batch. + /// - Returns: All canonical record objects and cumulative statistics. + /// - Throws: Position/batch/core resource/configuration failure or cancellation. + public func values(at positions: [JSONStorePosition], maximumRequests: Int = 1_024, + limits: JSONLimits = .balanced) throws -> JSONOperationReport<[JSON]> { + try checkInspectionBatch(positions.count, maximum: maximumRequests) + let report = try query(.values(positions), limits: limits) + guard case .values(let result) = report.result else { throw JSONFailure(.configuration, code: .invalidOperation) } + return .init(result: result, statistics: report.statistics) + } + + /// Resolves one same-store position through the one-request batch delegate. + /// - Parameters: + /// - position: Unforgeable same-store physical record position. + /// - limits: Finite native quotas for this independent admitted read. + /// - Returns: The original canonical record and actual operation statistics. + /// - Throws: Invalid position, core resource/configuration failure or cancellation. + public func value(at position: JSONStorePosition, limits: JSONLimits = .balanced) + throws -> JSONOperationReport { + let report = try values(at: [position], maximumRequests: 1, limits: limits) + return .init(result: report.result[0], statistics: report.statistics) + } + + /// Returns the complete BFS reachable closure, including start exactly once. + /// Only declared validated link slots are edges. Absent/null slots add no edge; + /// cycles are deduplicated by physical positions. Any exceeded depth/frontier/ + /// visited/result cap throws instead of publishing a truncated closure. No I/O. + /// - Parameters: + /// - start: Owner-bound starting record in this immutable store. + /// - bounds: Inclusive finite BFS policies, including a finite depth ceiling. + /// - limits: Finite native quotas shared by admission, aliases, scans and output. + /// - Returns: Deterministic positions in configured-field BFS order and statistics. + /// - Throws: Invalid position, typed capacity/native resource failure or cancellation. + public func follow(from start: JSONStorePosition, bounds: JSONStoreFollowLimits = .init(), + limits: JSONLimits = .balanced) throws -> JSONOperationReport<[JSONStorePosition]> { + try Task.checkCancellation(); try bounds.validate() + let report = try query(.follow(start, bounds), limits: limits) + guard case .positions(let result) = report.result else { throw JSONFailure(.configuration, code: .invalidOperation) } + return .init(result: result, statistics: report.statistics) + } + + package func followForQualification(from start: JSONStorePosition, bounds: JSONStoreFollowLimits, + limits: JSONLimits, checkpointObserver: @Sendable @escaping (UInt64) -> Void, + lifetimeObserver: @Sendable @escaping (Bool, Bool) -> Void, + edgeObserver: (@Sendable (Int) -> Void)? = nil) throws -> JSONOperationReport<[JSONStorePosition]> { + try bounds.validate() + let report = try query(.follow(start, bounds), limits: limits, + checkpointObserver: checkpointObserver, lifetimeObserver: lifetimeObserver, edgeObserver: edgeObserver) + guard case .positions(let result) = report.result else { throw JSONFailure(.configuration, code: .invalidOperation) } + return .init(result: result, statistics: report.statistics) + } + + private func query(_ operation: StoreReadOperation, limits: JSONLimits, + checkpointObserver: (@Sendable (UInt64) -> Void)? = nil, + lifetimeObserver: (@Sendable (Bool, Bool) -> Void)? = nil, + edgeObserver: (@Sendable (Int) -> Void)? = nil) + throws -> JSONOperationReport { + var reducer = StoreReadReducer(store: self, operation: operation, edgeObserver: edgeObserver) + var traversal = JSONTraversal(document: document, limits: limits) + traversal.checkpointObserver = checkpointObserver + traversal.lifetimeObserver = lifetimeObserver + return try traversal.inspectPreparedOwned(options: .init(depthLimit: 0), reducer: &reducer) + } +} + +private enum StoreReadOperation { + case lookup(JSONKey, String) + case values([JSONStorePosition]) + case follow(JSONStorePosition, JSONStoreFollowLimits) +} +private enum StoreReadResult { + case position(JSONStorePosition?) + case values([JSON]) + case positions([JSONStorePosition]) +} + +private struct StoreReadReducer: JSONReadPreparation, JSONTraversalOwnedReducer { + let store: JSONConfiguredStore + let operation: StoreReadOperation + let edgeObserver: (@Sendable (Int) -> Void)? + private var inputAliases: UInt64 = 0 + init(store: JSONConfiguredStore, operation: StoreReadOperation, edgeObserver: (@Sendable (Int) -> Void)?) { + self.store = store; self.operation = operation; self.edgeObserver = edgeObserver + } + mutating func prepare(context: inout JSONReadPreparationContext) throws { + inputAliases = try JSONStoreAccounting.sum(store.overlayLogicalBytes, 128) + try context.retain(inputAliases) + switch operation { + case .lookup(let collection, let id): + try context.admitText(collection.string, key: true); try context.admitText(id) + for text in [collection.string, id] { + for _ in text.utf8 { try context.work(); inputAliases = try JSONStoreAccounting.sum(inputAliases, 1) } + } + case .values(let positions): + for position in positions { + try context.work(); _ = try store.record(at: position) + try context.retain(8); inputAliases = try JSONStoreAccounting.sum(inputAliases, 8) + } + case .follow(let start, _): + try context.work(); _ = try store.record(at: start) + } + } + 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 -> StoreReadResult { + let result: StoreReadResult + switch operation { + case .lookup(let collection, let id): result = .position(try lookup(collection, id: id, context: &context)) + case .values(let positions): + var values: [JSON] = []; try context.retain(32) + for position in positions { + try context.work() + let record = try store.record(at: position) + let value = try context.selectedValue(at: record.path) + try context.retain(32); values.append(value) + } + result = .values(values) + case .follow(let start, let bounds): result = .positions(try follow(start, bounds: bounds, context: &context)) + } + try context.checkpoint(); try context.release(inputAliases) + return result + } + private func lookup(_ name: JSONKey, id: String, context: inout JSONTraversalContext) throws -> JSONStorePosition? { + try context.retain(32) + for collection in 0.. Int { + try context.work(); _ = try store.record(at: position) + var offset: UInt64 = 0 + for collection in 0.. [JSONStorePosition] { + var visited: [Bool] = [], result: [JSONStorePosition] = [], depths: [Int] = [] + try context.retain(32) + for _ in 0.. JSONStoreFailure { + .init(.capacityExceeded, phase: 9, domain: .operation, capacity: name, limit: limit) + } + func enqueue(_ position: JSONStorePosition, depth: Int, context: inout JSONTraversalContext) throws { + try context.work() + guard depth <= bounds.maximumDepth else { throw refuse("maximumDepth", bounds.maximumDepth) } + guard result.count - head < bounds.maximumFrontier else { throw refuse("maximumFrontier", bounds.maximumFrontier) } + guard result.count < bounds.maximumVisited else { throw refuse("maximumVisited", bounds.maximumVisited) } + guard result.count < bounds.maximumResults else { throw refuse("maximumResults", bounds.maximumResults) } + let index = try flatIndex(position, context: &context) + try context.retain(40); visited[index] = true; result.append(position); depths.append(depth) + } + try enqueue(start, depth: 0, context: &context) + while head < result.count { + try context.work() + let record = try store.record(at: result[head]), depth = depths[head]; head += 1 + for slot in record.referenceRange { + try context.work() + if case .link(let target) = store.references[slot].state { + let index = try flatIndex(target, context: &context) + if !visited[index] { + guard depth < bounds.maximumDepth else { throw refuse("maximumDepth", bounds.maximumDepth) } + try enqueue(target, depth: depth + 1, context: &context) + } + } + edges += 1; edgeObserver?(edges) + try context.checkpoint() + } + } + let scratch = try JSONStoreAccounting.sum(UInt64(visited.count), JSONStoreAccounting.product(UInt64(depths.count), 8)) + visited = []; depths = []; try context.release(scratch) + return result + } +} diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift index d1c986e..7bf4fff 100644 --- a/Sources/JSONNavigation/JSONTraversalContext.swift +++ b/Sources/JSONNavigation/JSONTraversalContext.swift @@ -124,6 +124,21 @@ package struct JSONTraversalContext: ~Copyable { try scope.document?.sourceMap?.span(at: path, kind: .value, meter: &state.meter) } } + /// Resolve a typed selection on this original admitted scope; no new ledger. + /// The value is an ephemeral native alias; returned owned slots need reservation. + package mutating func selectedValue(at path: [JSONLocationComponent]) throws -> JSON { + try perform(at: path) { state in + // A metadata path is not automatically admitted by this invocation. + // Use the original scope's checked path constructor before selection. + var checked: [JSONLocationComponent] = [] + for component in path { + checked = try scope.append(component, to: checked, state: state).components + } + try state.meter.work() + return try scope.select(checked, state: state) + } + } + /// Transfer already-admitted input ownership, without exposing its scope. package mutating func transferDocumentOwnership() throws -> JSONDocument { try perform { _ in try scope.transferDocumentOwnership() } diff --git a/Sources/JSONTransform/Documentation.docc/IndexedSelection.md b/Sources/JSONTransform/Documentation.docc/IndexedSelection.md new file mode 100644 index 0000000..c2f9000 --- /dev/null +++ b/Sources/JSONTransform/Documentation.docc/IndexedSelection.md @@ -0,0 +1,83 @@ +# Select explicit population positions + +Evaluate the existing query language over finite indices, preserving original +positions even when values are identical. Selection convention V1 supplies +`[0,...,N-1]` as input and the complete population in the reserved `$values` +binding. Declare `values` at compilation; supply exactly the remaining names +through explicit bindings. Stored values acquire no engine metadata. + +```swift +import JSON +import JSONTransform + +let definition = try JSONTransformProgramDefinition.prepare( + source: ".[] | select($values[.].enabled)", variables: ["values"]).result +let plan = try definition.compile().result +let row = try JSON.parse(#"{"enabled":true,"amount":9007199254740993}"#) +let report = try plan.withSelection(population: [row, row]) { scope in + var positions: [JSONTransformSelectionPosition] = [] + while let position = try scope.next() { positions.append(position) } + return positions +} +let ordinals = try report.result.map { try report.completion.ordinal(of: $0) } +precondition(ordinals == [0, 1]) +precondition(report.completion.populationCount == 2) +precondition(report.completion.outputCount == 2) +``` + +The evaluator is the ordinary Transform VM. Queries must emit exact nonnegative +UInt64 ordinals within the population and the native Int range. Mathematical +integer spellings such as 42.0 and negative zero convert exactly. Fractions, +negative values, overflow, other JSON kinds and out-of-range outputs fail without +Double conversion. Order and repeats are preserved. Sort tie rules belong in +the query. The engine supplies no arbitrary transformed-value provenance. + +## Completion and ownership + +Pull nil explicitly. Returning after a prefix, even an empty prefix, throws +``JSONTransformSelectionFailure/incompleteSelection``. A late invalid output, +resource failure, cancellation, provider failure or reentry latches outside query +catch handling; a caller catch cannot create a completion witness. Ordinary query +language catches continue to work. A first operational failure takes precedence +over a later caller-body error. An otherwise uncaught body error keeps its identity. + +Only ``JSONTransformProgram/withSelection(population:bindings:limits:source:_:)`` +creates a report and completion after EOF. Their initializers and position/scope +construction are inaccessible to public callers. Completion verifies a position's +invocation identity through ``JSONTransformSelectionCompletion/ordinal(of:)``; +identical population values from another invocation still throw foreignPosition. +That constant metadata inspection is caller work and does not reopen the VM or +retroactively change a completed invocation. + +A position retains the admitted population and its output-slot lease after scope +exit. A completion retains identity/report storage, not the population. Copies +share reservations. Escaped scopes and closures cannot resume pulls after close. +Use ``JSONTransformSelectionScope/report()`` to settle deferred ARC refunds and +observe actual native counts after success, failure or cancellation. Work never +refunds. Completion statistics are the successful close-time snapshot, not an +automatically updating post-ARC view. Keep the appropriate handles alive while +using accounted aliases. Scope, positions and completion are non-Sendable; +immutable compiled plans are Sendable and support independent concurrent scopes. + +## Policy and providers + +``JSONTransformSelectionLimits`` defaults to 65,536 population values and balanced +Transform/native limits. Population admission, index slots, all VM pulls, native +numeric validation, output slots, provider admission and completion storage share +one invocation ledger. A query producing no results still admits the whole +population. The original 49 core caps remain additional finite policies. +Logical weights are not heap allocation sizes or process RSS. + +An optional explicit source callback serves the existing input/inputs operators. +Nil provider output means EOF; JSON null is a value and fails if emitted as an +ordinal. Providers may influence selection but cannot enlarge the declared +population. Their own instructions and already allocated values are caller work. +Provider values and calls triggered by evaluation remain subject to cumulative +native/Transform admission and input/work limits. Provider failure/cancellation +stops further callbacks; a caught reentrant pull also fails the invocation. +During the callback, report/used/remainingWorkUnits expose a prepaid entry snapshot. + +This is a finite selection channel, not a stable Content ID, locator, read grant, +persistent execution, unbounded sort/stream guarantee or downstream admission. +The selection convention revision is separate from the definition/dialect revision +and the complete effective compilation/execution policies. diff --git a/Sources/JSONTransform/Documentation.docc/JSONTransform.md b/Sources/JSONTransform/Documentation.docc/JSONTransform.md index ee828d3..c17abaf 100644 --- a/Sources/JSONTransform/Documentation.docc/JSONTransform.md +++ b/Sources/JSONTransform/Documentation.docc/JSONTransform.md @@ -2,9 +2,10 @@ Compile the existing exact query dialect and pull bounded native JSON results. -This is the optional transformation library in the unreleased 1.7 source line. -Development main and a separately versioned candidate retain their own source -identities and qualification. Publication requires a separate maintainer decision. It shares one private compiler +The library was introduced in 1.7.0. This 1.8 candidate adds exact source definitions +successful compile reports and guarded finite indexed selection with owner-bound +positions and explicit completion witnesses. +Source identities and qualification stay separate. 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. @@ -18,7 +19,25 @@ finite cumulative meter and a guarded, non-Sendable execution handle. - ``JSONTransformExecution`` - ``JSONTransformOutput`` +### Source definitions + +- +- ``JSONTransformProgramDefinition`` +- ``JSONTransformDefinitionLimits`` +- ``JSONTransformDefinitionFailure`` + ### Limits and failures - ``JSONTransformLimits`` - ``JSONTransformFailure`` + +### Indexed selection + +- + +- ``JSONTransformSelectionLimits`` +- ``JSONTransformSelectionScope`` +- ``JSONTransformSelectionPosition`` +- ``JSONTransformSelectionCompletion`` +- ``JSONTransformSelectionReport`` +- ``JSONTransformSelectionFailure`` diff --git a/Sources/JSONTransform/Documentation.docc/SourceDefinitions.md b/Sources/JSONTransform/Documentation.docc/SourceDefinitions.md new file mode 100644 index 0000000..af53fea --- /dev/null +++ b/Sources/JSONTransform/Documentation.docc/SourceDefinitions.md @@ -0,0 +1,41 @@ +# Persist exact source definitions + +Store a versioned source definition without serializing compiled VM state. +Definitions are immutable and checked Sendable; source keeps its decoded scalar +spelling and declared ASCII names are unique in ascending UTF-8 order. + +```swift +import JSON +import JSONTransform + +let definition = try JSONTransformProgramDefinition.prepare( + source: "$amount", variables: ["amount"]).result +let bytes = try definition.encode().result +let restored = try JSONTransformProgramDefinition.decode(bytes).result +let compiled = try restored.compile() +let amount = JSON.number(try JSONNumber(parsing: "9007199254740993")) +let output = try compiled.result.withExecution(input: .null, bindings: ["amount": amount]) { + try $0.next()?.value +} +precondition(output.result == amount) +precondition(compiled.statistics[.workUnits] > 0) +``` + +Prepare, decode, encode and compile each have their own explicit finite operation. +Within an operation all helpers share one ledger. Codec limits bound source/names, +wire bytes, native traversal, sorting and logical retention. Execution limits are +supplied separately. Logical reservations are not RSS or caller instruction quotas. + +The closed V1 object contains definition, version, dialect, source and variables +in that canonical order. Strict decoding accepts equivalent native JSON spelling +and field order, then encoding emits canonical compact bytes. It rejects unknown +fields, duplicate keys/names and unsupported revisions. Canonical-input checking +is a separate gateway policy; codec decoding alone does not assert byte identity. +Malformed JSON remains a native JSON error; invalid query syntax is a compiler +error when compilation is explicitly requested. Neither schema validation nor a +new numeric or query engine is introduced. + +Use ``JSONTransformProgram/compileWithReport(_:variables:limits:)`` when compiling +ordinary source with successful native work statistics. These reports are data; +they are not unforgeable selection-completion witnesses. The separate guide defines the finite public selection +convention, owner-bound positions and EOF witnesses. diff --git a/Sources/JSONTransform/JSONTransformProgram.swift b/Sources/JSONTransform/JSONTransformProgram.swift index a0c5cfb..bd06c3c 100644 --- a/Sources/JSONTransform/JSONTransformProgram.swift +++ b/Sources/JSONTransform/JSONTransformProgram.swift @@ -20,6 +20,30 @@ public struct JSONTransformProgram: Sendable { limits: JSONTransformLimits = .init()) throws -> Self { do { var context = try TransformRuntimeContext(limits: limits.runtime) + return try compileValidated(source, variables: variables, context: &context) + } catch { throw JSONTransformFailure.classify(error) } + } + + /// Compiles with the same compiler and returns actual successful native work. + /// Compilation is independent of later execution. Statistics are data, not a + /// selection-completion witness or a charge for caller-owned work. + /// - Parameters: + /// - source: Exact query source in the existing dialect. + /// - variables: Declared ASCII names without dollar prefixes. + /// - limits: Finite compilation policy shared by validation and compilation. + /// - Returns: The immutable program and this compilation's native statistics. + /// - Throws: The same typed compilation, configuration and operational failures as compile. + public static func compileWithReport(_ source: String, variables: Set = [], + limits: JSONTransformLimits = .init()) throws -> JSONOperationReport { + do { + var context = try TransformRuntimeContext(limits: limits.runtime) + let result = try compileValidated(source, variables: variables, context: &context) + return .init(result: result, statistics: context.meter.statistics()) + } catch { throw JSONTransformFailure.classify(error) } + } + + static func compileValidated(_ source: String, variables: Set, + context: inout TransformRuntimeContext) throws -> Self { for name in variables { try context.charge(.bindings) try JSONAdmission.text(name, key: true, meter: &context.meter, owned: true) @@ -36,7 +60,6 @@ public struct JSONTransformProgram: Sendable { } 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 guarded pull scope. diff --git a/Sources/JSONTransform/JSONTransformProgramDefinition.swift b/Sources/JSONTransform/JSONTransformProgramDefinition.swift new file mode 100644 index 0000000..bdcd32f --- /dev/null +++ b/Sources/JSONTransform/JSONTransformProgramDefinition.swift @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONFilterRuntime + +/// Finite codec policy, independent of compilation and execution policies. +/// Native work, traversal, text, cancellation and live logical storage limits also apply. +public struct JSONTransformDefinitionLimits: Sendable { + /// Native cumulative quotas for one prepare, decode or encode operation. + public var json: JSONLimits = .balanced + /// Maximum UTF-8 bytes in query source; default one MiB. + public var maximumSourceBytes: UInt64 = 1_048_576 + /// Maximum declared names, including every supplied occurrence before duplicate detection. + public var maximumVariableNames: UInt64 = 128 + /// Maximum UTF-8 bytes in one ASCII variable name. + public var maximumNameBytes: UInt64 = 128 + /// Maximum total UTF-8 bytes in all supplied variable names. + public var maximumTotalNameBytes: UInt64 = 8_192 + /// Inclusive input/output wire-byte ceiling, additionally bounded by native quotas. + public var maximumEncodedBytes: UInt64 = 2_097_152 + var checkpointObserver: (@Sendable (UInt64) -> Void)? + /// Creates the finite balanced codec policy; zero capacities remain valid. + public init() {} + + func effective() throws -> JSONLimits { + for cap in [maximumSourceBytes, maximumVariableNames, maximumNameBytes, + maximumTotalNameBytes, maximumEncodedBytes] { + guard cap < UInt64.max else { throw JSONFailure(.configuration, code: .invalidLimits) } + } + var result = json + result[.inputBytes] = min(result[.inputBytes], maximumEncodedBytes) + result[.outputBytes] = min(result[.outputBytes], maximumEncodedBytes) + try result.validate() + return result + } + func meter() throws -> JSONWorkMeter { + var result = try JSONWorkMeter(limits: effective(), domain: .core) + result.checkpointObserver = checkpointObserver + return result + } +} + +/// Closed-definition refusals; native JSON syntax/resource errors and cancellation stay distinct. +public enum JSONTransformDefinitionFailure: Error, Sendable, Equatable { + /// Wrong root, fields, field types or format identifier. + case invalidDefinition + /// A definition revision other than mathematical integer one. + case unsupportedDefinitionRevision + /// A query dialect other than swift-json-query-v1. + case unsupportedDialectRevision + /// A name outside the compiler's ASCII identifier grammar. + case invalidVariableName + /// Two identical names were supplied; no Set conversion silently removed one. + case duplicateVariableName + /// An inclusive codec-specific capacity was exceeded. + /// - Parameters: + /// - metric: Fixed codec policy identifier, without rejected payloads. + /// - limit: Configured finite maximum. + /// - attempted: Observed usage at refusal. + case limitExceeded(metric: String, limit: UInt64, attempted: UInt64) +} + +/// Immutable source definition, separate from compiled VM plans and effective policies. +/// Source scalar spelling is preserved. Names are unique ASCII identifiers in UTF-8 +/// order. No synthesized String equality, source rewriting or bytecode serialization is used. +public struct JSONTransformProgramDefinition: Sendable { + /// Exact decoded query source, including whitespace and scalar spelling. + public let source: String + /// Unique declared variable names in exact ascending UTF-8 order. + public let variableNames: [String] + /// Known wire definition revision; independent of implementation identity. + public var definitionRevision: UInt32 { 1 } + /// Known query-language revision; independent of limits and the CLI source digest. + public var dialectRevision: String { "swift-json-query-v1" } + + private init(validatedSource: String, canonicalNames: [String]) { + source = validatedSource; variableNames = canonicalNames + } + + /// Validates authored source metadata using one finite ledger; query compilation is explicit. + /// - Parameters: + /// - source: Exact query source; syntax is checked only by compile. + /// - variables: Names before sorting; exact duplicates are rejected. + /// - limits: Finite text, sort, traversal and retention policy. + /// - Returns: The immutable definition and actual successful validation accounting. + /// - Throws: Definition/name/capacity, native resource/configuration errors or cancellation. + public static func prepare(source: String, variables: [String] = [], + limits: JSONTransformDefinitionLimits = .init()) throws -> JSONOperationReport { + var meter = try limits.meter() + let result = try make(source: source, variables: variables, limits: limits, meter: &meter) + return .init(result: result, statistics: meter.statistics()) + } + + /// Decodes one strict native JSON document with an exact closed V1 field set. + /// Field order, whitespace and equivalent JSON escapes may differ; encoding + /// produces canonical bytes. Duplicate fields/names and unknown fields fail. + /// - Parameters: + /// - bytes: Caller-owned UTF-8 input; no ambient I/O or byte-copy prepass. + /// - limits: One cumulative policy for parsing, field checks and canonical names. + /// - Returns: The decoded definition and actual successful cumulative accounting. + /// - Throws: Native JSON syntax/resource errors, definition failures or cancellation. + public static func decode(_ bytes: [UInt8], limits: JSONTransformDefinitionLimits = .init()) throws -> JSONOperationReport { + var meter = try limits.meter() + var root = try JSONParser().parse(bytes, meter: &meter) + guard case .object(let object) = root, object.count == 5 else { throw JSONTransformDefinitionFailure.invalidDefinition } + try meter.retain(80) + var fields: [JSON?] = Array(repeating: nil, count: 5) + for member in object { + var found = false + for (index, key) in ["definition", "version", "dialect", "source", "variables"].enumerated() { + if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { + fields[index] = member.value; found = true; break + } + } + guard found else { throw JSONTransformDefinitionFailure.invalidDefinition } + } + guard case .string(let tag) = fields[0], + try JSONComparison.textEquals(tag, "swift-json-transform-source", meter: &meter), + case .number(let revision) = fields[1], case .string(let dialect) = fields[2], + case .string(let source) = fields[3], case .array(let names) = fields[4] else { + throw JSONTransformDefinitionFailure.invalidDefinition + } + guard try revision.compare(to: JSONNumber(integer: 1), meter: &meter) == .orderedSame else { + throw JSONTransformDefinitionFailure.unsupportedDefinitionRevision + } + guard try JSONComparison.textEquals(dialect, "swift-json-query-v1", meter: &meter) else { + throw JSONTransformDefinitionFailure.unsupportedDialectRevision + } + try require(UInt64(names.count), limits.maximumVariableNames, "variableNames", meter: &meter) + var strings: [String] = [] + for name in names { + try meter.work() + guard case .string(let text) = name else { throw JSONTransformDefinitionFailure.invalidDefinition } + try meter.retain(16); strings.append(text) + } + let baseline = meter.used(.retainedBytes) + let result = try make(source: source, variables: strings, limits: limits, meter: &meter) + let resultBytes = meter.used(.retainedBytes) - baseline + root = .null; fields = []; strings = [] + // Parsed tree and extraction aliases are temporary; result strings were + // independently reserved by make. Work is never refunded or restarted. + try meter.checkpoint(); try meter.release(meter.used(.retainedBytes) - resultBytes) + return .init(result: result, statistics: meter.statistics()) + } + + /// Encodes canonical compact UTF-8 with fixed metadata order and native escaping. + /// The ordered fields are definition, version, dialect, source, variables. + /// - Parameter limits: One finite ledger for admission, metadata, sorting and output. + /// - Returns: Canonical bytes without a trailing newline and successful accounting. + /// - Throws: Codec/native capacity or configuration errors, or cancellation. + public func encode(limits: JSONTransformDefinitionLimits = .init()) throws -> JSONOperationReport<[UInt8]> { + var meter = try limits.meter() + let admitted = try Self.make(source: source, variables: variableNames, limits: limits, meter: &meter) + var names: [JSON] = [] + for name in admitted.variableNames { try meter.work(); try meter.retain(16); names.append(.string(name)) } + try meter.retain(5 * JSONLogicalWeights.member) + let members: [JSONMember] = [ + .init("definition", value: .string("swift-json-transform-source")), + .init("version", value: .number(JSONNumber(integer: 1))), + .init("dialect", value: .string(dialectRevision)), + .init("source", value: .string(admitted.source)), + .init("variables", value: .array(names))] + let object = try JSONObject.checked(members: members, meter: &meter) + let bytes = try JSONWriter().write(.object(object), meter: &meter) + try meter.checkpoint(); try meter.release(meter.used(.retainedBytes) - UInt64(bytes.count)) + return .init(result: bytes, statistics: meter.statistics()) + } + + /// Compiles this definition using the existing compiler and one compilation context. + /// The source definition and codec policy are not captured by the compiled plan. + /// - Parameter limits: Explicit finite compilation policy, independent of codec policy. + /// - Returns: The native compiled program and actual successful compilation work. + /// - Throws: The existing typed compiler/configuration/resource/cancellation failures. + public func compile(limits: JSONTransformLimits = .init()) throws -> JSONOperationReport { + do { + var context = try TransformRuntimeContext(limits: limits.runtime) + var names: Set = [] + for name in variableNames { + // Admit hashing/scan work before constructing an owned Set entry. + for _ in name.utf8 { try context.work() } + try context.meter.retain(32) + names.insert(name) + try context.checkpoint() + } + let result = try JSONTransformProgram.compileValidated(source, variables: names, context: &context) + return .init(result: result, statistics: context.meter.statistics()) + } catch { throw JSONTransformFailure.classify(error) } + } + + private static func require(_ attempted: UInt64, _ limit: UInt64, _ metric: String, + meter: inout JSONWorkMeter) throws { + try meter.checkpoint() + guard attempted <= limit else { + throw JSONTransformDefinitionFailure.limitExceeded(metric: metric, limit: limit, attempted: attempted) + } + } + private static func scanned(_ text: String, maximum: UInt64, metric: String, + meter: inout JSONWorkMeter) throws -> UInt64 { + var count: UInt64 = 0 + for _ in text.utf8 { + try meter.work(); count = try meter.adding(.aggregateTextBytes, count, 1) + try require(count, maximum, metric, meter: &meter) + } + return count + } + private static func make(source: String, variables: [String], limits: JSONTransformDefinitionLimits, + meter: inout JSONWorkMeter) throws -> Self { + try meter.retain(64) + _ = try scanned(source, maximum: limits.maximumSourceBytes, metric: "sourceBytes", meter: &meter) + try JSONAdmission.text(source, meter: &meter, owned: true) + try require(UInt64(variables.count), limits.maximumVariableNames, "variableNames", meter: &meter) + var names: [String] = [], total: UInt64 = 0 + try meter.retain(32) + for name in variables { + let size = try scanned(name, maximum: limits.maximumNameBytes, metric: "nameBytes", meter: &meter) + total = try meter.adding(.aggregateTextBytes, total, size) + try require(total, limits.maximumTotalNameBytes, "totalNameBytes", meter: &meter) + var first = true + for byte in name.utf8 { + try meter.work() + let letter = byte == 95 || (65...90).contains(byte) || (97...122).contains(byte) + guard letter || (!first && (48...57).contains(byte)) else { throw JSONTransformDefinitionFailure.invalidVariableName } + first = false + } + guard !first else { throw JSONTransformDefinitionFailure.invalidVariableName } + try JSONAdmission.text(name, key: true, meter: &meter, owned: true) + try meter.retain(16); names.append(name) + } + var scratch: [String] = [] + for name in names { try meter.work(); try meter.retain(16); scratch.append(name) } + var width = 1 + while width < names.count { + var start = 0 + while start < names.count { + let middle = start + min(width, names.count - start) + let end = middle + min(width, names.count - middle) + var a = start, b = middle + for target in start.. names.count / 2 { break }; width *= 2 + } + if names.count > 1 { + for index in 1.. Bool { + var a = lhs.utf8.makeIterator(), b = rhs.utf8.makeIterator() + while true { + try meter.work(); let x = a.next(), y = b.next() + if x != y { if let x, let y { return x < y }; return x == nil } + if x == nil { return false } + } + } +} diff --git a/Sources/JSONTransform/JSONTransformSelection.swift b/Sources/JSONTransform/JSONTransformSelection.swift new file mode 100644 index 0000000..21a4958 --- /dev/null +++ b/Sources/JSONTransform/JSONTransformSelection.swift @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONFilterRuntime + +/// Finite policy for indexed-selection convention V1, additional to native limits. +public struct JSONTransformSelectionLimits: Sendable { + /// The shared admission, projection, VM, provider and output policy. + public var transform: JSONTransformLimits = .init() + /// Inclusive population count; default 65,536. Zero admits an empty population. + public var maximumPopulationValues: UInt64 = 65_536 + var checkpointObserver: (@Sendable (UInt64) -> Void)? + var projectionObserver: (@Sendable (UInt64) -> Void)? + /// Creates the finite balanced selection policy. + public init() {} +} + +/// A selection-boundary refusal, distinct from language and operational engine errors. +public enum JSONTransformSelectionFailure: Error, Sendable, Equatable { + /// An output was not an exact nonnegative UInt64 index in the population. + case invalidPosition + /// A position belongs to a different invocation, even with identical values. + case foreignPosition + /// The caller returned before pulling explicit evaluator EOF. + case incompleteSelection +} + +private final class SelectionIdentity { + let lease: TransformValueLease + init(_ lease: TransformValueLease) { self.lease = lease } +} +private final class SelectionPopulation { + let value: FilterYield + let identity: SelectionIdentity + let metadata: TransformValueLease + init(_ value: FilterYield, identity: SelectionIdentity, metadata: TransformValueLease) { + self.value = value; self.identity = identity; self.metadata = metadata + } +} + +/// An unforgeable invocation-local ordinal, retaining the population and its accounting. +/// Copies share leases. The handle is deliberately non-Sendable; extracted ordinals +/// and caller mappings are not engine provenance, stable IDs or read grants. +public struct JSONTransformSelectionPosition { + /// Exact zero-based original population index; order and repeats are preserved. + public let ordinal: UInt64 + private let population: SelectionPopulation + private let reservation: TransformValueLease + fileprivate init(_ ordinal: UInt64, population: SelectionPopulation, reservation: TransformValueLease) { + self.ordinal = ordinal; self.population = population; self.reservation = reservation + } + fileprivate var identity: SelectionIdentity { population.identity } +} + +/// An owner-produced EOF witness and successful accounting snapshot; no public initializer. +/// The witness retains only identity/report storage, not the population. Escaped +/// positions retain that population separately. Logical reservations are not RSS. +public struct JSONTransformSelectionCompletion { + /// Indexed-selection convention revision, independent of query and definition versions. + public var conventionRevision: UInt32 { 1 } + /// Completely admitted population count, including duplicate-valued members. + public let populationCount: UInt64 + /// Successfully published positions, including repeats. + public let outputCount: UInt64 + /// Successful cumulative native snapshot after closing VM/provider state. + public let statistics: JSONOperationStatistics + private let identity: SelectionIdentity + private let reservation: TransformValueLease + fileprivate init(identity: SelectionIdentity, reservation: TransformValueLease, + populationCount: UInt64, outputCount: UInt64, statistics: JSONOperationStatistics) { + self.identity = identity; self.reservation = reservation + self.populationCount = populationCount; self.outputCount = outputCount; self.statistics = statistics + } + /// Verifies invocation identity and extracts an ordinal after successful completion. + /// This constant metadata inspection is caller work, not another VM invocation. + /// - Parameter position: A position emitted by this invocation. + /// - Returns: Its exact original population index. + /// - Throws: ForeignPosition for a handle from another invocation. + public func ordinal(of position: JSONTransformSelectionPosition) throws -> UInt64 { + guard identity === position.identity else { throw JSONTransformSelectionFailure.foreignPosition } + return position.ordinal + } +} + +/// The caller result paired with an unforgeable successful selection completion. +public struct JSONTransformSelectionReport { + /// The value returned by the synchronous caller body. + public let result: Result + /// Successful EOF, invocation identity and actual native accounting. + public let completion: JSONTransformSelectionCompletion + fileprivate init(result: Result, completion: JSONTransformSelectionCompletion) { + self.result = result; self.completion = completion + } +} + +private final class SelectionHealth { + struct Observation { + let statistics: JSONOperationStatistics + let remaining: UInt64 + let counters: [UInt64] + } + var pulling = false + var failure: (any Error)? + var observation: Observation? + func latch(_ error: any Error) { + if failure == nil { + failure = error is JSONTransformSelectionFailure ? error : JSONTransformFailure.classify(error) + } + } + func check(context: inout TransformRuntimeContext) throws { + if let failure { context.latch(failure); throw failure } + } + func observe(context: inout TransformRuntimeContext) throws { + try context.work(66) + 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 SelectionInput: FilterInput { + var source: (() throws -> JSON?)? + let health: SelectionHealth + 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 { + health.latch(JSONTransformFailure(error is CancellationError ? .cancelled : .provider, + error is CancellationError ? "cancelled" : "providerFailed")) + try health.check(context: &context); throw error + } + try health.check(context: &context); try context.checkpoint() + guard let value else { ended = true; self.source = nil; return nil } + try context.charge(.totalRecords) + return try FilterValues.produced(value, context: &context) + } +} + +/// A guarded, non-Sendable indexed-selection pull scope with one cumulative ledger. +/// V1 input is [0,...,N-1]; `$values` is the complete original population. Additional +/// bindings are explicit. Provider values may influence queries but never enlarge +/// the population. Returning without pulling nil cannot certify a prefix. +public final class JSONTransformSelectionScope { + private var context: TransformRuntimeContext + private var vm: FilterVM? + private var population: SelectionPopulation? + private var input: SelectionInput + private let health: SelectionHealth + private var planBytes: UInt64 = 0, bindingBytes: UInt64 = 0, initialBindings: UInt64 = 0 + private var ended = false, closed = false + private var published: UInt64 = 0 + private let count: UInt64 + private var cleanupFailure: (any Error)? + private static var stateBytes: UInt64 { 2048 } + private init(context: consuming TransformRuntimeContext, count: UInt64, source: (() throws -> JSON?)?) { + self.context = consume context; self.count = count + let health = SelectionHealth(); self.health = health + input = .init(source: source, health: health) + } + static func make(plan: JSONTransformProgram, values: [JSON], bindings: [String: JSON], + limits: JSONTransformSelectionLimits, source: (() throws -> JSON?)?, + observing: ((JSONTransformSelectionScope) -> Void)? = nil) throws -> JSONTransformSelectionScope { + do { + guard limits.maximumPopulationValues < UInt64(Int.max) else { + throw JSONTransformFailure(.configuration, "invalidLimits") + } + var context = try TransformRuntimeContext(limits: limits.transform.runtime) + try context.domain(.core) + context.meter.checkpointObserver = limits.checkpointObserver + try context.meter.retain(stateBytes) + let scope = JSONTransformSelectionScope(context: consume context, count: UInt64(values.count), source: source) + observing?(scope) + do { + try scope.prepare(plan, values: values, bindings: bindings, maximum: limits.maximumPopulationValues, projectionObserver: limits.projectionObserver) + return scope + } catch { + scope.health.latch(error) + scope.closePreservingFailure() + throw scope.health.failure ?? error + } + } catch { + if error is JSONTransformSelectionFailure { throw error } + throw JSONTransformFailure.classify(error) + } + } + private func prepare(_ plan: JSONTransformProgram, values: [JSON], bindings: [String: JSON], maximum: UInt64, + projectionObserver: (@Sendable (UInt64) -> Void)?) throws { + try context.checkpoint() + guard count <= maximum else { + throw JSONTransformFailure(.resource, "limitExceeded", metric: "populationValues", limit: maximum, attemptedUsage: count) + } + guard plan.variables.contains("values"), bindings.count == plan.variables.count - 1 else { + throw JSONTransformFailure(.configuration, "invalidBindings") + } + try context.meter.retain(plan.planReservation); planBytes = plan.planReservation + let identity = SelectionIdentity(try makeLease(64)) + let admitted = try FilterValues.array(values, context: &context) + let metadata = try makeLease(48) + population = SelectionPopulation(admitted, identity: identity, metadata: metadata) + var supplied: [String: FilterYield] = [:] + try admitName("values") + supplied["values"] = admitted + for (name, value) in bindings { + try admitName(name) + guard name != "values", plan.variables.contains(name) else { throw JSONTransformFailure(.configuration, "invalidBindings") } + supplied[name] = try FilterValues.produced(value, context: &context) + } + var indices: [JSON] = [], scratch: UInt64 = 0, slots: UInt64 = 0 + defer { + do { try context.meter.release(scratch); try context.release(.collectionValues, slots) } + catch { recordCleanup(error) } + } + for ordinal in values.indices { + try context.work(20) + try context.charge(.collectionValues); slots += 1 + try context.meter.retain(64); scratch += 64 + indices.append(.number(JSONNumber(integer: ordinal))) + projectionObserver?(UInt64(indices.count)) + } + let root = try FilterValues.array(indices, admittedCollectionCount: true, context: &context) + slots = 0 // The root's lease owns these slots now. + indices = []; try context.meter.release(scratch); scratch = 0 + try context.charge(.totalRecords) + vm = try FilterVM(program: plan.program, input: root, bindings: supplied, context: &context) + } + private func makeLease(_ bytes: UInt64) throws -> TransformValueLease { + try context.meter.retain(bytes) + do { return try TransformValueLease(ownedRetained: bytes, context: &context) } + catch { try context.meter.release(bytes); throw error } + } + private func admitName(_ name: String) throws { + try context.charge(.bindings); initialBindings += 1 + 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, "invalidBindings") } + first = false + } + guard !first else { throw JSONTransformFailure(.configuration, "invalidBindings") } + let before = context.meter.used(.retainedBytes) + do { + try JSONAdmission.text(name, key: true, meter: &context.meter, owned: true) + try context.meter.retain(64) + bindingBytes += context.meter.used(.retainedBytes) - before + } catch { + try context.meter.release(context.meter.used(.retainedBytes) - before); throw error + } + } + /// Pulls the next valid original-population position, or nil at explicit EOF. + /// Invalid outputs, resource/provider failures, cancellation and reentry latch; + /// query catches and later caller catches cannot restore successful completion. + public func next() throws -> JSONTransformSelectionPosition? { + guard !health.pulling else { + health.latch(JSONTransformFailure(.configuration, "reentrantSelection")) + throw health.failure ?? JSONTransformFailure(.configuration, "reentrantSelection") + } + health.pulling = true; defer { health.pulling = false } + do { + try context.reclaimValueLeases(); try health.check(context: &context) + try context.checkpoint() + guard !closed else { throw JSONTransformFailure(.configuration, "scopeClosed") } + guard !ended, var active = vm else { return nil } + vm = nil; defer { vm = active } + guard let raw = try active.next(input: &input, context: &context) else { ended = true; return nil } + let before = context.meter.used(.retainedBytes) + do { try context.meter.resetValueBounds(); try JSONAdmission.value(raw.value, meter: &context.meter) } + catch { try context.meter.release(context.meter.used(.retainedBytes) - before); throw error } + try context.charge(.totalResults) + guard case .number(let number) = raw.value else { throw JSONTransformSelectionFailure.invalidPosition } + let ordinal: UInt64 + do { ordinal = try number.integer(as: UInt64.self, meter: &context.meter) } + catch let failure as JSONFailure where failure.category == .conversion { throw JSONTransformSelectionFailure.invalidPosition } + guard let native = Int(exactly: ordinal), native >= 0, ordinal < count, let population else { + throw JSONTransformSelectionFailure.invalidPosition + } + let lease = try makeLease(32) + published += 1 + return .init(ordinal, population: population, reservation: lease) + } catch { + health.latch(error); throw health.failure ?? error + } + } + /// Drains deferred lease refunds and returns actual native accounting, even after failure/close. + /// During a provider callback this returns its admitted entry snapshot without + /// borrowing the VM's context. It never reopens data operations or refunds work. + /// - Throws: An accounting cleanup failure; a prior operational cause takes precedence. + public func report() throws -> JSONOperationStatistics { + if health.pulling { + if let observation = health.observation { return observation.statistics } + health.latch(JSONTransformFailure(.configuration, "reentrantSelection")) + throw health.failure ?? JSONTransformFailure(.configuration, "reentrantSelection") + } + do { try context.reclaimValueLeases() } + catch { recordCleanup(error); throw health.failure ?? error } + if let cleanupFailure { throw health.failure ?? cleanupFailure } + return context.meter.statistics() + } + /// Remaining cumulative owned work; provider entry uses an admitted snapshot. + public var remainingWorkUnits: UInt64 { + if health.pulling { return health.observation?.remaining ?? 0 } + let cap = min(context.limits.core[.workUnits], context.limits[.totalWorkUnits]) + return cap - min(cap, context.used(.totalWorkUnits)) + } + /// Reads a cumulative/live transform counter; call report to settle deferred refunds. + /// - Parameter metric: The existing transform counter, without a public mutable meter. + public func used(_ metric: JSONTransformLimits.Metric) -> UInt64 { + if health.pulling { return health.observation?.counters[metric.runtime.index] ?? 0 } + return context.used(metric.runtime) + } + /// Latches cancellation without borrowing a context loaned to the provider. + public func cancel() { + guard !closed else { return } + health.latch(JSONTransformFailure(.cancelled, "cancelled")) + if !health.pulling, let failure = health.failure { context.latch(failure) } + } + fileprivate var firstFailure: (any Error)? { health.failure } + fileprivate func complete() throws -> JSONTransformSelectionCompletion { + do { + try context.checkpoint(); try health.check(context: &context) + if let cleanupFailure { throw cleanupFailure } + guard ended, let identity = population?.identity else { throw JSONTransformSelectionFailure.incompleteSelection } + try context.work(50) + let lease = try makeLease(1024) + try close() + return .init(identity: identity, reservation: lease, populationCount: count, + outputCount: published, statistics: try report()) + } catch { health.latch(error); throw health.failure ?? error } + } + private func recordCleanup(_ error: any Error) { + if cleanupFailure == nil { cleanupFailure = error } + health.latch(error) + } + fileprivate func closePreservingFailure() { + do { try close() } catch { recordCleanup(error) } + } + private func close() throws { + guard !closed else { return }; closed = true + input.source = nil + if var active = vm { vm = nil; try active.close(context: &context) } + population = nil + try context.reclaimValueLeases() + try context.meter.release(planBytes + bindingBytes + Self.stateBytes) + try context.release(.bindings, initialBindings) + planBytes = 0; bindingBytes = 0; initialBindings = 0 + } +} + +extension JSONTransformProgram { + /// Runs indexed-selection convention V1 on one fully admitted finite population. + /// Input is an owned index array; `$values` is the corresponding value array. + /// The plan must declare values, and bindings supply exactly the other names. + /// Provider work/bytes already owned by the caller stay outside engine quotas. + /// - Parameters: + /// - population: Explicit original order, including duplicate-valued members. + /// - bindings: Additional declared bindings; values is reserved and cannot be supplied here. + /// - limits: One finite ledger for admission, projection, evaluation and outputs. + /// - source: Optional explicit input/inputs provider; nil means no extra inputs. + /// - body: Synchronous caller work; pull nil to certify complete selection. + /// - Returns: Caller result and an owner-produced EOF/accounting witness. + /// - Throws: Selection refusals, typed operational failures or uncaught caller errors. + public func withSelection(population: [JSON], bindings: [String: JSON] = [:], + limits: JSONTransformSelectionLimits = .init(), source: (() throws -> JSON?)? = nil, + _ body: (JSONTransformSelectionScope) throws -> Result) throws -> JSONTransformSelectionReport { + let scope = try JSONTransformSelectionScope.make(plan: self, values: population, bindings: bindings, limits: limits, source: source) + let result: Result + do { result = try body(scope) } + catch { + let cause = scope.firstFailure ?? error + scope.closePreservingFailure(); throw cause + } + do { return .init(result: result, completion: try scope.complete()) } + catch { + let cause = scope.firstFailure ?? error + scope.closePreservingFailure(); throw cause + } + } +} diff --git a/Tests/JSONCLITests/IntegrationTests.swift b/Tests/JSONCLITests/IntegrationTests.swift index 5157667..12ac2a7 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")) + #expect(try metadata.requiredMember("version") == .string("1.8.0-dev")) #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) diff --git a/Tests/JSONInspectionTests/WorkflowTests.swift b/Tests/JSONInspectionTests/WorkflowTests.swift new file mode 100644 index 0000000..82bf917 --- /dev/null +++ b/Tests/JSONInspectionTests/WorkflowTests.swift @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +@testable import JSONInspection + +private enum WorkflowTestError: Error { case expectedFailure } +private func wfCore(_ body: () throws -> Void) throws -> JSONFailure { + do { try body(); Issue.record("Expected core failure"); throw WorkflowTestError.expectedFailure } + catch let error as JSONFailure { return error } +} +private func wfStore(_ body: () throws -> Void) throws -> JSONStoreFailure { + do { try body(); Issue.record("Expected store failure"); throw WorkflowTestError.expectedFailure } + catch let error as JSONStoreFailure { return error } +} +private func wfDocument(_ text: String) throws -> JSONDocument { + try JSONParser().parseDocument(Data(text.utf8)) +} +private func wfStoreFixture() throws -> JSONConfiguredStore { + let document = try wfDocument(#"{"collections":{"users":[{"id":"é","count":9007199254740993,"friend":{"collection":"users","id":"e\u0301"},"optional":null},{"id":"e\u0301","friend":{"collection":"users","id":"é"}}],"items":[{"id":"x","owner":{"collection":"users","id":"é"}}]}}"#) + let profile = JSONStoreProfile(version: .v1, collectionsAt: [.key("collections")], collections: [ + .init(name: "items", recordsAt: [.key("items")], idKey: "id", referenceFields: [.init(key: "owner", presence: .required, nullable: false)]), + .init(name: "users", recordsAt: [.key("users")], idKey: "id", referenceFields: [ + .init(key: "friend", presence: .required, nullable: false), .init(key: "optional", presence: .optional, nullable: true)])], + references: .strict(marker: .v1, validateTargets: true)) + return try JSONStoreBuilder(document: document, profile: profile).build().result +} +private func wfStar() throws -> JSONConfiguredStore { + let document = try wfDocument(#"{"u":[{"id":"0","left":{"collection":"u","id":"1"},"right":{"collection":"u","id":"2"}},{"id":"1"},{"id":"2"}]}"#) + return try JSONStoreBuilder(document: document, profile: .init(version: .v1, collectionsAt: [], collections: [ + .init(name: "u", recordsAt: [.key("u")], idKey: "id", referenceFields: [ + .init(key: "left", presence: .optional, nullable: true), .init(key: "right", presence: .optional, nullable: true)])], + references: .strict(marker: .v1, validateTargets: true))).build().result +} + +@Test func workflowB01PathsPreserveOrderRepeatsNullAndExactScalars() throws { + let document = try wfDocument(#"{"é":9007199254740993,"e\u0301":null,"a/b~":0.1}"#) + let report = try JSONInspectionBatch(document: document).values(at: [[.key("e\u{301}")], [.key("é")], [.key("e\u{301}")], [.key("a/b~")]]) + #expect(report.result == [.null, .number(try JSONNumber(parsing: "9007199254740993")), .null, .number(try JSONNumber(parsing: "0.1"))]) + #expect(report.statistics[.workUnits] > 0) +} +@Test func workflowB02OneCompleteAdmissionAndEmptyBatch() throws { + let document = try wfDocument("[1,2,3]") + let base = try JSONTraversal(document: document).visit { _,_ in .skipDescendants } + let batch = try JSONInspectionBatch(document: document).values(at: [[.index(1)], [.index(1)]]) + #expect(batch.statistics[.nodes] == base.statistics[.nodes]) + #expect(batch.statistics[.sourceMapEntries] == base.statistics[.sourceMapEntries]) + let empty = try JSONInspectionBatch(document: document).values(at: [], maximumRequests: 0) + #expect(empty.result.isEmpty && empty.statistics[.nodes] == base.statistics[.nodes]) + var limits = JSONLimits.balanced; limits[.nodes] = 0 + #expect(try wfCore { _ = try JSONInspectionBatch(document: document, limits: limits).values(at: []) }.resource?.metric == .nodes) +} +@Test func workflowB03RequestCapacityConfigurationAndExactBoundary() throws { + let batch = JSONInspectionBatch(document: .init(root: .null)) + #expect(try batch.values(at: [[],[]], maximumRequests: 2).result == [.null,.null]) + do { _ = try batch.values(at: [[],[]], maximumRequests: 1); Issue.record("Expected batch refusal") } + catch let error as JSONInspectionBatchFailure { #expect(error == .requestLimitExceeded(limit: 1, actual: 2)) } + for invalid in [-1, Int.max] { + #expect(try wfCore { _ = try batch.values(at: [], maximumRequests: invalid) }.code == .invalidLimits) + } +} +@Test func workflowB04MissingAndWrongKindRetainTypedPaths() throws { + let batch = JSONInspectionBatch(document: try wfDocument(#"{"a":null}"#)) + for (path, category, code) in [([JSONLocationComponent.key("missing")], JSONFailure.Category.missingValue, JSONFailure.Code.missingMember), + ([.key("a"),.index(0)], .typeMismatch, .wrongKind)] { + do { _ = try batch.values(at: [[],path]); Issue.record("Expected atomic access refusal") } + catch let error as JSONAccessFailure { + #expect(error.path == path && error.cause.category == category && error.cause.code == code) + #expect(error.cause.location != nil) + } + } +} +@Test func workflowB05UnselectedSuffixAndCumulativeWorkCannotReset() throws { + let document = try wfDocument(#"{"selected":null,"outside":[1,2,3,4]}"#) + var limits = JSONLimits.balanced; limits[.nodes] = 2 + #expect(try wfCore { _ = try JSONInspectionBatch(document: document, limits: limits).values(at: [[.key("selected")]]) }.resource?.metric == .nodes) + let paths = Array(repeating: [JSONLocationComponent.key("selected")], count: 8) + let measured = try JSONInspectionBatch(document: document).values(at: paths) + limits = .balanced; limits[.workUnits] = measured.statistics[.workUnits] + #expect(try JSONInspectionBatch(document: document, limits: limits).values(at: paths).result.count == 8) + limits[.workUnits] -= 1 + let failure = try wfCore { _ = try JSONInspectionBatch(document: document, limits: limits).values(at: paths) } + #expect(failure.resource?.metric == .workUnits && failure.resource?.limit == limits[.workUnits]) +} +@Test func workflowB06SnapshotBatchKeepsOneSourceAndOrdinalMeaning() throws { + let snapshot = try JSONStructuralSnapshotBuilder(document: wfDocument("[9,8,null]")).build().result + let batch = try snapshot.values(for: [2,1,3,2]) + #expect(batch.result == [8,9,.null,8]) + let one = try snapshot.value(for: 2) + #expect(batch.statistics[.nodes] == one.statistics[.nodes]) + var limits = JSONLimits.balanced; limits[.nodes] = 0 + do { _ = try snapshot.values(for: [1,99], limits: limits); Issue.record("Expected bad ordinal") } + catch let error as JSONStructuralSnapshotFailure { #expect(error == .ordinalOutOfBounds(99)) } +} +@Test func workflowB07InnerCancellationInvalidatesOriginalScope() async throws { + let document = try wfDocument("[1,2,3]") + let lifetime = Mutex(false), outputs = Mutex(0) + let task = Task { + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + var batch = JSONInspectionBatch(document: document, limits: limits) + batch.lifetimeObserver = { root, state in lifetime.withLock { $0 = root && state } } + batch.outputObserver = { count in + outputs.withLock { $0 = count } + if count == 2 { withUnsafeCurrentTask { $0?.cancel() } } + } + return try batch.values(at: Array(repeating: [.index(0)], count: 20)) + } + do { _ = try await task.value; Issue.record("Expected cancellation") } catch is CancellationError {} + #expect(lifetime.withLock { $0 }) + #expect(outputs.withLock { $0 } == 2) +} +@Test func workflowB08RetentionBoundaryIsFiniteAndSourceIsUnchanged() throws { + let document = try wfDocument("[1,2]") + let report = try JSONInspectionBatch(document: document).values(at: [[],[.index(1)]]) + var limits = JSONLimits.balanced; limits[.retainedBytes] = report.statistics.peakLogicalRetainedBytes + _ = try JSONInspectionBatch(document: document, limits: limits).values(at: [[],[.index(1)]]) + limits[.retainedBytes] -= 1 + #expect(try wfCore { _ = try JSONInspectionBatch(document: document, limits: limits).values(at: [[],[.index(1)]]) }.resource?.metric == .retainedBytes) + #expect(document.root == [1,2] && document.sourceMap != nil) +} +@Test func workflowS01PublicStorePreservesPhysicalAndDeclaredOrder() throws { + let store = try wfStoreFixture() + #expect(store.collectionCount == 2 && store.recordCount == 3) + #expect(try store.collectionName(at: 0) == "users") + #expect(try store.collectionName(at: 1) == "items") + #expect(try store.recordCount(in: 0) == 2 && store.references.count == 5) + #expect(try store.record(collection: 0, row: 0).idPath == [.key("collections"),.key("users"),.index(0),.key("id")]) +} +@Test func workflowS02LookupUsesDecodedScalarIDsAndDistinguishesMissing() throws { + let store = try wfStoreFixture() + let composed = try #require(store.lookup(collection: "users", id: "é").result) + let decomposed = try #require(store.lookup(collection: "users", id: "e\u{301}").result) + #expect(composed.row == 0 && decomposed.row == 1 && composed != decomposed) + #expect(try store.lookup(collection: "users", id: "absent").result == nil) + let error = try wfStore { _ = try store.lookup(collection: "unknown", id: "é") } + #expect(error.code == .unknownCollection && error.domain == .operation && error.phase == 9) +} +@Test func workflowS03OwnerTokensCannotRebindAnotherInstance() throws { + let store = try wfStoreFixture(), other = try JSONStoreBuilder(document: store.document, profile: store.profile).build().result + let position = try store.record(collection: 0, row: 0).position + #expect(position != (try other.record(collection: 0, row: 0).position)) + #expect(try wfStore { _ = try other.values(at: [position]) }.code == .invalidPosition) + #expect(try wfStore { _ = try other.follow(from: position) }.code == .invalidPosition) +} +@Test func workflowS04StoreBatchValuesPreserveOrderExactnessAndOneAdmission() throws { + let store = try wfStoreFixture(), a = try store.record(collection: 0, row: 0).position, b = try store.record(collection: 0, row: 1).position + let report = try store.values(at: [b,a,b]) + #expect(try report.result[0].requiredMember("id") == .string("e\u{301}")) + #expect(try report.result[1].requiredMember("count") == .number(JSONNumber(parsing: "9007199254740993"))) + #expect(report.result[0] == report.result[2]) + let source = try JSONTraversal(document: store.document).visit { _,_ in .skipDescendants } + #expect(report.statistics[.nodes] == source.statistics[.nodes]) + #expect(try store.value(at: a).result == report.result[1]) + #expect(try store.values(at: [], maximumRequests: 0).statistics[.nodes] == source.statistics[.nodes]) +} +@Test func workflowS05ReferenceSlotsKeepAbsentNullAndLinkDistinct() throws { + let store = try wfStoreFixture() + if case .link(let p) = store.references[0].state { #expect(p.row == 1) } else { Issue.record("Expected link") } + if case .null = store.references[1].state {} else { Issue.record("Expected null") } + if case .absent = store.references[3].state {} else { Issue.record("Expected absence") } +} +@Test func workflowS06FollowCompletesCyclesAndPreservesBFSOrder() throws { + let store = try wfStoreFixture(), start = try store.record(collection: 1, row: 0).position + let report = try store.follow(from: start) + #expect(report.result.map(\.collection) == [1,0,0] && report.result.map(\.row) == [0,0,1]) + let star = try wfStar(), root = try star.record(collection: 0, row: 0).position + #expect(try star.follow(from: root).result.map(\.row) == [0,1,2]) +} +@Test func workflowS07FollowAllFourInclusiveBoundsThrowInsteadOfTruncate() throws { + let store = try wfStar(), start = try store.record(collection: 0, row: 0).position + let cases: [(String, WritableKeyPath, Int)] = [ + ("maximumDepth",\JSONStoreFollowLimits.maximumDepth,1), ("maximumFrontier",\JSONStoreFollowLimits.maximumFrontier,2), + ("maximumVisited",\JSONStoreFollowLimits.maximumVisited,3), ("maximumResults",\JSONStoreFollowLimits.maximumResults,3)] + for (name,key,needed) in cases { + var bounds = JSONStoreFollowLimits(); bounds[keyPath: key] = needed + #expect(try store.follow(from: start, bounds: bounds).result.count == 3) + bounds[keyPath: key] = needed - 1 + let failure = try wfStore { _ = try store.follow(from: start, bounds: bounds) } + #expect(failure.code == .capacityExceeded && failure.capacity == name && failure.limit == needed - 1) + for invalid in [-1, Int.max] { + bounds[keyPath: key] = invalid + #expect(try wfCore { _ = try store.follow(from: start, bounds: bounds) }.code == .invalidLimits) + } + } +} +@Test func workflowS08FollowAndLookupHaveCumulativeQuotaAndNoSecondAdmission() throws { + let store = try wfStar(), start = try store.record(collection: 0, row: 0).position + let report = try store.follow(from: start) + let lookup = try store.lookup(collection: "u", id: "2") + #expect(report.statistics[.nodes] == lookup.statistics[.nodes]) + for work in [report.statistics[.workUnits], lookup.statistics[.workUnits]] { #expect(work > 1) } + var limits = JSONLimits.balanced; limits[.workUnits] = report.statistics[.workUnits] + #expect(try store.follow(from: start, limits: limits).result.count == 3) + limits[.workUnits] -= 1 + #expect(try wfCore { _ = try store.follow(from: start, limits: limits) }.resource?.metric == .workUnits) +} +@Test func workflowS09FollowInnerCancellationAndInvalidation() async throws { + let store = try wfStar(), start = try store.record(collection: 0, row: 0).position + let lifetime = Mutex(false), edges = Mutex(0) + let task = Task { + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + return try store.followForQualification(from: start, bounds: .init(), limits: limits, + checkpointObserver: { _ in }, lifetimeObserver: { root, state in lifetime.withLock { $0 = root && state } }, + edgeObserver: { count in + edges.withLock { $0 = count } + if count == 1 { withUnsafeCurrentTask { $0?.cancel() } } + }) + } + do { _ = try await task.value; Issue.record("Expected inner cancellation") } catch is CancellationError {} + #expect(edges.withLock { $0 } == 1 && lifetime.withLock { $0 }) + #expect(try store.follow(from: start).result.count == 3) +} +@Test func workflowS10SourceReplacementCannotInventSpansOrRebindPositions() throws { + let store = try wfStoreFixture(), position = try store.record(collection: 0, row: 0).position + let copy = try JSONStoreBuilder(document: store.document.replacingRoot(store.document.root), profile: store.profile).build().result + #expect(store.document.sourceMap != nil && copy.document.sourceMap == nil) + #expect(try wfStore { _ = try copy.value(at: position) }.code == .invalidPosition) + let original = try store.record(at: position) + #expect(try store.document.sourceMap?.span(at: original.path) != nil) +} +@Test func workflowS11DisabledReferencesRemainOrdinaryData() throws { + let document = try wfDocument(#"{"u":[{"id":"a","arbitrary":{"collection":"missing","id":"not-a-link"}}]}"#) + let store = try JSONStoreBuilder(document: document, profile: .init(version: .v1, collectionsAt: [], collections: [ + .init(name: "u", recordsAt: [.key("u")], idKey: "id", referenceFields: [])], references: .disabled)).build().result + let start = try store.record(collection: 0, row: 0).position + var bounds = JSONStoreFollowLimits(); bounds.maximumDepth = 0 + #expect(try store.follow(from: start, bounds: bounds).result == [start] && store.references.isEmpty) +} +@Test func workflowSharedImmutablePlansHaveIndependentBatchAndStoreHealth() async throws { + let store = try wfStar(), start = try store.record(collection: 0, row: 0).position + let batch = JSONInspectionBatch(document: store.document) + try await withThrowingTaskGroup(of: Bool.self) { group in + for i in 0..<8 { group.addTask { + if i % 2 == 0 { + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + do { _ = try store.follow(from: start, limits: limits); return false } + catch let error as JSONFailure { return error.resource?.metric == .workUnits } + } + let read = try batch.values(at: [[]]) + return try store.follow(from: start).result.count == 3 && read.result == [store.document.root] + } } + for try await ok in group { #expect(ok) } + } +} + +@Test func workflowS12StorePathsHonorInvocationPointerAndDiagnosticLimits() throws { + let store = try wfStar(), position = try store.record(collection: 0, row: 0).position + for (metric, boundary) in [(JSONResourceMetric.pointerTokens, UInt64(2)), (.pointerBytes, 4), (.diagnosticBytes, 4)] { + var limits = JSONLimits.balanced; limits[metric] = boundary + #expect(try store.value(at: position, limits: limits).result == store.value(at: position).result) + limits[metric] = boundary - 1 + #expect(try wfCore { _ = try store.value(at: position, limits: limits) }.resource?.metric == metric) + } +} +@Test func workflowS13FollowSelfCycleAndLongNarrowChain() throws { + let profile = JSONStoreProfile(version: .v1, collectionsAt: [], collections: [ + .init(name: "u", recordsAt: [.key("u")], idKey: "id", referenceFields: [.init(key: "next", presence: .required, nullable: false)])], + references: .strict(marker: .v1, validateTargets: true)) + func store(_ text: String) throws -> JSONConfiguredStore { try JSONStoreBuilder(document: wfDocument(text), profile: profile).build().result } + let single = try store(#"{"u":[{"id":"0","next":{"collection":"u","id":"0"}}]}"#) + let start = try single.record(collection: 0, row: 0).position + var bounds = JSONStoreFollowLimits(); bounds.maximumDepth = 0; bounds.maximumFrontier = 1 + #expect(try single.follow(from: start, bounds: bounds).result == [start]) + let chain = try store(#"{"u":[{"id":"0","next":{"collection":"u","id":"1"}},{"id":"1","next":{"collection":"u","id":"2"}},{"id":"2","next":{"collection":"u","id":"3"}},{"id":"3","next":{"collection":"u","id":"3"}}]}"#) + let first = try chain.record(collection: 0, row: 0).position + bounds.maximumDepth = 3 + #expect(try chain.follow(from: first, bounds: bounds).result.map(\.row) == [0,1,2,3]) + bounds.maximumDepth = 2 + let failure = try wfStore { _ = try chain.follow(from: first, bounds: bounds) } + #expect(failure.code == .capacityExceeded && failure.capacity == "maximumDepth" && failure.limit == 2) +} + +@Test func workflowB09NestedSnapshotRepeatsQuotasAndLateCancellation() async throws { + let document = try wfDocument(#"{"a/b~":{"":{"é":[1,2],"e\u0301":null}}}"#) + let snapshot = try JSONStructuralSnapshotBuilder(document: document).build().result + // DFS: root0, a/b~1, empty2, é3, 1 at4, 2 at5, decomposed null6. + let ordinals = [5,4,5,6], report = try snapshot.values(for: [5,4,5,6]) + #expect(report.result == [2,1,2,.null] && report.statistics[.retainedBytes] == 160) + var limits = JSONLimits.balanced; limits[.workUnits] = report.statistics[.workUnits] + #expect(try snapshot.values(for: ordinals, limits: limits).result == report.result) + limits[.workUnits] -= 1 + #expect(try wfCore { _ = try snapshot.values(for: ordinals, limits: limits) }.resource?.metric == .workUnits) + let outputs = Mutex(0), lifetime = Mutex(false) + let task = Task { + var inner = JSONLimits.balanced; inner[.cancellationQuantum] = 1 + return try snapshot.valuesForQualification(for: ordinals, limits: inner, outputObserver: { count in + outputs.withLock { $0 = count }; if count == 2 { withUnsafeCurrentTask { $0?.cancel() } } + }, lifetimeObserver: { root, state in lifetime.withLock { $0 = root && state } }) + } + do { _ = try await task.value; Issue.record("Expected late cancellation") } catch is CancellationError {} + #expect(outputs.withLock { $0 } == 2 && lifetime.withLock { $0 }) + #expect(try snapshot.values(for: [5]).result == [2]) +} +@Test func workflowS14MetadataAliasesAndScratchHaveExactRetentionDisposition() throws { + let store = try wfStar(), start = try store.record(collection: 0, row: 0).position + let lookup = try store.lookup(collection: "u", id: "0") + #expect(lookup.statistics[.retainedBytes] == 32) + let follow = try store.follow(from: start) + #expect(follow.statistics[.retainedBytes] == 128) + let values = try store.values(at: [start,start]) + #expect(values.statistics[.retainedBytes] == 96) + #expect(follow.statistics.peakLogicalRetainedBytes >= store.overlayLogicalBytes) + var limits = JSONLimits.balanced; limits[.retainedBytes] = follow.statistics.peakLogicalRetainedBytes + #expect(try store.follow(from: start, limits: limits).result == follow.result) + limits[.retainedBytes] -= 1 + #expect(try wfCore { _ = try store.follow(from: start, limits: limits) }.resource?.metric == .retainedBytes) +} diff --git a/Tests/JSONIntegrationTests/CompositionContractTests.swift b/Tests/JSONIntegrationTests/CompositionContractTests.swift index c650339..caa665b 100644 --- a/Tests/JSONIntegrationTests/CompositionContractTests.swift +++ b/Tests/JSONIntegrationTests/CompositionContractTests.swift @@ -249,7 +249,9 @@ private func composedImport(limits: JSONLimits = .balanced, extra: UInt64 = 0) t _ = try JSON.withComposition(limits: limits) { scope in try scope.withMeter { meter in meter.checkpointObserver = { work in - if work >= (stage == 5 ? 4200 : 32) { withUnsafeCurrentTask { $0?.cancel() } } + // Test-only current-task handle stays inside its standard-library callback. + // Only this invocation is cancelled; no unsafe handle escapes or is shared. + if work >= (stage == 5 ? 4200 : 32) { unsafe withUnsafeCurrentTask { unsafe $0?.cancel() } } } } do { diff --git a/Tests/JSONIntegrationTests/PublicWorkflowTests.swift b/Tests/JSONIntegrationTests/PublicWorkflowTests.swift index b6aa799..0fef152 100644 --- a/Tests/JSONIntegrationTests/PublicWorkflowTests.swift +++ b/Tests/JSONIntegrationTests/PublicWorkflowTests.swift @@ -28,7 +28,9 @@ private actor Output { calls += 1 if calls == rejectAt { throw AppError.sink } bytes.append(contentsOf: chunk.bytes) - if calls == cancelAt { withUnsafeCurrentTask { $0?.cancel() } } + // Test-only current-task handle stays inside its standard-library callback. + // Only this invocation is cancelled; no unsafe handle escapes or is shared. + if calls == cancelAt { unsafe withUnsafeCurrentTask { unsafe $0?.cancel() } } } func snapshot() -> ([UInt8], Int) { (bytes, calls) } } diff --git a/Tests/JSONIntegrationTests/StagedImportTests.swift b/Tests/JSONIntegrationTests/StagedImportTests.swift index ed05876..739cce2 100644 --- a/Tests/JSONIntegrationTests/StagedImportTests.swift +++ b/Tests/JSONIntegrationTests/StagedImportTests.swift @@ -13,7 +13,9 @@ private actor ImportSource { func next() throws -> JSONByteChunk? { pulls += 1 if pulls == failAt { throw ImportError.source } - if pulls == cancelAt { withUnsafeCurrentTask { $0?.cancel() } } + // Test-only current-task handle stays inside its standard-library callback. + // Only this invocation is cancelled; no unsafe handle escapes or is shared. + if pulls == cancelAt { unsafe withUnsafeCurrentTask { unsafe $0?.cancel() } } return pulls <= texts.count ? .init(Array(texts[pulls - 1].utf8)) : nil } func count() -> Int { pulls } diff --git a/Tests/JSONIntegrationTests/StructuralAcceptanceTests.swift b/Tests/JSONIntegrationTests/StructuralAcceptanceTests.swift new file mode 100644 index 0000000..6720187 --- /dev/null +++ b/Tests/JSONIntegrationTests/StructuralAcceptanceTests.swift @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONSchema +import JSONPatch +import JSONStreaming +import JSONTransform +import JSONInspection + +private let acceptance18Input = #"{"u":[{"id":"é","amount":9007199254740993,"next":{"collection":"u","id":"e\u0301"}},{"id":"e\u0301","amount":0.10000000000000001,"next":null}]}"# +private func acceptance18Source() throws -> JSONDocument { + try JSONParser().parseDocument(Data(acceptance18Input.utf8)) +} +private func acceptance18Profile() -> JSONStoreProfile { + .init(version: .v1, collectionsAt: [], collections: [ + .init(name: "u", recordsAt: [.key("u")], idKey: "id", referenceFields: [ + .init(key: "next", presence: .required, nullable: true)]) + ], references: .strict(marker: .v1, validateTargets: true)) +} +private func acceptance18Plan() throws -> CompiledJSONSchema { + try JSONSchemaCompiler().compile(JSONSchemaDocument(JSONParser().parse( + #"{"type":"object","required":["u"],"properties":{"u":{"type":"array","minItems":2,"items":{"type":"object","required":["id","amount","next"],"properties":{"id":{"type":"string"},"amount":{"type":"number"},"next":{"type":["object","null"]}}}}}}"#))) +} +private func acceptance18Store(_ source: JSONDocument) throws -> JSONConfiguredStore { + try JSONStoreBuilder(document: source, profile: acceptance18Profile()).build().result +} +private enum Acceptance18Error: Error { case expectedFailure, removed } + +@Test func acceptance18A01SchemaStoreTransformKeepsExactGoldenBytes() throws { + let source = try acceptance18Source(), schema = try acceptance18Plan() + let verdict = try JSONSchemaValidator().validateWithReport(source.root, against: schema) + #expect(verdict.result.isValid && verdict.statistics[.evaluationWorkUnits] > 0) + let store = try acceptance18Store(source) + let start = try #require(store.lookup(collection: "u", id: "é").result) + let closure = try store.follow(from: start), records = try store.values(at: closure.result) + let output = try JSONTransformProgram.compile(".[] | .amount").withExecution(input: .array(records.result)) { execution in + // This array is authored caller storage, not owned transform output accounting. + var values: [JSON] = [] + while let item = try execution.next() { values.append(item.value) } + return JSON.array(values) + } + #expect(try JSONWriter(options: .init(numberStyle: .preserved)).write(output.result) == Data("[9007199254740993,0.10000000000000001]".utf8)) + #expect(records.statistics[.nodes] == (try JSONInspectionBatch(document: source).values(at: []).statistics[.nodes])) + #expect(closure.result.count == 2 && output.statistics[.workUnits] > 0) +} + +@Test func acceptance18A02InvalidSchemaVerdictIsDistinctFromOperationalRefusal() throws { + let plan = try acceptance18Plan() + let invalid = try JSONSchemaValidator().validate(42, against: plan) + #expect(!invalid.isValid && !invalid.issues.isEmpty) + var limits = JSONLimits.balanced; limits[.evaluationWorkUnits] = 0 + do { + _ = try JSONSchemaValidator(options: .init(limits: limits)).validate(acceptance18Source().root, against: plan) + Issue.record("An exhausted evaluator returned a verdict") + } catch let error as JSONFailure { + #expect(error.category == .resource && error.resource?.metric == .evaluationWorkUnits) + } + #expect(try JSONSchemaValidator().validate(acceptance18Source().root, against: plan).isValid) +} + +@Test func acceptance18A03PatchedRebuildHasNewOwnerAndNoInventedSourceMap() throws { + let source = try acceptance18Source(), original = try acceptance18Store(source) + let position = try #require(original.lookup(collection: "u", id: "e\u{301}").result) + let amount = try JSONNumber(parsing: "1.25") + let patch = try JSONPatch(operations: [.replace(path: JSONPointer(parsing: "/u/1/amount"), value: .number(amount))]) + guard case .document(let root) = try patch.applying(to: source.root) else { throw Acceptance18Error.removed } + let changed = source.replacingRoot(root), rebuilt = try acceptance18Store(changed) + #expect(changed.sourceMap == nil && changed.sourceBytes == nil && original.document.sourceMap != nil) + do { _ = try rebuilt.values(at: [position]); Issue.record("Rebound an original position") } + catch let error as JSONStoreFailure { #expect(error.code == .invalidPosition) } + let replacement = try #require(rebuilt.lookup(collection: "u", id: "e\u{301}").result) + #expect(try rebuilt.value(at: replacement).result.requiredMember("amount") == .number(amount)) + #expect(try original.value(at: position).result.requiredMember("amount") == .number(JSONNumber(parsing: "0.10000000000000001"))) +} + +@Test func acceptance18A04DanglingPatchedReferenceCannotPublishStore() throws { + let source = try acceptance18Source(), original = try acceptance18Store(source) + let patch = try JSONPatch(operations: [.replace(path: JSONPointer(parsing: "/u/1/id"), value: "renamed")]) + guard case .document(let root) = try patch.applying(to: source.root) else { throw Acceptance18Error.removed } + do { _ = try acceptance18Store(source.replacingRoot(root)); Issue.record("Published a dangling reference") } + catch let error as JSONStoreFailure { + #expect(error.code == .referenceDanglingID && error.phase == 7) + #expect(error.path == [.key("u"),.index(0),.key("next"),.key("id")]) + #expect(error.sourceSpan == nil) + } + let first = try #require(original.lookup(collection: "u", id: "é").result) + #expect(try original.follow(from: first).result.count == 2 && source.root == original.document.root) +} + +@Test func acceptance18A05ScalarCollectionNamesSurvivePatchAndCrossLinks() throws { + let source = try JSONParser().parseDocument(Data(#"{"é":[{"id":"a","v":1,"go":{"collection":"e\u0301","id":"a"}}],"e\u0301":[{"id":"a","v":2,"go":{"collection":"é","id":"a"}}]}"#.utf8)) + let profile = JSONStoreProfile(version: .v1, collectionsAt: [], collections: ["e\u{301}", "é"].map { name in + .init(name: JSONKey(name), recordsAt: [.key(JSONKey(name))], idKey: "id", referenceFields: [.init(key: "go", presence: .required, nullable: false)]) + }, references: .strict(marker: .v1, validateTargets: true)) + let store = try JSONStoreBuilder(document: source, profile: profile).build().result + let a = try #require(store.lookup(collection: "é", id: "a").result) + let b = try #require(store.lookup(collection: JSONKey("e\u{301}"), id: "a").result) + #expect(a != b && a.collection == 0 && b.collection == 1) + #expect(try store.follow(from: a).result == [a,b]) + let patch = try JSONPatch(operations: [.replace(path: JSONPointer(parsing: "/é/0/v"), value: 3)]) + guard case .document(let root) = try patch.applying(to: source.root) else { throw Acceptance18Error.removed } + let selected = try JSONInspectionBatch(document: source.replacingRoot(root)).values(at: [ + [.key("é"),.index(0),.key("v")], [.key(JSONKey("e\u{301}")),.index(0),.key("v")]]) + #expect(selected.result == [3,2]) +} + +@Test func acceptance18A06TransformFailureStaysTerminalOverStoreInput() throws { + let store = try acceptance18Store(acceptance18Source()) + let positions = [try store.record(collection: 0, row: 0).position, try store.record(collection: 0, row: 1).position] + let values = try store.values(at: positions), plan = try JSONTransformProgram.compile(".[] | .amount") + var limits = JSONTransformLimits(); limits[.results] = 1 + var reachedFailure = false + do { + _ = try plan.withExecution(input: .array(values.result), limits: limits) { execution in + #expect(try execution.next()?.value == .number(JSONNumber(parsing: "9007199254740993"))) + let firstWork = execution.statistics[.workUnits], remaining = execution.remainingWorkUnits + for _ in 0..<2 { + do { _ = try execution.next(); Issue.record("A failed execution resumed") } + catch let error as JSONTransformFailure { + #expect(error.category == .resource && error.metric == "results" && error.limit == 1) + reachedFailure = true + } + } + #expect(execution.statistics[.workUnits] >= firstWork && execution.remainingWorkUnits <= remaining) + } + Issue.record("Caught failure allowed successful completion") + } catch let error as JSONTransformFailure { #expect(error.category == .resource && error.metric == "results") } + #expect(reachedFailure) + #expect(try store.values(at: positions).result == values.result) +} + +@Test func acceptance18A07RootRemovalIsNotNullOrAnEmptyStore() throws { + let source = try acceptance18Source(), pointer = try JSONPointer(parsing: "") + let removed = try JSONPatch(operations: [.remove(path: pointer)]).applying(to: source.root) + let null = try JSONPatch(operations: [.replace(path: pointer, value: .null)]).applying(to: source.root) + #expect(removed == .removed && null == .document(.null) && removed != null) + guard case .document(let root) = null else { throw Acceptance18Error.removed } + do { _ = try acceptance18Store(source.replacingRoot(root)); Issue.record("Null became an empty store") } + catch let error as JSONStoreFailure { #expect(error.code == .collectionsNotObject) } +} + +private actor Acceptance18Sink { + var bytes: [UInt8] = [], calls = 0 + private var accepted: CheckedContinuation? + private var handoff: CheckedContinuation? + func accept(_ chunk: JSONByteChunk) async { + calls += 1; bytes.append(contentsOf: chunk.bytes) + await withCheckedContinuation { continuation in + handoff = continuation + accepted?.resume(); accepted = nil + } + } + func waitForAcceptance() async { + if handoff != nil { return } + await withCheckedContinuation { accepted = $0 } + } + func completeAcceptance() { + handoff?.resume(); handoff = nil + } + func snapshot() -> ([UInt8], Int) { (bytes, calls) } +} +@Test func acceptance18A08CancelledWriterKeepsOnlyItsAcceptedPrefix() async throws { + let store = try acceptance18Store(acceptance18Source()), sink = Acceptance18Sink() + let position = try store.record(collection: 0, row: 0).position + let values = try store.values(at: [position]), value = JSON.array(values.result) + let golden = try JSONWriter().write(value) + let task = Task { try await JSONAsyncWriter(chunkSize: 4).write(value) { chunk in await sink.accept(chunk) } } + await sink.waitForAcceptance() + task.cancel() + await sink.completeAcceptance() + do { try await task.value; Issue.record("Expected deterministic cancellation") } catch is CancellationError {} + let accepted = await sink.snapshot() + #expect(accepted.1 == 1 && accepted.0 == Array(golden.prefix(4))) + #expect(try store.values(at: [position]).result == values.result) +} + +@Test func acceptance18SharedPlansKeepIndependentCrossProductHealth() async throws { + let source = try acceptance18Source(), schema = try acceptance18Plan(), store = try acceptance18Store(source) + let transform = try JSONTransformProgram.compile(".[] | .amount"), position = try store.record(collection: 0, row: 0).position + let transformInput = JSON.array(try store.values(at: [position]).result) + try await withThrowingTaskGroup(of: Bool.self) { group in + for index in 0..<12 { group.addTask { + switch index % 4 { + case 0: + guard try JSONSchemaValidator().validate(source.root, against: schema).isValid else { return false } + let values = try store.values(at: [position]) + return try transform.withExecution(input: .array(values.result)) { execution in + let value = try execution.next()?.value + return try value == .number(JSONNumber(parsing: "9007199254740993")) && execution.next() == nil + }.result + case 1: + var limits = JSONLimits.balanced; limits[.evaluationWorkUnits] = 0 + do { _ = try JSONSchemaValidator(options: .init(limits: limits)).validate(source.root, against: schema); return false } + catch let error as JSONFailure { return error.resource?.metric == .evaluationWorkUnits } + case 2: + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + do { _ = try store.values(at: [position], limits: limits); return false } + catch let error as JSONFailure { return error.resource?.metric == .workUnits } + default: + var limits = JSONTransformLimits(); limits[.results] = 0 + do { _ = try transform.withExecution(input: transformInput, limits: limits) { try $0.next() }; return false } + catch let error as JSONTransformFailure { return error.metric == "results" } + } + } } + for try await success in group { #expect(success) } + } + #expect(try JSONSchemaValidator().validate(source.root, against: schema).isValid) +} diff --git a/Tests/JSONTransformTests/DefinitionTests.swift b/Tests/JSONTransformTests/DefinitionTests.swift new file mode 100644 index 0000000..8d19dc6 --- /dev/null +++ b/Tests/JSONTransformTests/DefinitionTests.swift @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import Foundation +import Synchronization +import JSON +@testable import JSONTransform + +private enum DefinitionTestError: Error { case expectedFailure } +private func definitionFailure(_ body: () throws -> Void) throws -> JSONTransformDefinitionFailure { + do { try body(); Issue.record("Expected definition failure"); throw DefinitionTestError.expectedFailure } + catch let error as JSONTransformDefinitionFailure { return error } +} +private func definitionResource(_ body: () throws -> Void) throws -> JSONFailure { + do { try body(); Issue.record("Expected native failure"); throw DefinitionTestError.expectedFailure } + catch let error as JSONFailure { #expect(error.category == .resource); return error } +} +private let dotDefinition = #"{"definition":"swift-json-transform-source","version":1,"dialect":"swift-json-query-v1","source":".","variables":[]}"# + +@Test func bindingD01CanonicalGoldenBytesAndReload() throws { + let definition = try JSONTransformProgramDefinition.prepare(source: "$z + $a", variables: ["z", "a"]) + let expected = #"{"definition":"swift-json-transform-source","version":1,"dialect":"swift-json-query-v1","source":"$z + $a","variables":["a","z"]}"# + let bytes = try definition.result.encode() + #expect(bytes.result == Array(expected.utf8)) + let decoded = try JSONTransformProgramDefinition.decode(bytes.result) + #expect(decoded.result.variableNames == ["a", "z"]) + #expect(try decoded.result.encode().result == bytes.result) + #expect(decoded.result.definitionRevision == 1 && decoded.result.dialectRevision == "swift-json-query-v1") + #expect(decoded.statistics[.workUnits] > definition.statistics[.workUnits]) +} + +@Test func bindingD02CanonicalEscapingAndExactScalarSource() throws { + let source = "é e\u{301}\n\t\"\\/" + let expected = #"{"definition":"swift-json-transform-source","version":1,"dialect":"swift-json-query-v1","source":"é é\u000a\u0009\"\\/","variables":[]}"# + let a = try JSONTransformProgramDefinition.prepare(source: source).result + #expect(try a.encode().result == Array(expected.utf8)) + let reload = try JSONTransformProgramDefinition.decode(Array(expected.utf8)).result + #expect(Array(reload.source.utf8) == Array(source.utf8)) + let composed = try JSONTransformProgramDefinition.prepare(source: "é").result + let decomposed = try JSONTransformProgramDefinition.prepare(source: "e\u{301}").result + #expect(try composed.encode().result != decomposed.encode().result) +} + +@Test func bindingD03StrictSemanticDecodeCanonicalizesPresentation() throws { + let input = #" { "variables":[], "source":"\u002e", "dialect":"swift-json-query-v1", "version":1.0, "definition":"swift-json-transform-source" } "# + let value = try JSONTransformProgramDefinition.decode(Array(input.utf8)).result + #expect(try value.encode().result == Array(dotDefinition.utf8)) + let reordered = #"{"definition":"swift-json-transform-source","version":1,"dialect":"swift-json-query-v1","source":".","variables":["z","_a","B"]}"# + #expect(try JSONTransformProgramDefinition.decode(Array(reordered.utf8)).result.variableNames == ["B", "_a", "z"]) +} + +@Test func bindingD04ClosedFieldsVersionsAndKindsArePrecise() throws { + for input in ["null", "[]", "{}", dotDefinition.replacingOccurrences(of: "\"version\":1", with: "\"version\":true"), + dotDefinition.replacingOccurrences(of: "\"source\":\".\"", with: "\"unknown\":\".\""), + dotDefinition.replacingOccurrences(of: "[]}", with: "[],\"extra\":null}"), + dotDefinition.replacingOccurrences(of: "[]}", with: "[42]}"), + dotDefinition.replacingOccurrences(of: "swift-json-transform-source", with: "other")] { + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.decode(Array(input.utf8)) } == .invalidDefinition) + } + let version = dotDefinition.replacingOccurrences(of: "\"version\":1", with: "\"version\":2") + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.decode(Array(version.utf8)) } == .unsupportedDefinitionRevision) + let dialect = dotDefinition.replacingOccurrences(of: "query-v1", with: "query-v2") + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.decode(Array(dialect.utf8)) } == .unsupportedDialectRevision) +} + +@Test func bindingD05NativeGrammarAndDuplicateFieldsStayAuthoritative() throws { + let duplicate = dotDefinition.replacingOccurrences(of: "\"version\":1", with: "\"version\":1,\"ver\\u0073ion\":1") + do { _ = try JSONTransformProgramDefinition.decode(Array(duplicate.utf8)); Issue.record("Accepted duplicate key") } + catch let error as JSONFailure { #expect(error.category == .syntax && error.code == .duplicateKey) } + for bytes in [Array((dotDefinition + "false").utf8), [0xef, 0xbb, 0xbf] + Array(dotDefinition.utf8), [0xff]] { + do { _ = try JSONTransformProgramDefinition.decode(bytes); Issue.record("Accepted malformed JSON") } + catch let error as JSONFailure { #expect(error.category == .syntax) } + } +} + +@Test func bindingD06NamesRejectBeforeSetConversion() throws { + for name in ["", "$x", "é", "K", "a-b", "1a", "a\u{0}"] { + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: [name]) } == .invalidVariableName) + } + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["b","a","b"]) } == .duplicateVariableName) + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["K","K"]) } == .invalidVariableName) + let bytes = Array(dotDefinition.replacingOccurrences(of: "[]}", with: "[\"a\",\"a\"]}").utf8) + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.decode(bytes) } == .duplicateVariableName) +} + +@Test func bindingD07SpecificCapsAreInclusiveAndFinite() throws { + var limits = JSONTransformDefinitionLimits(); limits.maximumSourceBytes = 1 + _ = try JSONTransformProgramDefinition.prepare(source: ".", limits: limits) + limits.maximumSourceBytes = 0 + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", limits: limits) } == .limitExceeded(metric: "sourceBytes", limit: 0, attempted: 1)) + _ = try JSONTransformProgramDefinition.prepare(source: "", limits: limits) + limits = .init(); limits.maximumVariableNames = 2 + _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["a","b"], limits: limits) + limits.maximumVariableNames = 1 + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["a","b"], limits: limits) } == .limitExceeded(metric: "variableNames", limit: 1, attempted: 2)) + limits = .init(); limits.maximumNameBytes = 1 + _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["a"], limits: limits) + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["ab"], limits: limits) } == .limitExceeded(metric: "nameBytes", limit: 1, attempted: 2)) + limits = .init(); limits.maximumTotalNameBytes = 2 + _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["a","b"], limits: limits) + limits.maximumTotalNameBytes = 1 + #expect(try definitionFailure { _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["a","b"], limits: limits) } == .limitExceeded(metric: "totalNameBytes", limit: 1, attempted: 2)) + let keys: [WritableKeyPath] = [\.maximumSourceBytes, \.maximumVariableNames, \.maximumNameBytes, \.maximumTotalNameBytes, \.maximumEncodedBytes] + for key in keys { + limits = .init(); limits[keyPath: key] = .max + do { _ = try JSONTransformProgramDefinition.prepare(source: ".", limits: limits); Issue.record("Accepted infinite cap") } + catch let error as JSONFailure { #expect(error.category == .configuration && error.code == .invalidLimits) } + } +} + +@Test func bindingD08WireAndOwnedWorkRetentionHaveExactBoundaries() throws { + let definition = try JSONTransformProgramDefinition.prepare(source: ".").result + let encoded = try definition.encode(); var limits = JSONTransformDefinitionLimits() + limits.maximumEncodedBytes = UInt64(encoded.result.count) + #expect(try definition.encode(limits: limits).result == encoded.result) + #expect(try JSONTransformProgramDefinition.decode(encoded.result, limits: limits).result.source == ".") + limits.maximumEncodedBytes -= 1 + #expect(try definitionResource { _ = try definition.encode(limits: limits) }.resource?.metric == .outputBytes) + #expect(try definitionResource { _ = try JSONTransformProgramDefinition.decode(encoded.result, limits: limits) }.resource?.metric == .inputBytes) + for operation in 0..<3 { + func run(_ policy: JSONTransformDefinitionLimits) throws -> JSONOperationStatistics { + if operation == 0 { return try JSONTransformProgramDefinition.prepare(source: "$b", variables: ["b","a"], limits: policy).statistics } + if operation == 1 { return try JSONTransformProgramDefinition.decode(encoded.result, limits: policy).statistics } + return try definition.encode(limits: policy).statistics + } + let measured = try run(.init()) + for metric in [JSONResourceMetric.workUnits, .retainedBytes] { + var p = JSONTransformDefinitionLimits() + let boundary = metric == .workUnits ? measured[metric] : measured.peakLogicalRetainedBytes + #expect(boundary > 0); p.json[metric] = boundary; _ = try run(p) + p.json[metric] = boundary - 1 + #expect(try definitionResource { _ = try run(p) }.resource?.metric == metric) + } + } + #expect(encoded.statistics[.retainedBytes] == UInt64(encoded.result.count)) +} + +@Test func bindingD09CompileReportsPreserveExistingPlanBehaviorAndBudget() throws { + let report = try JSONTransformProgram.compileWithReport("[$n, .]", variables: ["n"]) + #expect(report.statistics.domain == .core && report.statistics[.workUnits] > 0) + var limits = JSONTransformLimits(); limits[.workUnits] = report.statistics[.workUnits] + let old = try JSONTransformProgram.compile("[$n, .]", variables: ["n"], limits: limits) + for plan in [report.result, old] { + let result = try plan.withExecution(input: 1, bindings: ["n": 42]) { try $0.next()?.value } + #expect(result.result == .array([42,1])) + } + limits[.workUnits] -= 1 + do { _ = try JSONTransformProgram.compile("[$n, .]", variables: ["n"], limits: limits); Issue.record("Compile reset work") } + catch let error as JSONTransformFailure { #expect(error.category == .resource && error.metric == "workUnits") } + let definition = try JSONTransformProgramDefinition.prepare(source: "[$n, .]", variables: ["n"]).result + #expect(try definition.compile().statistics[.workUnits] > report.statistics[.workUnits]) +} + +@Test func bindingD10CodecDoesNotInventQueryValidationOrDecimalAuthority() throws { + let bad = try JSONTransformProgramDefinition.prepare(source: "(").result + _ = try JSONTransformProgramDefinition.decode(bad.encode().result) + do { _ = try bad.compile(); Issue.record("Accepted invalid query") } + catch let error as JSONTransformFailure { #expect(error.category == .compilation) } + let source = "[9007199254740993,0.1,\"é\"]" + let definition = try JSONTransformProgramDefinition.prepare(source: source).result + let reload = try JSONTransformProgramDefinition.decode(definition.encode().result).result + let plan = try reload.compile().result + let result = try plan.withExecution(input: .null) { try $0.next()?.value } + let expected: JSON = .array([.number(try JSONNumber(parsing: "9007199254740993")), + .number(try JSONNumber(parsing: "0.1")),.string("e\u{301}")]) + #expect(result.result == expected) +} + +@Test func bindingD11CancellationOccursInsideOwnedCodecWork() async throws { + let handle = Mutex?>(nil) + let ready = Mutex?>(nil) + let observed = Mutex(0) + let task = Task { + await withCheckedContinuation { continuation in ready.withLock { $0 = continuation } } + var limits = JSONTransformDefinitionLimits() + limits.checkpointObserver = { work in + if work >= 32 { observed.withLock { $0 = work }; handle.withLock { $0?.cancel() } } + } + _ = try JSONTransformProgramDefinition.prepare(source: String(repeating: "x", count: 4096), limits: limits) + } + handle.withLock { $0 = task } + var continuation: CheckedContinuation? + while continuation == nil { continuation = ready.withLock { $0 }; if continuation == nil { await Task.yield() } } + continuation?.resume() + do { try await task.value; Issue.record("Owned codec ignored cancellation") } catch is CancellationError {} + #expect(observed.withLock { $0 } >= 32) +} + +@Test func bindingSharedDefinitionAndCompilationHaveIndependentHealth() async throws { + let definition = try JSONTransformProgramDefinition.prepare(source: ".").result + let results = try await withThrowingTaskGroup(of: Bool.self) { group in + for index in 0..<6 { group.addTask { + if index == 0 { + var limits = JSONTransformLimits(); limits[.workUnits] = 0 + do { _ = try definition.compile(limits: limits); return false } + catch let error as JSONTransformFailure { return error.category == .resource } + } + let bytes = try definition.encode().result + let reload = try JSONTransformProgramDefinition.decode(bytes).result + let plan = try reload.compile().result + return try plan.withExecution(input: 42) { try $0.next()?.value }.result == 42 + } } + var values: [Bool] = []; for try await value in group { values.append(value) }; return values + } + #expect(results.count == 6 && results.allSatisfy { $0 }) +} diff --git a/Tests/JSONTransformTests/IndexedSelectionSpike.swift b/Tests/JSONTransformTests/IndexedSelectionSpike.swift new file mode 100644 index 0000000..6aee3fe --- /dev/null +++ b/Tests/JSONTransformTests/IndexedSelectionSpike.swift @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: Apache-2.0 +// Test-only ownership experiment. This is not a shipped selection API. +import Testing +import Synchronization +import JSON +import JSONFilterRuntime +@testable import JSONTransform + +private enum SelectionSpikeFailure: Error, Equatable { + case invalidPosition, incomplete, closed, provider, invalidBindings +} +private final class PopulationOwner { + let population: FilterYield + let metadata: TransformValueLease + init(_ population: FilterYield, metadata: TransformValueLease) { + self.population = population; self.metadata = metadata + } +} +private struct SpikePosition { + let ordinal: UInt64 + let owner: PopulationOwner + let reservation: TransformValueLease +} +private struct SpikeInput: FilterInput { + var fail = false + var calls = 0 + mutating func next(context: inout TransformRuntimeContext) throws -> FilterYield? { + calls += 1 + if fail { throw SelectionSpikeFailure.provider } + return nil + } +} +private final class SelectionSpike { + private var context: TransformRuntimeContext + private var vm: FilterVM? + private var owner: PopulationOwner? + private var input = SpikeInput() + private var failure: (any Error)? + private var ended = false, closed = false + private var planBytes: UInt64 = 0 + private init(_ context: consuming TransformRuntimeContext) { self.context = consume context } + + static func make(_ plan: JSONTransformProgram, population: [JSON], + limits: JSONTransformLimits = .init(), providerFailure: Bool = false, + observer: (@Sendable (UInt64) -> Void)? = nil, + projectionObserver: (@Sendable (Int) -> Void)? = nil, + cleanupObserver: (@Sendable (UInt64) -> Void)? = nil) throws -> SelectionSpike { + var context = try TransformRuntimeContext(limits: limits.runtime) + try context.meter.retain(128) + context.meter.checkpointObserver = observer + let result = SelectionSpike(consume context) + do { + guard plan.variables == ["values"] else { throw SelectionSpikeFailure.invalidBindings } + try result.prepare(plan, population: population, providerFailure: providerFailure, projectionObserver: projectionObserver) + return result + } catch { + let cause = error + do { try result.close() } catch { Issue.record("Prototype preparation cleanup failed: \(error)") } + if let cleanupObserver { + do { cleanupObserver(try result.settlement()[.retainedBytes]) } + catch { Issue.record("Prototype terminal observation failed: \(error)") } + } + throw cause + } + } + private func prepare(_ plan: JSONTransformProgram, population: [JSON], providerFailure: Bool, + projectionObserver: (@Sendable (Int) -> Void)?) throws { + try context.meter.retain(plan.planReservation); planBytes = plan.planReservation + // A prototype population cap, independent of lexical token counts. + guard population.count <= 1024 else { throw SelectionSpikeFailure.invalidBindings } + try context.charge(.bindings) + let value = try FilterValues.produced(.array(population), context: &context) + try context.meter.retain(48) + let metadata = try TransformValueLease(ownedRetained: 48, context: &context) + owner = PopulationOwner(value, metadata: metadata) + var indices: [JSON] = [], scratch: UInt64 = 0 + defer { + do { try context.meter.release(scratch) } + catch { Issue.record("Prototype projection scratch cleanup failed: \(error)") } + } + for ordinal in population.indices { + try context.work(); try context.meter.retain(64); scratch += 64 + indices.append(.number(JSONNumber(integer: ordinal))) + projectionObserver?(indices.count) + } + let root = try FilterValues.array(indices, context: &context) + indices = []; try context.meter.release(scratch); scratch = 0 + try context.charge(.totalRecords) + vm = try FilterVM(program: plan.program, input: root, bindings: ["values": value], context: &context) + input.fail = providerFailure + } + func next() throws -> SpikePosition? { + do { + try context.reclaimValueLeases() + if let failure { throw failure } + try context.checkpoint() + guard !closed else { throw SelectionSpikeFailure.closed } + guard var active = vm else { throw SelectionSpikeFailure.closed } + vm = nil; defer { vm = active } + guard let raw = try active.next(input: &input, context: &context) else { ended = true; return nil } + let before = context.meter.used(.retainedBytes) + do { + try context.meter.resetValueBounds() + try JSONAdmission.value(raw.value, meter: &context.meter) + } catch { + try context.meter.release(context.meter.used(.retainedBytes)-before); throw error + } + try context.charge(.totalResults) + guard case .number(let number) = raw.value else { throw SelectionSpikeFailure.invalidPosition } + let ordinal: UInt64 + do { ordinal = try number.integer(as: UInt64.self, meter: &context.meter) } + catch let error as JSONFailure where error.category == .conversion { throw SelectionSpikeFailure.invalidPosition } + guard let native = Int(exactly: ordinal), let owner, + case .array(let values) = owner.population.value, native < values.count else { + throw SelectionSpikeFailure.invalidPosition + } + try context.meter.retain(32) + let slot = try TransformValueLease(ownedRetained: 32, context: &context) + return .init(ordinal: ordinal, owner: owner, reservation: slot) + } catch { + if failure == nil { failure = error; context.latch(error) } + throw failure ?? error + } + } + func accepts(_ position: SpikePosition) -> Bool { owner === position.owner } + func finish() throws -> JSONOperationStatistics { + if failure == nil && !ended { failure = SelectionSpikeFailure.incomplete; context.latch(SelectionSpikeFailure.incomplete) } + let cause = failure + try close() + if let cause { throw cause } + return try settlement() + } + func settlement() throws -> JSONOperationStatistics { + try context.reclaimValueLeases() + return context.meter.statistics() + } + var providerCalls: Int { input.calls } + private func close() throws { + guard !closed else { return }; closed = true + if var active = vm { vm = nil; try active.close(context: &context) } + owner = nil + try context.reclaimValueLeases() + try context.meter.release(planBytes + 128); planBytes = 0 + try context.release(.bindings, context.used(.bindings)) + } +} +private func spikePlan(_ source: String) throws -> JSONTransformProgram { + try JSONTransformProgram.compile(source, variables: ["values"]) +} + +@Test func bindingP01ExplicitIndicesPreserveDuplicatesOrderAndTies() throws { + let row = try JSON.parse(#"{"score":1}"#) + for (source, expected) in [(".[] | select(. == 1)", [UInt64(1)]), + ("1,0,1", [1,0,1]), + ("sort_by([$values[.].score, .])[]", [0,1])] { + let probe = try SelectionSpike.make(spikePlan(source), population: [row,row]) + var ordinals: [UInt64] = [] + while let position = try probe.next() { ordinals.append(position.ordinal) } + let report = try probe.finish() + #expect(ordinals == expected && report[.workUnits] > 0) + } + let empty = try SelectionSpike.make(spikePlan("empty"), population: []) + #expect(try empty.next() == nil); #expect(try empty.finish()[.workUnits] > 0) +} + +@Test func bindingP02InvalidOutputAndAbandonedPrefixNeverCertify() throws { + for bad in ["null", "true", "\"0\"", "-1", "0.1", "18446744073709551616", "9223372036854775808", "1"] { + let probe = try SelectionSpike.make(spikePlan("try (0, " + bad + ") catch 0"), population: [42]) + #expect(try probe.next()?.ordinal == 0) + for _ in 0..<2 { + do { _ = try probe.next(); Issue.record("Invalid position escaped") } + catch let error as SelectionSpikeFailure { #expect(error == .invalidPosition) } + } + do { _ = try probe.finish(); Issue.record("Caught refusal certified selection") } + catch let error as SelectionSpikeFailure { #expect(error == .invalidPosition) } + } + let prefix = try SelectionSpike.make(spikePlan("0,0"), population: [42]) + #expect(try prefix.next()?.ordinal == 0) + do { _ = try prefix.finish(); Issue.record("Abandoned prefix certified") } + catch let error as SelectionSpikeFailure { #expect(error == .incomplete) } +} + +@Test func bindingP03PopulationLeaseSurvivesCloseAndForeignOwnerRefuses() throws { + let plan = try spikePlan("0") + let source: [JSON] = [.string(String(repeating: "x", count: 8192))] + let a = try SelectionSpike.make(plan, population: source), b = try SelectionSpike.make(plan, population: source) + var position = try a.next(), alias = position + #expect(position?.ordinal == 0) + #expect(position.map { !b.accepts($0) } == true) + #expect(try a.next() == nil) + _ = try a.finish() + let retainedBefore = try withExtendedLifetime(alias) { try a.settlement()[.retainedBytes] } + #expect(retainedBefore > 8192) + position = nil + let retainedAfter = try withExtendedLifetime(alias) { try a.settlement()[.retainedBytes] } + #expect(retainedAfter > 8192) + alias = nil + #expect(try a.settlement()[.retainedBytes] == 0) + do { _ = try a.next(); Issue.record("Closed handle resumed") } + catch let error as SelectionSpikeFailure { #expect(error == .closed) } + #expect(try b.next()?.ordinal == 0); #expect(try b.next() == nil); _ = try b.finish() + for source in ["0.0", "-0"] { + let probe = try SelectionSpike.make(spikePlan(source), population: [42]) + #expect(try probe.next()?.ordinal == 0); #expect(try probe.next() == nil); _ = try probe.finish() + } +} + +@Test func bindingP04ResourceAndProviderFailuresStayTerminal() throws { + var limits = JSONTransformLimits(); limits[.results] = 1 + let probe = try SelectionSpike.make(spikePlan("0,0"), population: [42], limits: limits) + #expect(try probe.next()?.ordinal == 0) + for _ in 0..<2 { + do { _ = try probe.next(); Issue.record("Results cap reset") } + catch { let e = JSONTransformFailure.classify(error); #expect(e.category == .resource && e.metric == "results") } + } + do { _ = try probe.finish(); Issue.record("Failed quota certified") } + catch { #expect(JSONTransformFailure.classify(error).category == .resource) } + let provider = try SelectionSpike.make(spikePlan("try input catch 0"), population: [42], providerFailure: true) + for _ in 0..<2 { + do { _ = try provider.next(); Issue.record("Provider error was swallowed") } + catch let error as SelectionSpikeFailure { #expect(error == .provider) } + } + #expect(provider.providerCalls == 1) + do { _ = try provider.finish(); Issue.record("Provider error certified") } + catch let error as SelectionSpikeFailure { #expect(error == .provider) } +} + +@Test func bindingSharedSelectionSpikeReusesOnePlanIndependently() async throws { + let plan = try spikePlan(".[]") + let values = try await withThrowingTaskGroup(of: Bool.self) { group in + for _ in 0..<6 { group.addTask { + let probe = try SelectionSpike.make(plan, population: [42,42]) + let a = try probe.next()?.ordinal, b = try probe.next()?.ordinal + guard try probe.next() == nil else { return false } + return try probe.finish()[.workUnits] > 0 && a == 0 && b == 1 + } } + var result: [Bool] = []; for try await x in group { result.append(x) }; return result + } + #expect(values.count == 6 && values.allSatisfy { $0 }) +} + +@Test func bindingP05ProjectionEvaluationAndCancellationShareOneLedger() async throws { + let plan = try spikePlan("0"), population: [JSON] = Array(repeating: .string("row"), count: 64) + func run(_ limits: JSONTransformLimits) throws -> JSONOperationStatistics { + let probe = try SelectionSpike.make(plan, population: population, limits: limits) + do { + while let position = try probe.next() { #expect(position.ordinal == 0) } + return try probe.finish() + } catch { + let first = error + do { _ = try probe.finish() } catch { #expect(JSONTransformFailure.classify(error) == JSONTransformFailure.classify(first)) } + throw first + } + } + let measured = try run(.init()) + var limits = JSONTransformLimits(); limits[.workUnits] = measured[.workUnits] + #expect(try run(limits)[.workUnits] == measured[.workUnits]) + limits[.workUnits] -= 1 + do { _ = try run(limits); Issue.record("Projection/evaluation restarted work") } + catch { #expect(JSONTransformFailure.classify(error).category == .resource) } + let handle = Mutex?>(nil), ready = Mutex?>(nil) + let observed = Mutex(0), finalRetained = Mutex(nil) + let task = Task { + await withCheckedContinuation { continuation in ready.withLock { $0 = continuation } } + _ = try SelectionSpike.make(plan, population: population, projectionObserver: { count in + if count == 4 { observed.withLock { $0 = count }; handle.withLock { $0?.cancel() } } + }, cleanupObserver: { value in finalRetained.withLock { $0 = value } }) + } + handle.withLock { $0 = task } + var continuation: CheckedContinuation? + while continuation == nil { continuation = ready.withLock { $0 }; if continuation == nil { await Task.yield() } } + continuation?.resume() + do { try await task.value; Issue.record("Projection ignored cancellation") } catch is CancellationError {} + #expect(observed.withLock { $0 } == 4) + #expect(finalRetained.withLock { $0 } == 0) +} diff --git a/Tests/JSONTransformTests/SelectionTests.swift b/Tests/JSONTransformTests/SelectionTests.swift new file mode 100644 index 0000000..c3fc006 --- /dev/null +++ b/Tests/JSONTransformTests/SelectionTests.swift @@ -0,0 +1,338 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import Synchronization +import JSON +@testable import JSONTransform + +private func selectionPlan(_ source: String, extra: Set = []) throws -> JSONTransformProgram { + try .compile(source, variables: extra.union(["values"])) +} +private func select(_ source: String, _ values: [JSON], limits: JSONTransformSelectionLimits = .init()) throws -> JSONTransformSelectionReport<[JSONTransformSelectionPosition]> { + try selectionPlan(source).withSelection(population: values, limits: limits) { scope in + var positions: [JSONTransformSelectionPosition] = [] + while let p = try scope.next() { positions.append(p) } + return positions + } +} +private enum SelectionTestError: Error, Equatable { case sentinel } +private func refusal(_ expected: JSONTransformSelectionFailure, _ body: () throws -> Void) { + do { try body(); Issue.record("Expected selection refusal \(expected)") } + catch let e as JSONTransformSelectionFailure { #expect(e == expected) } + catch { Issue.record("Wrong failure \(error)") } +} + +@Test func selectionS01ExplicitOrdinalsPreserveDuplicateValuesOrderRepeatsAndTies() throws { + let row = try JSON.parse(#"{"score":1,"é":9007199254740993,"é":0.1}"#) + for (source, indices) in [(".[] | select(. == 1)", [UInt64(1)]), ("1,0,1", [1,0,1]), + ("sort_by([$values[.].score, .])[]", [0,1]), + (".[] | select($values[.][\"é\"] == 9007199254740993)", [0,1])] { + let report = try select(source, [row,row]) + #expect(report.result.map(\.ordinal) == indices) + #expect(try report.result.map { try report.completion.ordinal(of: $0) } == indices) + #expect(report.completion.populationCount == 2 && report.completion.outputCount == UInt64(indices.count)) + #expect(report.completion.conventionRevision == 1 && report.completion.statistics.domain == .core) + } +} + +@Test func selectionS02EmptyEOFNullAndIncompletePrefixAreDistinct() throws { + var zero = JSONTransformSelectionLimits(); zero.maximumPopulationValues = 0 + let empty = try select("empty", [], limits: zero) + #expect(empty.result.isEmpty && empty.completion.outputCount == 0 && empty.completion.populationCount == 0) + refusal(.incompleteSelection) { _ = try selectionPlan("empty").withSelection(population: []) { _ in () } } + refusal(.incompleteSelection) { + _ = try selectionPlan("0,0").withSelection(population: [42]) { s in _ = try s.next() } + } + refusal(.invalidPosition) { _ = try select("null", [.null]) } + let presentNull = try select("0", [.null]) + #expect(presentNull.result.first?.ordinal == 0 && presentNull.completion.outputCount == 1) +} + +@Test func selectionS03EveryInvalidOutputIsExactAndTerminalOutsideQueryCatch() throws { + for bad in ["null","false","\"0\"","[]","{}","-1","0.1","18446744073709551616","18446744073709551615","9223372036854775808","1"] { + var escaped: JSONTransformSelectionScope? + refusal(.invalidPosition) { + _ = try selectionPlan("try (0, " + bad + ") catch 0").withSelection(population: [42]) { s in + escaped = s; let first = try s.next(); #expect(first?.ordinal == 0) + for _ in 0..<2 { refusal(.invalidPosition) { _ = try s.next() } } + } + } + let scope = try #require(escaped) + refusal(.invalidPosition) { _ = try scope.next() } + #expect(try scope.report()[.retainedBytes] == 0) + } + #expect(try select("42.0", Array(repeating: .null, count: 43)).result.first?.ordinal == 42) + for source in ["0.0", "-0", "0e100"] { #expect(try select(source, [42]).result.first?.ordinal == 0) } +} + +@Test func selectionS04CompletionRejectsForeignInvocationsAndPositionsRetainPopulation() throws { + let plan = try selectionPlan("0") + var scope: JSONTransformSelectionScope? + var a: JSONTransformSelectionReport? = try plan.withSelection(population: [.string(String(repeating: "x", count: 8192))]) { s in + scope = s; let p = try s.next(); #expect(try s.next() == nil); return p + } + var position = a?.result, alias = position + var completion = a?.completion + let b = try select("0", [.string(String(repeating: "x", count: 8192))]) + let p = try #require(position), witness = try #require(completion) + refusal(.foreignPosition) { _ = try b.completion.ordinal(of: p) } + #expect(try witness.ordinal(of: p) == 0) + // Keep only aliases in a nested scope so last ARC releases are observable below. + a = nil; position = nil + let owner = try #require(scope) + let withAlias = try withExtendedLifetime(alias) { try owner.report()[.retainedBytes] } + #expect(withAlias > 8192) + alias = nil; completion = nil + // p/witness still own their real reservations until this function exits; + // the separate S05 case proves the exact last-alias transition. + #expect(try owner.report()[.retainedBytes] > 8192) + do { _ = try owner.next(); Issue.record("Escaped closed scope resumed") } + catch let e as JSONTransformFailure { #expect(e.category == .configuration && e.code == "scopeClosed") } +} + +@Test func selectionS05FinalAliasAndCompletionReleaseSettleToZeroWithoutDataPulls() throws { + var scope: JSONTransformSelectionScope? + var position: JSONTransformSelectionPosition? + var completion: JSONTransformSelectionCompletion? + do { + let report = try selectionPlan("0").withSelection(population: [.string(String(repeating: "p", count: 8192))]) { s in + scope = s; position = try s.next(); #expect(try s.next() == nil) + } + completion = report.completion + } + let owner = try #require(scope) + let bytes = try withExtendedLifetime(position) { try owner.report()[.retainedBytes] } + #expect(bytes > 8192) + position = nil + let identityOnly = try withExtendedLifetime(completion) { try owner.report()[.retainedBytes] } + #expect(identityOnly > 0 && identityOnly < 8192) + completion = nil + let settled = try owner.report() + #expect(settled[.retainedBytes] == 0 && owner.used(.collectionValues) == 0 && owner.used(.bindings) == 0) + #expect(settled[.workUnits] > 0) +} + +@Test func selectionS06BindingsAreExplicitReservedAndScalarChecked() throws { + let plan = try selectionPlan(".[] | select($values[.] >= $minimum)", extra: ["minimum"]) + let report = try plan.withSelection(population: [1,2,3], bindings: ["minimum": 2]) { s in + var ids: [UInt64] = []; while let p = try s.next() { ids.append(p.ordinal) }; return ids + } + #expect(report.result == [1,2]) + for bindings: [String:JSON] in [[:],["values": .null],["other": 2],["K": 2]] { + do { _ = try plan.withSelection(population: [1], bindings: bindings) { _ in () }; Issue.record("Accepted binding override/mismatch") } + catch let e as JSONTransformFailure { #expect(e.category == .configuration && e.code == "invalidBindings") } + } + do { _ = try JSONTransformProgram.compile("0").withSelection(population: [1]) { _ in () }; Issue.record("Missing reserved declaration accepted") } + catch let e as JSONTransformFailure { #expect(e.code == "invalidBindings") } +} + +@Test func selectionS07PopulationPolicyIsInclusiveFiniteAndCannotSkipCompleteAdmission() throws { + var policy = JSONTransformSelectionLimits(); policy.maximumPopulationValues = 2 + _ = try select("empty", [1,2], limits: policy) + policy.maximumPopulationValues = 1 + do { _ = try select("empty", [1,2], limits: policy); Issue.record("Population cap escaped") } + catch let e as JSONTransformFailure { #expect(e.category == .resource && e.metric == "populationValues" && e.limit == 1 && e.attemptedUsage == 2) } + for cap in [UInt64.max, UInt64(Int.max)] { + policy = .init(); policy.maximumPopulationValues = cap + do { _ = try select("empty", [], limits: policy); Issue.record("Infinite/native overflow policy accepted") } + catch let e as JSONTransformFailure { #expect(e.category == .configuration && e.code == "invalidLimits") } + } + policy = .init(); policy.transform.json[.stringBytes] = 1 + do { _ = try select("empty", [0,.string("too long")], limits: policy); Issue.record("Unused population tail bypassed admission") } + catch let e as JSONTransformFailure { #expect(e.category == .resource && e.metric == "stringBytes") } +} + +@Test func selectionS08ProjectionEvaluationAndCompletionShareExactCumulativeWorkAndRetention() throws { + let source = "sort_by([$values[.],.])[]", values: [JSON] = [3,1,2] + let baseline = try select(source, values) + var policy = JSONTransformSelectionLimits() + let work = baseline.completion.statistics[.workUnits], peak = baseline.completion.statistics.peakLogicalRetainedBytes + policy.transform[.workUnits] = work + #expect(try select(source, values, limits: policy).result.map(\.ordinal) == [1,2,0]) + policy.transform[.workUnits] = work - 1 + do { _ = try select(source, values, limits: policy); Issue.record("Completion work reset") } + catch let e as JSONTransformFailure { #expect(e.category == .resource && e.metric == "workUnits") } + policy = .init(); policy.transform[.retainedBytes] = peak + _ = try select(source, values, limits: policy) + policy.transform[.retainedBytes] = peak - 1 + do { _ = try select(source, values, limits: policy); Issue.record("Retention escaped") } + catch let e as JSONTransformFailure { #expect(e.category == .resource && e.metric == "retainedBytes") } + policy = .init(); policy.transform[.collectionValues] = 5 + do { _ = try select("empty", values, limits: policy); Issue.record("Projection slots not pre-admitted") } + catch let e as JSONTransformFailure { #expect(e.category == .resource && e.metric == "collectionValues") } +} + +@Test func selectionS09QuotaAndCancellationRefundTerminalAliasesAndPreserveFirstCause() throws { + for cancel in [false,true] { + var scope: JSONTransformSelectionScope?, output: JSONTransformSelectionPosition? + var policy = JSONTransformSelectionLimits(); policy.transform[.results] = 1 + do { + _ = try selectionPlan("0,0").withSelection(population: [.string(String(repeating: "q", count: 4096))], limits: policy) { s in + scope = s; output = try s.next() + if cancel { s.cancel() } + for _ in 0..<2 { + do { _ = try s.next(); Issue.record("Terminal error swallowed") } + catch let e as JSONTransformFailure { #expect(e.category == (cancel ? .cancelled : .resource)) } + } + throw SelectionTestError.sentinel + } + Issue.record("Failed invocation certified") + } catch let e as JSONTransformFailure { #expect(e.category == (cancel ? .cancelled : .resource)) } + let s = try #require(scope) + let bytes = try withExtendedLifetime(output) { try s.report()[.retainedBytes] } + #expect(bytes > 4096); output = nil + #expect(try s.report()[.retainedBytes] == 0) + } +} + +@Test func selectionS10ProviderEOFNullErrorAndNoFurtherPullsRemainDistinct() throws { + var calls = 0 + let empty = try selectionPlan("inputs").withSelection(population: [], source: { calls += 1; return nil }) { s in let eof = try s.next(); #expect(eof == nil) } + #expect(calls == 1 && empty.completion.outputCount == 0) + calls = 0 + refusal(.invalidPosition) { + _ = try selectionPlan("input").withSelection(population: [1], source: { calls += 1; return .null }) { s in + for _ in 0..<2 { refusal(.invalidPosition) { _ = try s.next() } } + } + } + #expect(calls == 1) + calls = 0 + do { + _ = try selectionPlan("try input catch 0").withSelection(population: [1], source: { calls += 1; throw SelectionTestError.sentinel }) { s in + for _ in 0..<2 { + do { _ = try s.next(); Issue.record("Provider failure swallowed") } + catch let e as JSONTransformFailure { #expect(e.category == .provider && e.code == "providerFailed") } + } + } + Issue.record("Provider error certified") + } catch let e as JSONTransformFailure { #expect(e.category == .provider) } + #expect(calls == 1) +} + +@Test func selectionS11ReentryAndProviderCancellationUseSafeEntrySnapshots() throws { + for cancel in [false,true] { + var escaped: JSONTransformSelectionScope?, calls = 0 + do { + _ = try selectionPlan("input").withSelection(population: [42], source: { + calls += 1 + if let s = escaped { + let report = try s.report(); #expect(report[.workUnits] == s.used(.workUnits)) + #expect(s.remainingWorkUnits > 0) + if cancel { s.cancel() } + else { + do { _ = try s.next(); Issue.record("Provider reentry accepted") } + catch let e as JSONTransformFailure { #expect(e.code == "reentrantSelection") } + } + } + return 0 + }) { s in + escaped = s + do { _ = try s.next(); Issue.record("Provider ignored terminal health") } + catch let e as JSONTransformFailure { #expect(e.category == (cancel ? .cancelled : .configuration)) } + } + Issue.record("Caught provider failure certified") + } catch let e as JSONTransformFailure { #expect(e.category == (cancel ? .cancelled : .configuration)) } + #expect(calls == 1) + #expect(try #require(escaped).report()[.retainedBytes] == 0) + } +} + +@Test func selectionS12CallerErrorAndEscapedClosuresReleaseProviderWithoutCompletion() throws { + final class Source { var calls = 0; func next() -> JSON? { calls += 1; return 0 } } + weak var weakSource: Source? + var scope: JSONTransformSelectionScope?, closure: (() throws -> JSONTransformSelectionPosition?)? + do { + let source = Source(); weakSource = source + do { + _ = try selectionPlan("input").withSelection(population: [42], source: { source.next() }) { s in + scope = s; closure = { try s.next() }; throw SelectionTestError.sentinel + } + Issue.record("Caller error lost") + } catch let e as SelectionTestError { #expect(e == .sentinel) } + } + #expect(weakSource == nil) + do { _ = try closure?(); Issue.record("Escaped closure resumed") } + catch let e as JSONTransformFailure { #expect(e.code == "scopeClosed") } + #expect(try #require(scope).report()[.retainedBytes] == 0) +} + +@Test func selectionSharedImmutablePlanHasIndependentPopulationAndFailureHealth() async throws { + let plan = try selectionPlan(".[]") + let results = try await withThrowingTaskGroup(of: [UInt64].self) { group in + for size in 0..<6 { + group.addTask { + var policy = JSONTransformSelectionLimits() + if size == 5 { policy.transform[.results] = 0 } + do { + let report = try plan.withSelection(population: Array(repeating: .null, count: size), limits: policy) { s in + var ids: [UInt64] = []; while let p = try s.next() { ids.append(p.ordinal) }; return ids + } + #expect(size != 5 && report.completion.populationCount == UInt64(size)); return report.result + } catch let e as JSONTransformFailure { #expect(size == 5 && e.category == .resource); return [99] } + } + } + var result: [[UInt64]] = []; for try await row in group { result.append(row) }; return result + } + #expect(results.count == 6 && results.contains([99]) && results.contains([0,1,2,3])) +} + +@Test func selectionS13InnerProjectionTaskCancellationReleasesUnpublishedPreparation() async throws { + let plan = try selectionPlan(".[]") + let handle = Mutex?>(nil), ready = Mutex?>(nil) + let observed = Mutex(0) + let task = Task { + await withCheckedContinuation { continuation in ready.withLock { $0 = continuation } } + var policy = JSONTransformSelectionLimits() + policy.projectionObserver = { count in + if count == 4 { observed.withLock { $0 = count }; handle.withLock { $0?.cancel() } } + } + var failed: JSONTransformSelectionScope? + do { + _ = try JSONTransformSelectionScope.make(plan: plan, values: Array(repeating: .null, count: 32), bindings: [:], limits: policy, source: nil, observing: { failed = $0 }) + Issue.record("Projection ignored task cancellation") + } catch let e as JSONTransformFailure { #expect(e.category == .cancelled) } + return try #require(failed).report()[.retainedBytes] + } + handle.withLock { $0 = task } + var continuation: CheckedContinuation? + while continuation == nil { continuation = ready.withLock { $0 }; if continuation == nil { await Task.yield() } } + continuation?.resume() + #expect(try await task.value == 0) + #expect(observed.withLock { $0 } == 4) +} + +@Test func selectionS14FailedPreparationAndCompletionAllocationsRefundExactBoundaries() throws { + let plan = try selectionPlan("empty") + var failed: JSONTransformSelectionScope? + var policy = JSONTransformSelectionLimits(); policy.transform[.collectionValues] = 3 + do { + _ = try JSONTransformSelectionScope.make(plan: plan, values: [1,2], bindings: [:], limits: policy, source: nil, observing: { failed = $0 }) + Issue.record("Partial projection admitted") + } catch let e as JSONTransformFailure { #expect(e.category == .resource && e.metric == "collectionValues") } + #expect(try #require(failed).report()[.retainedBytes] == 0) + let base = try select("empty", []) + policy = .init(); policy.transform[.workUnits] = base.completion.statistics[.workUnits] - 1 + var closed: JSONTransformSelectionScope? + do { + _ = try selectionPlan("empty").withSelection(population: [], limits: policy) { s in + closed = s; let eof = try s.next(); #expect(eof == nil) + } + Issue.record("Completion exceeded work budget") + } catch let e as JSONTransformFailure { #expect(e.category == .resource) } + #expect(try #require(closed).report()[.retainedBytes] == 0) +} + +@Test func selectionS15LanguageCatchesRemainOrdinaryAndUncaughtFailuresStayTerminal() throws { + #expect(try select("try error(\"expected\") catch 0", [42]).result.first?.ordinal == 0) + var scope: JSONTransformSelectionScope? + do { + _ = try selectionPlan("0,error(\"expected\")").withSelection(population: [42]) { s in + scope = s; let p = try s.next(); #expect(p?.ordinal == 0) + for _ in 0..<2 { + do { _ = try s.next(); Issue.record("Language failure reopened") } + catch let e as JSONTransformFailure { #expect(e.category == .language) } + } + } + Issue.record("Uncaught language failure certified") + } catch let e as JSONTransformFailure { #expect(e.category == .language) } + #expect(try #require(scope).report()[.retainedBytes] == 0) +} diff --git a/Tools/TransformBindingQualification/Package.swift b/Tools/TransformBindingQualification/Package.swift new file mode 100644 index 0000000..91d5f5b --- /dev/null +++ b/Tools/TransformBindingQualification/Package.swift @@ -0,0 +1,8 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "TransformBindingQualification", platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "TransformBindingConsumer", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONTransform", package: "Swift-JSON") + ])], swiftLanguageModes: [.v6]) diff --git a/Tools/TransformBindingQualification/README.md b/Tools/TransformBindingQualification/README.md new file mode 100644 index 0000000..aa8fb40 --- /dev/null +++ b/Tools/TransformBindingQualification/README.md @@ -0,0 +1,8 @@ +# Source definition public consumer + +Run `swift run --package-path Tools/TransformBindingQualification TransformBindingConsumer`. +Only JSON and JSONTransform products are imported. Six named witnesses cover the +compiled DocC example, independent canonical bytes, canonical names, duplicate-name +rejection, operational compilation failure and null/value/EOF execution reports. +This is path-dependent preparation, not version-tag resolution. No public indexed +selection API is claimed; its Sprint1 experiment stays in the package test target. diff --git a/Tools/TransformBindingQualification/Sources/TransformBindingConsumer/Main.swift b/Tools/TransformBindingQualification/Sources/TransformBindingConsumer/Main.swift new file mode 100644 index 0000000..f0ff9f4 --- /dev/null +++ b/Tools/TransformBindingQualification/Sources/TransformBindingConsumer/Main.swift @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform + +func guideExample() throws { + let definition = try JSONTransformProgramDefinition.prepare( + source: "$amount", variables: ["amount"]).result + let bytes = try definition.encode().result + let restored = try JSONTransformProgramDefinition.decode(bytes).result + let compiled = try restored.compile() + let amount = JSON.number(try JSONNumber(parsing: "9007199254740993")) + let output = try compiled.result.withExecution(input: .null, bindings: ["amount": amount]) { + try $0.next()?.value + } + precondition(output.result == amount) + precondition(compiled.statistics[.workUnits] > 0) +} +@main enum Consumer { + static func main() throws { + try guideExample() + let golden = #"{"definition":"swift-json-transform-source","version":1,"dialect":"swift-json-query-v1","source":".","variables":[]}"# + let definition = try JSONTransformProgramDefinition.prepare(source: ".").result + let canonical = try definition.encode().result + precondition(canonical == Array(golden.utf8)) + let names = try JSONTransformProgramDefinition.prepare(source: "$z", variables: ["z","a"]).result + precondition(names.variableNames == ["a","z"]) + do { + _ = try JSONTransformProgramDefinition.prepare(source: ".", variables: ["a","a"]) + preconditionFailure("Duplicate name accepted") + } catch let error as JSONTransformDefinitionFailure { precondition(error == .duplicateVariableName) } + var limits = JSONTransformLimits(); limits[.workUnits] = 0 + do { _ = try definition.compile(limits: limits); preconditionFailure("Compile budget reset") } + catch let error as JSONTransformFailure { precondition(error.category == .resource) } + let report = try JSONTransformProgram.compileWithReport("null,42") + let output = try report.result.withExecution(input: .null) { execution in + let first = try execution.next(), second = try execution.next(), end = try execution.next() + precondition(first?.value == .null && second?.value == 42 && end == nil) + } + precondition(report.statistics[.workUnits] > 0 && output.statistics[.workUnits] > 0) + print("TRANSFORM_BINDINGS passed witnesses=6") + } +} diff --git a/Tools/TransformSelectionQualification/Package.swift b/Tools/TransformSelectionQualification/Package.swift new file mode 100644 index 0000000..3985a93 --- /dev/null +++ b/Tools/TransformSelectionQualification/Package.swift @@ -0,0 +1,8 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "TransformSelectionQualification", platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "TransformSelectionConsumer", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONTransform", package: "Swift-JSON") + ])], swiftLanguageModes: [.v6]) diff --git a/Tools/TransformSelectionQualification/README.md b/Tools/TransformSelectionQualification/README.md new file mode 100644 index 0000000..02a5783 --- /dev/null +++ b/Tools/TransformSelectionQualification/README.md @@ -0,0 +1,10 @@ +# Public indexed-selection consumer + +Run `swift run --package-path Tools/TransformSelectionQualification TransformSelectionConsumer`. +Only JSON and JSONTransform are imported. Ten witnesses execute the exact DocC +example, order/repeats, empty completion, late invalidity, foreign ownership, +explicit bindings, final lease release, provider terminality, cumulative work +and cancellation. The qualification script copies this package outside Git, +runs Debug/Release and checks ordinary full-codegen public boundary refusals. +A path dependency is preparation evidence, not version-tag resolution or +Content S06/S13 admission. Logical reservations are not RSS or caller work caps. diff --git a/Tools/TransformSelectionQualification/Sources/TransformSelectionConsumer/Main.swift b/Tools/TransformSelectionQualification/Sources/TransformSelectionConsumer/Main.swift new file mode 100644 index 0000000..d6a214d --- /dev/null +++ b/Tools/TransformSelectionQualification/Sources/TransformSelectionConsumer/Main.swift @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONTransform + +func selectionGuideExample() throws { + let definition = try JSONTransformProgramDefinition.prepare( + source: ".[] | select($values[.].enabled)", variables: ["values"]).result + let plan = try definition.compile().result + let row = try JSON.parse(#"{"enabled":true,"amount":9007199254740993}"#) + let report = try plan.withSelection(population: [row, row]) { scope in + var positions: [JSONTransformSelectionPosition] = [] + while let position = try scope.next() { positions.append(position) } + return positions + } + let ordinals = try report.result.map { try report.completion.ordinal(of: $0) } + precondition(ordinals == [0, 1]) + precondition(report.completion.populationCount == 2) + precondition(report.completion.outputCount == 2) +} +private func run(_ source: String, _ population: [JSON], limits: JSONTransformSelectionLimits = .init()) throws -> JSONTransformSelectionReport<[JSONTransformSelectionPosition]> { + try JSONTransformProgram.compile(source, variables: ["values"]).withSelection(population: population, limits: limits) { s in + var positions: [JSONTransformSelectionPosition] = [] + while let p = try s.next() { positions.append(p) }; return positions + } +} +private enum ConsumerError: Error { case provider } +@main enum Consumer { + static func main() throws { + var witnesses: [String] = [] + try selectionGuideExample(); witnesses.append("compiled-guide") + let ordered = try run("1,0,1", [42,42]) + precondition(ordered.result.map(\.ordinal) == [1,0,1]); witnesses.append("order-repeats") + let empty = try run("empty", []) + precondition(empty.completion.outputCount == 0 && empty.completion.populationCount == 0); witnesses.append("empty-eof") + do { + _ = try JSONTransformProgram.compile("try (0,null) catch 0", variables: ["values"]).withSelection(population: [42]) { s in + let first = try s.next(); precondition(first?.ordinal == 0) + do { _ = try s.next(); preconditionFailure("Invalid position accepted") } + catch let e as JSONTransformSelectionFailure { precondition(e == .invalidPosition) } + } + preconditionFailure("Caught boundary error certified") + } catch let e as JSONTransformSelectionFailure { precondition(e == .invalidPosition) } + witnesses.append("late-invalid") + let other = try run("0", [42,42]) + do { _ = try other.completion.ordinal(of: ordered.result[0]); preconditionFailure("Foreign position accepted") } + catch let e as JSONTransformSelectionFailure { precondition(e == .foreignPosition) } + witnesses.append("foreign-owner") + let parameterized = try JSONTransformProgram.compile(".[] | select($values[.] >= $minimum)", variables: ["values","minimum"]) + let selection = try parameterized.withSelection(population: [1,2,3], bindings: ["minimum":2]) { s in + var ids: [UInt64] = []; while let p = try s.next() { ids.append(p.ordinal) }; return ids + } + precondition(selection.result == [1,2]); witnesses.append("bindings") + var escaped: JSONTransformSelectionScope? + var position: JSONTransformSelectionPosition? + var completion: JSONTransformSelectionCompletion? + do { + let report = try JSONTransformProgram.compile("0", variables: ["values"]).withSelection(population: [.string(String(repeating:"x",count:8192))]) { s in + escaped = s; position = try s.next(); let eof = try s.next(); precondition(eof == nil) + } + completion = report.completion + } + let owner = escaped! + let retained = try withExtendedLifetime(position) { try owner.report()[.retainedBytes] } + precondition(retained > 8192); position = nil + let identityOnly = try withExtendedLifetime(completion) { try owner.report()[.retainedBytes] } + precondition(identityOnly > 0 && identityOnly < 8192); completion = nil + let settled = try owner.report(); precondition(settled[.retainedBytes] == 0) + do { _ = try owner.next(); preconditionFailure("Escaped scope resumed") } + catch let e as JSONTransformFailure { precondition(e.code == "scopeClosed") } + witnesses.append("terminal-refunds") + var calls = 0 + do { + _ = try JSONTransformProgram.compile("try input catch 0", variables: ["values"]).withSelection(population: [42], source: { calls += 1; throw ConsumerError.provider }) { s in + for _ in 0..<2 { + do { _ = try s.next(); preconditionFailure("Provider error swallowed") } + catch let e as JSONTransformFailure { precondition(e.category == .provider) } + } + } + preconditionFailure("Provider error certified") + } catch let e as JSONTransformFailure { precondition(e.category == .provider) } + precondition(calls == 1); witnesses.append("provider-terminal") + let measured = try run(".[]", [1,2,3]) + var limits = JSONTransformSelectionLimits(); limits.transform[.workUnits] = measured.completion.statistics[.workUnits] + _ = try run(".[]", [1,2,3], limits: limits) + limits.transform[.workUnits] -= 1 + do { _ = try run(".[]", [1,2,3], limits: limits); preconditionFailure("Budget reset") } + catch let e as JSONTransformFailure { precondition(e.category == .resource) } + witnesses.append("cumulative-work") + var failed: JSONTransformSelectionScope? + do { + _ = try JSONTransformProgram.compile("0", variables: ["values"]).withSelection(population: [42]) { s in + failed = s; s.cancel() + do { _ = try s.next(); preconditionFailure("Cancel ignored") } + catch let e as JSONTransformFailure { precondition(e.category == .cancelled) } + } + preconditionFailure("Cancelled invocation certified") + } catch let e as JSONTransformFailure { precondition(e.category == .cancelled) } + let terminal = try failed!.report(); precondition(terminal[.retainedBytes] == 0) + witnesses.append("cancellation") + precondition(witnesses.count == 10) + print("TRANSFORM_SELECTION passed witnesses=10") + } +}