From 07ed28095dde5e8063570aa6a2436039b5bd43ca Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:35:51 +0100 Subject: [PATCH 1/2] Add cumulative inspection batches and immutable read-only stores --- AGENTS.md | 19 +- CHANGELOG.md | 17 + CONTRIBUTING.md | 9 + Documentation/Architecture/API.md | 13 + .../Architecture/ExtractionManifest.json | 11 + .../BoundedWorkflows1.8.md | 63 ++++ .../SOURCE-AUTHORITY.md | 9 + Documentation/Qualification/Release1.8.md | 96 ++++++ Documentation/Qualification/ReleaseLines.md | 10 + .../Qualification/TestDiscovery.json | 36 +- Documentation/README.md | 7 + Documentation/ReleaseRoadmap.md | 10 + Examples/BoundedWorkflows/Package.swift | 13 + .../Sources/BoundedWorkflows/Main.swift | 50 +++ README.md | 78 +++-- Scripts/build_documentation_site.py | 6 +- Scripts/ci.py | 50 ++- Scripts/qualify_store.py | 2 +- Scripts/qualify_structural_workflows.py | 77 +++++ Scripts/test_qualification_harness.py | 37 ++- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- Sources/JSONCLIEngine/CLIReleaseVersion.swift | 2 +- .../Documentation.docc/BoundedWorkflows.md | 73 +++++ .../Documentation.docc/JSONInspection.md | 13 + .../JSONInspection/JSONInspectionBatch.swift | 153 +++++++++ .../Store/JSONConfiguredStore.swift | 133 ++++++-- .../Store/JSONStoreProfile.swift | 256 +++++++++++---- .../Store/JSONStoreQueries.swift | 234 +++++++++++++ .../JSONNavigation/JSONTraversalContext.swift | 15 + Tests/JSONCLITests/IntegrationTests.swift | 2 +- Tests/JSONInspectionTests/WorkflowTests.swift | 309 ++++++++++++++++++ 31 files changed, 1647 insertions(+), 158 deletions(-) create mode 100644 Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md create mode 100644 Documentation/Qualification/Release1.8.md create mode 100644 Examples/BoundedWorkflows/Package.swift create mode 100644 Examples/BoundedWorkflows/Sources/BoundedWorkflows/Main.swift create mode 100644 Scripts/qualify_structural_workflows.py create mode 100644 Sources/JSONInspection/Documentation.docc/BoundedWorkflows.md create mode 100644 Sources/JSONInspection/JSONInspectionBatch.swift create mode 100644 Sources/JSONInspection/Store/JSONStoreQueries.swift create mode 100644 Tests/JSONInspectionTests/WorkflowTests.swift diff --git a/AGENTS.md b/AGENTS.md index b0ca32d..88f7f62 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,10 +50,13 @@ 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe6eb8..4f4f047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## Unreleased — 1.8.0-dev + +- 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..ba2262c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,12 @@ +# 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. 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..55b2bcd 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -1,3 +1,16 @@ +# 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). +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/StructuralIntelligence/BoundedWorkflows1.8.md b/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md new file mode 100644 index 0000000..a22601d --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md @@ -0,0 +1,63 @@ +# 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. 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/Release1.8.md b/Documentation/Qualification/Release1.8.md new file mode 100644 index 0000000..9768c20 --- /dev/null +++ b/Documentation/Qualification/Release1.8.md @@ -0,0 +1,96 @@ +# 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/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index ee42d25..646ae2f 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 190, + "JSONInspectionTests": 214, "JSONTransformTests": 23, "JSONDSLTests": 47 }, @@ -37,7 +37,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 10, + "JSONInspectionTests": 11, "JSONTransformTests": 1, "JSONDSLTests": 1 }, @@ -84,7 +84,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 190, + "JSONInspectionTests": 214, "JSONTransformTests": 23, "JSONDSLTests": 47 }, @@ -99,7 +99,7 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 57, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 190, + "JSONInspectionTests": 214, "JSONTransformTests": 23, "JSONDSLTests": 47 }, @@ -115,7 +115,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 2, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 10, + "JSONInspectionTests": 11, "JSONTransformTests": 1, "JSONDSLTests": 1 }, @@ -148,5 +148,31 @@ "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()" ] } 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..62eaba2 --- /dev/null +++ b/Examples/BoundedWorkflows/Package.swift @@ -0,0 +1,13 @@ +// 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: "JSONNavigation", package: "Swift-JSON"), + .product(name: "JSONInspection", package: "Swift-JSON") + ])], swiftLanguageModes: [.v6] +) diff --git a/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Main.swift b/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Main.swift new file mode 100644 index 0000000..0d436bb --- /dev/null +++ b/Examples/BoundedWorkflows/Sources/BoundedWorkflows/Main.swift @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONNavigation +import JSONInspection + +@main struct BoundedWorkflows { + static func main() throws { + func witness(_ id: String, _ success: Bool) { + precondition(success, id); print("WORKFLOW_CASE \(id) passed") + } + let document = try JSONParser().parseDocument(Data(#"{"collections":{"u":[{"id":"é","amount":9007199254740993,"next":{"collection":"u","id":"e\u0301"}},{"id":"e\u0301","next":{"collection":"u","id":"é"}}]},"nothing":null}"#.utf8)) + let paths: [[JSONLocationComponent]] = [[.key("collections"), .key("u"), .index(0), .key("amount")], [.key("nothing")], [.key("nothing")]] + let batch = try JSONInspectionBatch(document: document).values(at: paths) + witness("batch-exact-order", batch.result == [.number(try JSONNumber(parsing: "9007199254740993")), .null, .null]) + do { _ = try JSONInspectionBatch(document: document).values(at: [[],[.key("missing")]]); preconditionFailure() } + catch let error as JSONAccessFailure { witness("missing-is-error", error.cause.code == .missingMember && error.path == [.key("missing")]) } + do { _ = try JSONInspectionBatch(document: document).values(at: paths, maximumRequests: 2); preconditionFailure() } + catch let error as JSONInspectionBatchFailure { witness("batch-capacity", error == .requestLimitExceeded(limit: 2, actual: 3)) } + let snapshot = try JSONStructuralSnapshotBuilder(document: document).build().result + witness("snapshot-root", try snapshot.values(for: [0,0]).result == [document.root, document.root]) + let profile = JSONStoreProfile(version: .v1, collectionsAt: [.key("collections")], collections: [ + .init(name: "u", recordsAt: [.key("u")], idKey: "id", referenceFields: [ + .init(key: "next", presence: .required, nullable: false)])], references: .strict(marker: .v1, validateTargets: true)) + let built = try JSONStoreBuilder(document: document, profile: profile).build(), store = built.result + witness("checked-construction", store.recordCount == 2 && store.references.count == 2 && built.statistics[.nodes] > 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])") + } +} diff --git a/README.md b/README.md index 4137295..cd0e3d6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ # Swift-JSON -This is **Swift-JSON 1.7.0 source prepared for release**. Version metadata identifies -the source; the [official releases](https://github.com/ThorFuchs/Swift-JSON/releases) -establish publication. At preparation, latest stable remains **1.6.1**. No tag or -publication is implied by this checkout. The source combines exact Navigation and -Inspection projections/graphs, JSONTransform and optional JSONValueDSL/JSONSchemaDSL. -It has twelve library products plus the CLI and thirteen DocC catalogs including -the shared, non-product JSONDSLCore reference. - -See the [combined release checklist](Documentation/Qualification/Release1.7Preparation.md), -[release notes](Documentation/ReleaseNotes/1.7.0.md) and -[1.7–2.0 roadmap](Documentation/ReleaseRoadmap.md). Every final candidate claim -requires source-bound receipts; predecessor green checks alone do not qualify it. +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. +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. See the [release-line matrix](Documentation/Qualification/ReleaseLines.md) for exact source scopes and publication boundaries. Version metadata identifies source; the @@ -34,24 +32,15 @@ Maintained by [Thorsten Fuchs](https://github.com/ThorFuchs). Swift-JSON follows semantic versioning. Use a released version and retain `Package.resolved` for reproducible builds; `main` tracks ongoing development. -**1.6.1** is the latest stable maintenance release: eight libraries plus CLI, -eight DocC catalogs and the unchanged 1.6 API/runtime. The optional verified schema -examples, structural traversal, structured HTML previews and bounded composition -were introduced in 1.6.0. Exact JSON and offline schema validation remain the -semantic authorities. -The published 1.6.1 CLI reports `1.6.1`; this prepared source reports `1.7.0`. -Both use a deterministic source-content digest. -The [published 1.6.1 release](https://github.com/ThorFuchs/Swift-JSON/releases/tag/1.6.1) -is pinned to `e431f157ececc0cf3d061192420b0e36a9062f83`. Use its matching eight-catalog -DocC and qualification assets for the stable reference; see the -[maintenance qualification notes](Documentation/Qualification/Release1.6.1.md). -Historical N0/N1 plans and later candidates are distinguished in the -[source authority map](Documentation/Architecture/StructuralIntelligence/SOURCE-AUTHORITY.md). - -This development source includes opt-in Inspection, Transform and authored DSLs: -twelve libraries plus the CLI, thirteen products in total. Inspection, Transform and DSLs are absent from stable 1.6 releases. Pin a reviewed -unreleased source commit to use those products; the compatible stable installation -requirement below does not supply them. Integration into main does not publish 1.7.0. +**1.7.0** is the latest stable release: twelve libraries plus CLI and thirteen +DocC catalogs. Inspection, Transform and the authored DSL products are included. +The 1.6.1 maintenance line retains its eight-library API separately. Published +1.7.0 reports `1.7.0`; this feature source reports `1.8.0-dev`. Both expose a +source-content digest distinct from the Git commit. Use exact version pins and +matching release assets when reproducing a published edition. + +The new batch/store APIs require the 1.8 feature source. No package product, +dependency, platform minimum or existing JSON/JSONSchema authority changes. The 1.5.1 maintenance repairs remain included: best-effort schema diagnostics, the complete 1.2.1 regression suites and Git-free qualification. See the @@ -752,7 +741,7 @@ can have restricted descendant traversal; structural rectangular width is not numeric matrix or schema admission. See the [compiled evidence guide](Sources/JSONInspection/Documentation.docc/ArrayEvidence.md), [public consumer](Tools/InspectionQualification) and [accounting contract](Documentation/Architecture/StructuralIntelligence/Inspection.md). -## Complete source-attached structural views (unreleased) +## Complete source-attached structural views (introduced in 1.7) Opt-in JSONInspection in this candidate combines array observations with a complete parent-linked snapshot, selected physical tree views and strict sparse @@ -767,7 +756,7 @@ Every construction has one cumulative finite ledger. Each later catalogue/cell/ location/value/span lookup is an explicitly separate full-source-admitted operation. Ordinals are snapshot-local trusted positions, not durable identity. Logical retention is not RSS; G4 controlled performance remains HOLD, with no speed claim. -These additions are absent from published 1.6.1 and do not publish 1.7.0. +These additions were absent from published 1.6.1 and are included in published 1.7.0. The current development branch also contains package-private structural set and exact numeric matrix experiments. They add no public API; see the @@ -782,7 +771,7 @@ 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 remains explicitly deferred to NAV13. Stable 1.6.1 is unchanged. +The 1.8 candidate adds aggregate value reads; set/matrix algebra remains outside scope. Stable 1.6.1 is unchanged. NAV11 experiments with a package-only, exact source-backed directed simple graph inside JSONInspection. Graph construction and BFS are not public API and are not @@ -821,3 +810,26 @@ for source-bound gates, the explicit allocation-instrumentation exception and platform limits. A path or revision consumer is preparation evidence; exact 1.7.0 tag consumption is a postauthorization check. This product is absent from stable 1.6.1 and must pass qualification on its final versioned source before publication. + +## 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. diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index d17ad71..9bad227 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -20,8 +20,8 @@ REPOSITORY = 'https://github.com/ThorFuchs/Swift-JSON' BASE_PATH = '/Swift-JSON' SITE_URL = 'https://thorfuchs.github.io' + BASE_PATH + '/' -SOURCE_VERSION = '1.7.0' -STABLE_VERSION = '1.6.1' +SOURCE_VERSION = '1.8.0-dev' +STABLE_VERSION = '1.7.0' GUIDES = {'JSON': ('Exact values and native coding', 'exactvaluesandcoding'), 'JSONSchema': ('Offline schema validation', 'offlinevalidation'), 'JSONSchemaExamples': ('Verified exact candidates', 'verifiedcandidates'), @@ -115,7 +115,7 @@ def landing(commit):

Exact JSON. Clear contracts.

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

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

-

Development main includes JSONInspection, JSONTransform and the optional authored DSLs; this source is prepared as 1.7.0. Stable {STABLE_VERSION} has eight library catalogs; use its matching compiled DocC assets or tagged source documentation.

+

Development main includes JSONInspection, JSONTransform and the optional authored DSLs; this source is unreleased 1.8.0-dev with bounded batch/store work. Stable {STABLE_VERSION} has twelve library catalogs plus the shared JSONDSLCore reference; use its matching compiled DocC assets or tagged source documentation.

{''.join(cards)}

Start with the contract

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

diff --git a/Scripts/ci.py b/Scripts/ci.py index 707114f..0b7cef7 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -8,6 +8,7 @@ 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 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 +27,7 @@ SHARED_TESTS += '|graphG30ConcurrentViewsHaveIndependentHealth|graphGP14ConcurrentQueries' SHARED_TESTS += '|dslSharedFinalizationAndPlansAreInvocationOwned' SHARED_TESTS += '|storeR06TerminalQuotasNeverPublishAndImmutableConcurrentHealth' +SHARED_TESTS += '|workflowSharedImmutablePlansHaveIndependentBatchAndStoreHealth' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -34,6 +36,26 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + 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 +80,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 +91,7 @@ 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_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')], @@ -229,6 +252,7 @@ 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')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -279,6 +303,7 @@ 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') 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()) @@ -291,7 +316,7 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 - if 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 +331,18 @@ 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 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 not prepared_test and not store_test and not workflow_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: @@ -388,6 +419,9 @@ 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_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 +465,12 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=26' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=28' in log.splitlines() 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=['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 +482,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 ('composition', 'inspection', 'structural-workflows', 'structural-workflows-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_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/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 4662219..d766277 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -28,6 +28,33 @@ def store_markers(): 'STORE_RUNTIME R%02d passed' % n for n in range(1,11)]) class HarnessTests(unittest.TestCase): + 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 +180,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,12 +194,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 57) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONInspectionTests'], 190) + self.assertEqual(policy[key]['JSONInspectionTests'], 214) self.assertEqual(policy[key]['JSONIntegrationTests'], 34) 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]['JSONInspectionTests'], 11) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): @@ -364,8 +391,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=28')[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', 'HARNESS_TESTS passed cases=26', '']: 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..8af8093 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:43023670d6664373702741d960b010494d86149aaa181b4eeb9d6ff13701d60e" 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/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..ad9572a 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -57,3 +57,16 @@ 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/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) +} From 5e27899230262a8d61cf0a5fd7d71fed18e105ac Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:16:05 +0100 Subject: [PATCH 2/2] Qualify cross-product workflows and bounded structural capacity --- CHANGELOG.md | 6 + CONTRIBUTING.md | 5 +- .../BoundedWorkflows1.8.md | 17 ++ Documentation/Qualification/Release1.8.md | 64 ++++++ .../StructuralWorkflows1.8Matrix.json | 88 ++++++++ .../Qualification/TestDiscovery.json | 66 +++++- Examples/BoundedWorkflows/Package.swift | 3 + Examples/BoundedWorkflows/README.md | 50 +++++ .../Sources/BoundedWorkflows/Acceptance.swift | 57 +++++ .../Sources/BoundedWorkflows/Capacity.swift | 157 +++++++++++++ .../Sources/BoundedWorkflows/Main.swift | 7 + README.md | 7 + Scripts/ci.py | 44 +++- Scripts/qualify_structural_acceptance.py | 134 +++++++++++ Scripts/test_qualification_harness.py | 56 ++++- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../BoundedWorkflowRecipes.md | 67 ++++++ .../Documentation.docc/JSONInspection.md | 1 + .../CompositionContractTests.swift | 4 +- .../PublicWorkflowTests.swift | 4 +- .../StagedImportTests.swift | 4 +- .../StructuralAcceptanceTests.swift | 209 ++++++++++++++++++ 22 files changed, 1034 insertions(+), 18 deletions(-) create mode 100644 Documentation/Qualification/StructuralWorkflows1.8Matrix.json create mode 100644 Examples/BoundedWorkflows/README.md create mode 100644 Examples/BoundedWorkflows/Sources/BoundedWorkflows/Acceptance.swift create mode 100644 Examples/BoundedWorkflows/Sources/BoundedWorkflows/Capacity.swift create mode 100644 Scripts/qualify_structural_acceptance.py create mode 100644 Sources/JSONInspection/Documentation.docc/BoundedWorkflowRecipes.md create mode 100644 Tests/JSONIntegrationTests/StructuralAcceptanceTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4f047..630926e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased — 1.8.0-dev +- 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba2262c..9171704 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,10 @@ 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. Run +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. diff --git a/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md b/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md index a22601d..77c6890 100644 --- a/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md +++ b/Documentation/Architecture/StructuralIntelligence/BoundedWorkflows1.8.md @@ -61,3 +61,20 @@ cases. Keep partial profiling/G4 HOLD and the1.7-only P4 exception historical; 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/Qualification/Release1.8.md b/Documentation/Qualification/Release1.8.md index 9768c20..daff11a 100644 --- a/Documentation/Qualification/Release1.8.md +++ b/Documentation/Qualification/Release1.8.md @@ -1,3 +1,67 @@ +# 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 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 646ae2f..3251689 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -6,7 +6,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 34, + "JSONIntegrationTests": 43, "JSONHTMLTests": 97, "JSONBridgesTests": 10, "JSONCLITests": 88, @@ -31,7 +31,7 @@ "JSONSchemaTests": 3, "JSONRegexRuntimeTests": 1, "JSONStreamingTests": 16, - "JSONIntegrationTests": 11, + "JSONIntegrationTests": 12, "JSONHTMLTests": 5, "JSONBridgesTests": 1, "JSONCLITests": 1, @@ -78,7 +78,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 34, + "JSONIntegrationTests": 43, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONCLITests": 88, @@ -94,7 +94,7 @@ "JSONRegexRuntimeTests": 16, "JSONStreamingTests": 30, "JSONPatchTests": 16, - "JSONIntegrationTests": 34, + "JSONIntegrationTests": 43, "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONNavigationTests": 57, @@ -109,7 +109,7 @@ "JSONSchemaTests": 3, "JSONRegexRuntimeTests": 1, "JSONStreamingTests": 16, - "JSONIntegrationTests": 11, + "JSONIntegrationTests": 12, "JSONHTMLTests": 4, "JSONBridgesTests": 1, "JSONCLITests": 1, @@ -174,5 +174,61 @@ "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()" ] } diff --git a/Examples/BoundedWorkflows/Package.swift b/Examples/BoundedWorkflows/Package.swift index 62eaba2..6fb8b27 100644 --- a/Examples/BoundedWorkflows/Package.swift +++ b/Examples/BoundedWorkflows/Package.swift @@ -7,6 +7,9 @@ let package = Package( 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..= 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/test_qualification_harness.py b/Scripts/test_qualification_harness.py index d766277..254b3e7 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -28,6 +28,54 @@ def store_markers(): 'STORE_RUNTIME R%02d passed' % n for n in range(1,11)]) class HarnessTests(unittest.TestCase): + 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) @@ -195,12 +243,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel self.assertEqual(policy[key]['JSONNavigationTests'], 57) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) self.assertEqual(policy[key]['JSONInspectionTests'], 214) - self.assertEqual(policy[key]['JSONIntegrationTests'], 34) + 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'], 11) - self.assertEqual(policy[key]['JSONIntegrationTests'], 11) + self.assertEqual(policy[key]['JSONIntegrationTests'], 12) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): names = sorted(PROJECTION_FUNCTIONS) @@ -391,8 +439,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=28')[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', 'HARNESS_TESTS passed cases=26', '']: + self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=30')[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', '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 8af8093..2c10613 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:43023670d6664373702741d960b010494d86149aaa181b4eeb9d6ff13701d60e" + package static let revision = "sha256:d29a80776f26331bf75b98c24d5df1cf5b69ecfbc4d9e39426d393e23c02c3ec" package static let kind = "source-sha256" } 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/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md index ad9572a..405526a 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -61,6 +61,7 @@ or numeric matrix. Sampling does not exempt any source from admission. ### Bounded structural workflows (1.8 candidate) - +- - ``JSONInspectionBatch`` - ``JSONInspectionBatchFailure`` - ``JSONStoreBuilder`` 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) +}