diff --git a/AGENTS.md b/AGENTS.md index 8ae3d86..4f87be1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,13 +4,14 @@ Swift-JSON requires Swift tools/compiler 6.4+, Swift language mode 6 and macOS/SDK 27+. Read README.md, CONTRIBUTING.md and the relevant contracts in Documentation/Architecture before changing behavior. -- Keep nine public products: JSON, JSONSchema, JSONSchemaExamples, JSONStreaming, - JSONPatch, JSONHTML, JSONBridges, JSONNavigation and the swift-json executable. +- Keep ten candidate products: JSON, JSONSchema, JSONSchemaExamples, JSONStreaming, + JSONPatch, JSONHTML, JSONBridges, JSONNavigation, JSONInspection and the swift-json executable. JSONSchemaExamples is opt-in and depends on JSON and JSONSchema only. JSONFilterRuntime and JSONCLIEngine are package-private. JSONHTML and opt-in JSONBridges depend on JSON only; core never re-exports the bridges. The approved CLI and E2 additions preserve historical product contracts. - JSONNavigation is an opt-in product candidate depending on JSON only; merge/release remain human-gated. + JSONNavigation depends on JSON only. Opt-in JSONInspection depends on JSON and + JSONNavigation; its candidate promotion remains human-gated. Core re-exports neither. JSONRegexRuntime is package-private. Root package dependencies remain empty. - Preserve exact bounded decimals, decoded Unicode scalar identity, ordered objects with order-independent equality, one strict UTF-8 grammar, native diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index e0e2939..95a8694 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -17,7 +17,7 @@ "blob_sha": "25cb8eab982ed0fc41e5f1dde5abeab18113f55e", "destination": "Sources/JSON/Value/JSON.swift", "disposition": "Six enum case concepts selectively adapted; equality/hash traversal and exact object/key storage rewritten.", - "attribution": "Original Thor Fuchs 2024\u20132026 header retained; Reference LICENSE and NOTICE preserved.", + "attribution": "Original Thor Fuchs 2024–2026 header retained; Reference LICENSE and NOTICE preserved.", "validation": [ "Tests/JSONTests/ValueTests.swift", "Tests/JSONTests/PresenceTests.swift" @@ -163,7 +163,7 @@ "blob_sha": null, "destination": "Sources/JSONRegexRuntime/RegexUnicodeTables.swift", "disposition": "407 immutable tables generated by original deterministic Python generator from fixed Unicode data; no generated Regress table copied. Regeneration compares bytes and provenance.", - "attribution": "\u00a9 2020 Unicode, Inc.; Unicode-3.0 copyright/permission notice retained in Licenses/Unicode-3.0.txt.", + "attribution": "© 2020 Unicode, Inc.; Unicode-3.0 copyright/permission notice retained in Licenses/Unicode-3.0.txt.", "validation": "Byte-equal regeneration and native exact-alias/GC/binary/Script/Script_Extensions/IdentifierName vectors", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" }, @@ -174,7 +174,7 @@ "blob_sha": null, "destination": "Sources/JSONSchema/Formats/JSONIDNAData.swift and Tests/Fixtures/Unicode/NormalizationTest-13.0.0.txt", "disposition": "Original generator derives RFC5892/5893, NFC and UTS46 nontransitional STD3 tables; normalization fixture copied byte-for-byte. No external engine or system Unicode authority.", - "attribution": "\u00a9 2019\u20132020 Unicode, Inc.; Unicode-3.0 notice preserved in Licenses/Unicode-3.0.txt.", + "attribution": "© 2019–2020 Unicode, Inc.; Unicode-3.0 notice preserved in Licenses/Unicode-3.0.txt.", "validation": "18853 normalization cases (94265 native comparisons); 874 independent optional format cases; byte-equal regeneration", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" }, @@ -284,7 +284,8 @@ "JSONHTML typed annotations, iterative exact-path binding, static/interactive attachment and coherent JSONDocument span presentation are original Apache-2.0 code. No schema engine, third-party renderer, source archive or new dependency is adapted.", "Structured forms/cards, finite prefix/preview staging, closed inline scanner, atomic mini-math MathML emitter, canonical PNG/JPEG header admission and line/media-bounded PDF pagination are original Apache-2.0 code. The approved structured concept informed the fixed CSS/JS; no demo Site content or external parser/renderer/codec engine is distributed.", "Experimental synchronous composition facade and its monotone external reservations are original Apache-2.0 implementations reusing native meter, admission, codec, Pointer and URI helpers. Contract tests concretize a maintainer-provided research scaffold; private packet evidence is excluded from source distribution. No third-party semantic engine or dependency is adapted. Compiler substitution fixtures materialize maintainer-provided R1 public-interface review cases, including exact work, retention and caught-failure restoration probes.", - "Experimental N0/N1 JSONNavigation is original Apache-2.0 Swift: iterative traversal and explicit overlap policy reuse unchanged core read-scope/admission/path/meter authorities. No historical navigation/query algorithm or additional dependency is adapted. Three maintainer-provided synthetic S1 JSON fixtures are copied unchanged; their packet and file SHA-256 pins are in Tests/JSONNavigationTests/Fixtures/Provenance.json. Public product promotion remains gated." + "Experimental N0/N1 JSONNavigation is original Apache-2.0 Swift: iterative traversal and explicit overlap policy reuse unchanged core read-scope/admission/path/meter authorities. No historical navigation/query algorithm or additional dependency is adapted. Three maintainer-provided synthetic S1 JSON fixtures are copied unchanged; their packet and file SHA-256 pins are in Tests/JSONNavigationTests/Fixtures/Provenance.json. Public product promotion remains gated.", + "Experimental exclusive traversal reducer, evidence-only array inspector and I01-I17 synthetic tests are original Apache-2.0 Swift. They reuse the original core read scope, exact key comparison and one cumulative meter; no additional semantic engine, upstream code or dependency is adapted. JSONInspection has no public product and is isolated from Navigation promotion." ], "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies.", "original_notes": [ @@ -297,7 +298,8 @@ "1.5.1 restores 126 original Apache-2.0 test functions and qualification tools from Swift-JSON 1.2.1 (e8b687f822c577311156b13bf1d15c2d5620b84e), with file hashes and stable function identities in Qualification/Release1.5.1/Restoration.json. The diagnostic omission and Git-free provenance repairs are restored original code; current seven-product harness reconciliation and deterministic CLI identity are original contributions. No new upstream adaptation or dependency.", "1.6 S3 G7 seeded grammar, scalar-key permutation tests, public replay consumer and strict cardinality/matrix harness are original Apache-2.0 contributions. The 64-schema authored fixture has SHA-256 cb55604183a6db7df5fa968db283e7abeae794277d2b0cef033b0bdb92e615cb. Independent python-jsonschema 4.26.0 is research qualification only, with exact numeric binding; no Python runtime dependency or copied validator/engine is distributed.", "1.6 S3 G8 materializes the unchanged synthetic corpus per caller access, replacing process-lifetime stored JSON payloads exposed by Linux ASan/LSan. The public process-lifetime consumer and strengthened exact/value-copy test are original Apache-2.0 qualification code; no suppression, runtime fallback, new dependency or corpus/digest change is introduced.", - "Composition reference-load fixtures, serial descriptor staging, public-only adapter, capacity/terminal tests and strict CI receipt checks are original Apache-2.0 code. Generated JSON graphs introduce no upstream protocol fixtures, engine or dependency. Core public APIs, finite profiles and accounting weights are unchanged." + "Composition reference-load fixtures, serial descriptor staging, public-only adapter, capacity/terminal tests and strict CI receipt checks are original Apache-2.0 code. Generated JSON graphs introduce no upstream protocol fixtures, engine or dependency. Core public APIs, finite profiles and accounting weights are unchanged.", + "NAV5 public array-evidence packaging, external consumer and DocC are original Apache-2.0 code. Existing exact shape evidence, exclusive reducer, weights and I01-I17 semantics are unchanged; no upstream engine or dependency is adapted. Promotion is a draft candidate, not a released product." ], "jsonhtml_export_original_contributions": { "license": "Apache-2.0", diff --git a/Documentation/Architecture/StructuralIntelligence/A0Recovery.md b/Documentation/Architecture/StructuralIntelligence/A0Recovery.md new file mode 100644 index 0000000..3944cc4 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/A0Recovery.md @@ -0,0 +1,54 @@ +# Experimental exclusive traversal reducer + +The original borrowed noncopyable callback at e11f93f is blocked: an escaping +capture aborts Swift 6.4 code generation in SILGenCleanup in Debug and Release. +Type checking alone accepts it and cannot establish ownership safety. + +The isolated recovery first tried an inout capability and Copyable reducer. +Explicit closure capture, consuming return, storage and copying +were rejected normally, but returning a bound nonmutating method still aborted. +That form is rejected. A standard-library-only reduced reproducer preserves the +failure. Nonescapable lifetime attributes also require experimental features on +the supported compiler; no flags or unsupported lifetime attributes are adopted. + +The selected experiment uses a package-only `JSONTraversalOwnedReducer` with +mutating observation/finalization and an `inout JSONTraversalContext`. All exposed +capability operations are mutating: they require exclusive access to the one +invocation's mutable ledger, and Swift forbids turning them into bound method +values. The scope field is private; initialization is internal to Navigation. +Reducer state and Result remain Copyable, so neither can own the noncopyable +capability. No general borrowed owned-callback entry point is exposed. + +`JSONTraversal.inspectOwned(scope:options:reducer:)` calls the existing traversal +executor, which creates exactly one original JSONReadScope and admission. The +context only delegates to that scope. No meter, admission, limit default, cursor +or mutable scope reference is returned. Owned errors latch on the original +health; finalization preserves the original failure even after a caught error +or attempted replacement. Scope invalidation runs on every exit. + +At the isolated 31879e8 checkpoint, actual full Debug/Release `swiftc -c` passed +two positives and eighteen qualified ordinary negative diagnostics: return, +escaping closure, Sendable capture, forge, long-lived storage, bound method, +global storage, copy and external-package access per configuration. Final review +found that the two additional Task cases had stopped on an unused-throwing-task +warning, not the required ownership diagnostic. That evidence gap is preserved. +The corrected probe explicitly discards the Task handle and requires an actual +escaping-inout primary diagnostic. Every negative now matches the primary error +text, never echoed source or an unrelated warning. The complete matrix remains +two positive and twenty negative code-generation cases. All 47 +Navigation functions passed Debug/Release, strict compilation and the public +consumer/DocC lane. The eight new functions prove every-counter no-op parity, +same-ledger work/finalization, inclusive quotas, output retention, terminal error +priority, cancellation, exact paths/spans and post-invalidation root/state release. +These are architecture-specific proofs, not a claim that the compiler bug is fixed. + +The new capability is experimental and absent from the independent public +Navigation integration. Subsequent inspection work must reuse it without adding +an inner composition scope. All actual command/source/toolchain receipts remain +bound to their own checkpoint; later source needs its own applicable qualification. + +Qualification provenance follows the established checkout_head helper: source +archives and Git-ownership refusals record null Git identity, with exact source +input hashes. They still execute the complete positive/negative codegen matrix. +No global safe.directory or repository-trust setting is changed. The initial +archive/container harness failures are preserved as failures in the return. diff --git a/Documentation/Architecture/StructuralIntelligence/Inspection.md b/Documentation/Architecture/StructuralIntelligence/Inspection.md new file mode 100644 index 0000000..6fe3912 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/Inspection.md @@ -0,0 +1,73 @@ +# Array evidence public-product candidate + +JSONInspection is an opt-in library depending on JSON and JSONNavigation. Core +never re-exports it. This isolated promotion candidate is unmerged and unreleased. +It reports observations rather than table/schema admission. Navigation public +declarations and semantics are unchanged. See [public surface review](N2Promotion.md) +and the [DocC guide](../../../Sources/JSONInspection/Documentation.docc/ArrayEvidence.md). + +`JSONShapeInspector(document:limits:)` captures one canonical source. +`inspectArray(at:sampling:maximumColumns:includeSourceSpan:)` selects an existing +array using exact JSONLocationComponent keys/indices and returns the existing +JSONOperationReport envelope. JSONShapeSampling supports complete rows or +`prefix(maximumRows:)`. The column bound defaults to 100,000, inclusive. Negative +sampling or column configuration fails before admission, a deliberate new-API +precedence contract. Valid operations always admit the full captured root/source/map +once, even for a zero prefix, scalar selection error or unseen suffix. + +The [exclusive reducer](A0Recovery.md) runs observation and finalization inside +Navigation's original read scope and meter. No nested public operation, new +resource context, parsing, default reset or additional root admission occurs. +Selection and wrong-kind failures use core access errors. Column capacity, +resource exhaustion and cancellation throw and cannot publish partial evidence. + +## Evidence and coverage + +Row coverage and traversal coverage are independent. The walker explores the +selected array root and rows at depth one, and the reducer inspects object cells +directly. Row descendants are pruned; complete row evidence can therefore have +restricted traversal coverage. An incomplete positive prefix intentionally stops +at its final observed row. A zero prefix has no positive traits. A prefix reaching +the final row has complete **row** coverage, without certifying descendant shape. + +Reports retain source selection, optional coherent selected-array span, full-source +admission completion, total/examined row counts, six exact row kinds and deterministic +first-seen columns. Column present includes explicit null; explicitNull is separate; +missing uses observed object rows only. Nonobject rows contribute no missing cells. +Decoded scalar-distinct keys stay distinct and retain source insertion order. +No String hash/set, normalization, numeric projection or replacement JSON is used. + +Observed-all-objects and uniform-kind traits describe examined rows only. Empty +evidence yields nil traits. Array length range describes observed array rows; +rectangular width requires all examined rows to be arrays of that length. It says +nothing about numeric cells, a matrix, schema validity or an unexamined suffix. +Numbers remain canonical JSONNumber values in the unchanged source. Replacement +documents retain no stale original span. + +## Finite work and ownership + +All 49 core profiles and metric definitions remain unchanged. The inspector uses +their source, path, work, key, retention and cancellation limits. Its additional +inclusive column bound is finite by default. Logical overlay version 1 reserves +256 bytes for report/reducer summary, 192 per returned column, exact UTF-8 key +bytes, and core path-slot/key weights for returned selection. Reservations precede +collection growth. Output slots transfer from reducer to report; they remain +charged at handoff. These are declared logical weights requiring calibration, +not heap layout, allocation count or process RSS guarantees. + +Every row/member/column scan and finalization step charges the original meter; +key comparisons delegate to core scalar comparison, including inner cancellation. +Counters are bounded by admitted row/member counts and work before increments. +Column lookup can be quadratic. No indexing or speed claim is made. + +The seventeen I01–I17 Swift Testing functions cover empty, wrong-kind, missing, +complete/mixed/late-counterexample/zero-prefix rows, permutations, scalar key identity, +typed numeric paths, null/absence denominators, exact numbers/lexemes, ragged and +nonnumeric arrays, full-source admission, inclusive work/retention/column limits, +inner cancellation, source coherence and independent shared-source invocations. +`Scripts/ci.py --lane inspection` enforces exact discovery and compiler outcomes. +Current native/Linux/WASI/platform receipts must qualify final source; this document +does not turn an unexecuted lane into a pass. No N2 merge or release is authorized. + +Snapshots, persistent indexes, projections, retrieval, BFS, JSONPath, HTML/CLI +integration, schema inference and numeric matrices remain deferred. diff --git a/Documentation/Architecture/StructuralIntelligence/N2Promotion.md b/Documentation/Architecture/StructuralIntelligence/N2Promotion.md new file mode 100644 index 0000000..65da777 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/N2Promotion.md @@ -0,0 +1,58 @@ +# NAV5 N2 public-surface decision + +Source reviewed: 462f3ea33d462e00ed4fc241fc4fff1d1d0ff95c, descendant of fresh +main 7d72c8825982550f0ee87218136fda12d8bae13b. Status: accepted for an isolated +public-product candidate, subject to fresh qualification; no merge/release approval. + +Keep all existing public declarations in JSONShapeEvidence.swift and +JSONShapeInspector.swift, including internal-only setters and the selected-array +report. No facade, overload, alias or existing module declaration is added. The +inspector's only semantic delegates are JSONTraversal.inspectOwned and its original +JSONReadScope/JSONWorkMeter. Exact key comparison and optional source-span lookup +use that capability. The package-private reducer and capability remain inaccessible +to external packages. No competing API was found in the current source graph; +independent DSL/composition/schema branches remain outside this candidate. + +## Integer proof + +Swift's supported platforms use a signed Int no wider than UInt64. Array/object +counts are nonnegative Int. Mandatory admission bounds the entire canonical graph +before observation. For a selected array, 0 <= wanted <= total <= Int.max and each +row is observed once in increasing index order. Before examined += 1, examined < +wanted <= total, so the increment cannot exceed Int.max. Each six-kind row bucket +is at most UInt64(examined) <= UInt64(Int.max), strictly below UInt64.max. + +Canonical JSONObject contains each decoded-scalar key once. Each column's present +count increases at most once per observed object row. Thus explicitNull <= present +<= observedObjectRows <= UInt64(examined), and each column kind bucket <= present. +The missing subtraction observedObjectRows - present and nonobject subtraction +UInt64(examined) - observedObjectRows cannot underflow. First-row ordinal is the +preincrement examined value; column indices come from bounded array indices. +No multiplication or unchecked count addition is used by this reducer. The core +meter performs checked work/retention arithmetic before growth and increments; +operational overflow remains a typed resource failure. This proof depends on the +current single physical selection, depth-one traversal, and canonical duplicate +rejection; broader scopes would require a new audit. + +## Frozen meaning and costs + +Complete row coverage does not imply complete descendant traversal. Prefix traits +describe only examined rows. Source admission is complete even for prefix zero. +Present includes explicit null; missing excludes nonobject rows. First-seen columns +follow exact source insertion order. Rectangular width is structural, never numeric +matrix or schema admission. Empty evidence has no positive inferred trait. + +Negative prefix/column configuration precedes admission. Maximum columns defaults +to 100,000 inclusive. Overlay v1 stays 256 fixed logical bytes, 192 per returned +column plus exact UTF-8 key bytes and core selection-path weights. No new resource +profile or fresh meter is introduced. Logical retention is not RSS. The metered +linear column search can be quadratic in total columns/members. + +## Required proof binding + +Retain I01-I17 without changing identities/meaning. Add a separate external SwiftPM +consumer with exact outcomes, compiled DocC examples, core-only import rejection, +and package-capability access rejection. Native, Linux, WASI, SDK, safety, discovery, +49 metrics/126 identities, source archive and unmodified 2-positive/20-negative +full-codegen ownership matrix must bind to the final source. Unexecuted lanes remain +pending, not historical successes promoted to fresh ones. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV6Reconciliation.md b/Documentation/Architecture/StructuralIntelligence/NAV6Reconciliation.md new file mode 100644 index 0000000..dd018d1 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV6Reconciliation.md @@ -0,0 +1,23 @@ +# NAV6 source reconciliation + +The isolated candidate combines original N2 8313e06 with protected main c79d48c +(schema examples, PR #39). Original #41 and #42 commits remain preserved. No +Inspection or Navigation semantic source changes occur in this reconciliation. +The manifest contains ten candidate products: nine libraries and the CLI. Root +dependencies remain empty. JSONSchemaExamples and its 98 functions remain intact; +Navigation has 47 functions and Inspection retains all seventeen I01–I17 cases. +The combined root inventory is 720 native, 704 Linux and 616 WASI, with 325/288 +parameter invocations unchanged. Shared-state counts are 48 native and 47 Linux. + +Conflicts in manifest consumers, docs catalogue/site lists, CI and discovery are +resolved by retaining both increments. The harness regression fixture expecting +39 Navigation functions is raised to 47 and adds exact Inspection identities; +missing/wrong-module/changed-parameter counterexamples remain required. The CLI +source digest is regenerated from the finalized manifest and Sources. All 49 +metric probes, 126 maintenance identities, composition qualification and schema +example ownership/matrix/lifetime checks remain mandatory. + +These are requirements, not execution claims. Source-bound receipts determine +qualification. This candidate grants no merge, tag or release authority. N2's +public API and prefix/complete row coverage are unchanged; N3 promotion remains +an independent child experiment. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV7Reconciliation.md b/Documentation/Architecture/StructuralIntelligence/NAV7Reconciliation.md new file mode 100644 index 0000000..92e323f --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV7Reconciliation.md @@ -0,0 +1,27 @@ +# NAV7 current-main integration proposal + +This isolated N2 proposal replays the reviewed NAV6 N2 delta (37d84e8 relative to +c79d48c) onto main f975ac1, preserving #39 schema examples, #44 DocC navigation +indexes and #45 version 1.6.0 release metadata. Original #41/#42/#43 branches and +NAV6 candidates remain unchanged. No N3 snapshot or P1 projection source enters +this N2 branch; all merge, ready, tag and release decisions remain owner gates. + +JSONInspection retains44 authored declarations and JSONNavigation56. Native, +Linux and WASI test identities and the original exclusive-inout reducer are +unchanged. All49 capacity probes and126 maintenance identities remain enforced. +Current main has eight public libraries plus CLI; this proposal adds Inspection +as the ninth library. Root dependencies remain empty. + +DocC integration retains all four #44 files and twelve guide-index regression +cases. The existing source-to-HTML guide routes, per-product byte-identical +navigator aliases and verification functions remain. Only Inspection's catalogue, +guide entry and descriptions are added. Archive catalogue count is derived from +the actual product list. The independent site regression and complete native docs +lane must pass on this source; historical status is comparison evidence only. + +README preserves main's release notes while explicitly labeling Inspection an +unreleased candidate. CLI release version remains1.6.0; its generated source hash +is refreshed from this exact manifest and source. Native, Linux, safety, external +consumer, compiler-negative, WASI/WasmKit, mobile SDK and Git-free receipts are +bound to the final candidate in the private NAV7 return. This document does not +claim results before execution or change any historical receipt. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 36770c0..02bcb04 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,10 @@ +# NAV6 integrated inspection candidate + +Adds Inspection to schema-example-enabled main: ten products, nine libraries, +720 native / 704 Linux / 616 WASI functions, 48 native / 47 Linux shared functions. +All 49 caps, 126 maintenance identities and schema/composition lanes remain. +These are discovery expectations; receipts establish execution. + # Combined S3 / composition / Navigation qualification The current inventory requires 695 Mac / 679 Linux / 591 WASI functions, @@ -12,6 +19,28 @@ See [G8 reconciliation](../Architecture/Release1.6/G8Integration.md). The following sections preserve their original increment histories. +# NAV5 public inspection candidate + +The candidate adds JSONInspection as the ninth product (eight libraries plus CLI). +Its existing I01-I17 identities, root discovery, 49 cap probes and 126 restored +maintenance identities are unchanged. Native/Linux invoke the external inspection +consumer and compiled guide/negative import proofs through qualify_inspection.py. +Eight public DocC catalogues include Inspection. The original 2-positive/20-negative +owned-reducer code-generation matrix remains unchanged. Candidate qualification +must bind to current source; historical counts below retain their original scope. + +## Experimental inspection branch + +This isolated N2 branch retains the eight-product Navigation integration and adds +an experimental JSONInspection target, with no product. A0 adds eight Navigation +functions (47 total); I01–I17 add 17 inspection functions. Current roots are 622 +native, 606 Linux and 518 WASI; parameters remain 325/288. Shared-state expectations +are 44 native/43 Linux, including I17. All 49 caps and 126 maintenance identities +remain mandatory. Native/Linux add the full-codegen ownership matrix: two positive +and twenty ordinary negative outcomes, with signals/assertions rejected. The +inspection lane enforces six commands including Debug/Release, ASan and strict +memory compilation. Earlier sections retain their original source identities. + # Native CI ## Reconciled Navigation and composition increment diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index 188c960..c8da8bb 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -1,7 +1,7 @@ # Building and maintaining the DocC catalogs Swift source comments are the API documentation authority. Native compiler symbol -graphs supply the declarations and links for eight Swift DocC catalogs. Each catalog +graphs supply the declarations and links for nine candidate Swift DocC catalogs. Each catalog combines the product reference with curated Topics and a focused usage guide. See the [Swift DocC documentation](https://www.swift.org/documentation/docc/). @@ -19,7 +19,7 @@ python3 Scripts/ci.py --lane docs The lane records native preflight, extracts fresh public symbol graphs, requires summaries and named-parameter documentation on every authored public declaration, checks README Swift fences against their executable consumer source, runs that -consumer, and compiles all eight DocC catalogs with warnings as errors. Zero public +consumer, and compiles all nine candidate DocC catalogs with warnings as errors. Zero public symbols, missing comments, stale examples or a failed catalog fail the lane. The lane also executes the standalone schema-examples consumer (15 preserved cases and two @@ -39,12 +39,13 @@ statistics; the consumer imports no private meters. | `.build/handoff/docc/JSONPatch.doccarchive` | Patch API and choosing/applying patches guide | | `.build/handoff/docc/JSONBridges.doccarchive` | Opt-in native numeric conversion and fidelity guide | | `.build/handoff/docc/JSONNavigation.doccarchive` | Opt-in exact structural traversal and coverage | +| `.build/handoff/docc/JSONInspection.doccarchive` | Observed array evidence and row coverage | | `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | | `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | | `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | -| `.build/handoff/docc/compiled-catalogs.tar.gz` | All eight compiled catalogs with their exact native filenames | +| `.build/handoff/docc/compiled-catalogs.tar.gz` | All nine candidate compiled catalogs with their exact native filenames | | `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Independently verifiable hashes for every archived catalog file | -| `.build/handoff/pages/site/` | Static site, landing page and all eight references under the project base path | +| `.build/handoff/pages/site/` | Static site, landing page and all nine candidate references under the project base path | | `.build/handoff/pages/receipt.json` | Native transformations, input/output hashes and route/asset verification | Open a `.doccarchive` in Xcode for the compiled reference. To preview a source @@ -73,7 +74,7 @@ The manifest and receipt preserve file-level and archive-level digests. The docs lane uses native `docc process-archive transform-for-static-hosting` to prepare each qualified archive under `/Swift-JSON/`. A shared landing -page links all eight references and guides. The build verifies product/guide routes, +page links all nine candidate references and guides. The build verifies product/guide routes, local HTML links and assets, rejects private-path/credential markers and symlinks, and records a file manifest. Only the generated site is packaged for Pages; source graphs, command logs and private maintainer records are not website content. @@ -90,7 +91,7 @@ native docs lane; run them separately with `python3 -B Scripts/test_documentatio Browser acceptance must also open a guide directly, confirm the sidebar has links without a fetching error, follow a sidebar symbol link and return to a guide. -Repeat direct guide navigation for all eight catalogs and the five schema-example +Repeat direct guide navigation for all nine candidate catalogs and the five schema-example guides. A successful article render or static HTML-link check alone does not prove that the client fetched its navigator index. @@ -161,6 +162,6 @@ swift run --package-path Examples/SchemaExamplesConsumer SchemaExamplesConsumer Review the generated DocumentationExamples.swift diff after editing a guide. Missing, extra, empty or foreign-import snippets and source drift fail qualification. The `#sourceLocation` directives map compiler errors to the authored guide. The root -README has fourteen executable Swift fences in its separate seven-library consumer; +README has fifteen executable Swift fences in its separate eight-library consumer; installation fragments remain manifest examples, checked by real package manifests. Native DocC checks symbol links and the site verifies the new limits-guide route. diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 7814f17..c55f9b9 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -10,8 +10,9 @@ "JSONHTMLTests": 97, "JSONBridgesTests": 10, "JSONCLITests": 88, - "JSONNavigationTests": 39, - "JSONSchemaExamplesTests": 98 + "JSONNavigationTests": 47, + "JSONSchemaExamplesTests": 98, + "JSONInspectionTests": 17 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -33,7 +34,8 @@ "JSONBridgesTests": 1, "JSONCLITests": 1, "JSONNavigationTests": 1, - "JSONSchemaExamplesTests": 4 + "JSONSchemaExamplesTests": 4, + "JSONInspectionTests": 1 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -59,7 +61,8 @@ "JSONFilterRuntime", "JSONCLIEngine", "JSONCLI", - "JSONNavigation" + "JSONNavigation", + "JSONInspection" ], "portable_root_functions": { "JSONTests": 190, @@ -71,8 +74,9 @@ "JSONHTMLTests": 81, "JSONBridgesTests": 10, "JSONCLITests": 88, - "JSONNavigationTests": 39, - "JSONSchemaExamplesTests": 98 + "JSONNavigationTests": 47, + "JSONSchemaExamplesTests": 98, + "JSONInspectionTests": 17 }, "wasi_root_functions": { "JSONTests": 190, @@ -83,8 +87,9 @@ "JSONIntegrationTests": 32, "JSONHTMLTests": 81, "JSONBridgesTests": 10, - "JSONNavigationTests": 39, - "JSONSchemaExamplesTests": 98 + "JSONNavigationTests": 47, + "JSONSchemaExamplesTests": 98, + "JSONInspectionTests": 17 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -97,7 +102,8 @@ "JSONBridgesTests": 1, "JSONCLITests": 1, "JSONNavigationTests": 1, - "JSONSchemaExamplesTests": 4 + "JSONSchemaExamplesTests": 4, + "JSONInspectionTests": 1 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/Examples/Consumer/Package.swift b/Examples/Consumer/Package.swift index c5bb8c9..8920e87 100644 --- a/Examples/Consumer/Package.swift +++ b/Examples/Consumer/Package.swift @@ -14,7 +14,8 @@ let package = Package( .product(name: "JSONStreaming", package: "Swift-JSON"), .product(name: "JSONPatch", package: "Swift-JSON"), .product(name: "JSONHTML", package: "Swift-JSON"), - .product(name: "JSONBridges", package: "Swift-JSON") + .product(name: "JSONBridges", package: "Swift-JSON"), + .product(name: "JSONInspection", package: "Swift-JSON") ])], swiftLanguageModes: [.v6] ) diff --git a/Examples/Consumer/Sources/Consumer/READMEExamples.swift b/Examples/Consumer/Sources/Consumer/READMEExamples.swift index de4f45f..421fae6 100644 --- a/Examples/Consumer/Sources/Consumer/READMEExamples.swift +++ b/Examples/Consumer/Sources/Consumer/READMEExamples.swift @@ -8,6 +8,7 @@ import JSONSchemaExamples import JSONPatch import JSONStreaming import JSONHTML +import JSONInspection func inspectArray(_ source: Source) async throws -> UInt64 where Source.Element == JSONByteChunk { @@ -135,4 +136,12 @@ func runREADMEExamples() throws { let syntaxHTML = try JSONHTMLRenderer(options: .init( layout: .syntax, presentation: .formatted) ).render(valueForHTML) + + let shapeSource = try JSON.parse(#"[{"name":"Ada","note":null},{"name":"Grace"},false]"#) + let shapeReport = try JSONShapeInspector(document: JSONDocument(root: shapeSource)) + .inspectArray(sampling: .prefix(maximumRows: 2)) + precondition(shapeReport.result.rowCoverage == .prefix(examined: 2, total: 3)) + precondition(shapeReport.result.firstSeenColumns[1].explicitNull == 1) + precondition(shapeReport.result.firstSeenColumns[1].missing == 1) + precondition(shapeReport.statistics[.workUnits] > 0) } diff --git a/Package.swift b/Package.swift index 6e9d455..a2cd93c 100644 --- a/Package.swift +++ b/Package.swift @@ -14,12 +14,15 @@ let package = Package( .library(name: "JSONHTML", targets: ["JSONHTML"]), .library(name: "JSONBridges", targets: ["JSONBridges"]), .library(name: "JSONNavigation", targets: ["JSONNavigation"]), + .library(name: "JSONInspection", targets: ["JSONInspection"]), .executable(name: "swift-json", targets: ["JSONCLI"]) ], targets: [ .target(name: "JSON"), // Opt-in N1 product candidate; promotion does not change core navigation semantics. .target(name: "JSONNavigation", dependencies: ["JSON"]), + .target(name: "JSONInspection", dependencies: ["JSON", "JSONNavigation"]), + .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"]), .testTarget(name: "JSONNavigationTests", dependencies: ["JSONNavigation", "JSON"], exclude: ["Fixtures"]), .target(name: "JSONBridges", dependencies: ["JSON"]), .testTarget(name: "JSONIntegrationTests", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch"]), diff --git a/README.md b/README.md index 3f6314e..73e3f74 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ examples and structural traversal, structured HTML previews and bounded native composition. Exact JSON and offline schema validation remain the semantic authorities. The CLI reports `1.6.0` and a deterministic source-content digest. +This isolated candidate adds unreleased opt-in JSONInspection: nine libraries plus +the CLI. Inspection remains a draft proposal and is not part of version 1.6.0. + The 1.5.1 maintenance repairs remain included: best-effort schema diagnostics, the complete 1.2.1 regression suites and Git-free qualification. See the [1.6 qualification and compatibility notes](Documentation/Qualification/Release1.6.0.md) @@ -78,6 +81,7 @@ Choose the products your application needs: | **swift-json** | Bounded command-line JSON workflows (available since 1.5.0) | | **JSONBridges** | Opt-in native integer, Double and Foundation Decimal conversions (available in the 1.4.0 tag and 1.5.0 release) | | **JSONNavigation** | Opt-in exact many-node DFS, physical/virtual scopes, finite metadata and explicit coverage, introduced in 1.6.0 | +| **JSONInspection** | Unreleased candidate: observed array kinds, exact first-seen columns and explicit complete/prefix row coverage | | **JSONPatch** | Staged RFC 6902 JSON Patch and RFC 7396 Merge Patch | `JSONSchema`, `JSONStreaming`, `JSONPatch`, `JSONHTML` and opt-in `JSONBridges` build on `JSON`. The package-private @@ -125,7 +129,7 @@ Then add products to your target dependencies: Add `JSONStreaming`, `JSONPatch`, `JSONHTML`, `JSONBridges`, `JSONNavigation` or `JSONSchemaExamples` in the same way when needed. A local checkout can use `.package(path: "../Swift-JSON")`. The requirement above accepts compatible 1.x -releases. See the [seven-library consumer](Examples/Consumer) for a complete buildable +releases. See the [eight-library consumer](Examples/Consumer) for a complete buildable manifest. The website tracks `main`; release assets include compiled DocC catalogues for the exact released source. @@ -508,11 +512,11 @@ Matching compiled catalogs accompany the GitHub release when published. Known web-client limitations and direct-link/reload guidance are recorded in the [qualification notes](Documentation/Qualification/Release1.6.0.md#documentation). -The [documentation index](Documentation/README.md) links the behavior contracts. The eight DocC catalogs +The [documentation index](Documentation/README.md) links the behavior contracts. The nine candidate DocC catalogs provide public API references, focused guides, parameter descriptions and failure contracts, compiled from the native Swift public symbol graphs. -Build all eight catalogs on a supported native host: +Build all nine candidate catalogs on a supported native host: ```sh python3 Scripts/ci.py --lane docs @@ -631,4 +635,28 @@ Add the JSONNavigation product and import it explicitly. Existing one-node reads remain in JSON. Full source admission precedes traversal option validation. See the [compiled traversal guide](Sources/JSONNavigation/Documentation.docc/Traversal.md) and [real external SwiftPM consumer](Tools/NavigationQualification). Navigation is -opt-in in 1.6.0; one-node lookup remains in JSON. Deeper N2 inspection is deferred. +opt-in in 1.6.0; one-node lookup remains in JSON. N2 inspection is proposed separately below. + +## Inspect array evidence + +Add the opt-in JSONInspection product and import it explicitly. Inspect a complete +array or a deterministic prefix without changing its exact values. Column counts +use observed object rows: present includes null, while missing excludes nonobject +rows. Prefix observations make no statement about an unseen suffix. + +```swift +import JSONInspection + +let shapeSource = try JSON.parse(#"[{"name":"Ada","note":null},{"name":"Grace"},false]"#) +let shapeReport = try JSONShapeInspector(document: JSONDocument(root: shapeSource)) + .inspectArray(sampling: .prefix(maximumRows: 2)) +precondition(shapeReport.result.rowCoverage == .prefix(examined: 2, total: 3)) +precondition(shapeReport.result.firstSeenColumns[1].explicitNull == 1) +precondition(shapeReport.result.firstSeenColumns[1].missing == 1) +precondition(shapeReport.statistics[.workUnits] > 0) +``` + +Full source admission still includes the unobserved suffix. Complete row evidence +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). diff --git a/Scripts/build_documentation.py b/Scripts/build_documentation.py index 674f95f..f359050 100644 --- a/Scripts/build_documentation.py +++ b/Scripts/build_documentation.py @@ -61,7 +61,7 @@ def package_catalogs(output): 'bytes': archive.stat().st_size, 'verified_files': len(records), 'manifest': str(manifest.relative_to(ROOT)), 'manifest_sha256': hashlib.sha256(manifest.read_bytes()).hexdigest(), - 'policy': 'Eight native DocC archives; exact internal filenames, sorted entries and normalized tar/gzip metadata.'} + 'policy': f'{len(documentation_catalog.PRODUCTS)} native DocC archives; exact internal filenames, sorted entries and normalized tar/gzip metadata.'} def main(): diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index 90c06b2..04988df 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -27,7 +27,8 @@ 'JSONPatch': ('Choosing and applying patches', 'applyingpatches'), 'JSONHTML': ('Exact interactive HTML', 'interactiverendering'), 'JSONBridges': ('Opt-in native conversions', 'numericfidelity'), - 'JSONNavigation': ('Exact bounded structural traversal', 'traversal')} + 'JSONNavigation': ('Exact bounded structural traversal', 'traversal'), + 'JSONInspection': ('Observed array evidence', 'arrayevidence')} DESCRIPTIONS = { 'JSON': 'Exact values, strict UTF-8, native Codable, Pointer, presence and finite resource reports.', 'JSONSchema': 'Draft 2020-12, explicit offline registries, immutable plans, annotations and bounded regex.', @@ -36,7 +37,8 @@ 'JSONPatch': 'Transactional RFC 6902 JSON Patch and RFC 7396 Merge Patch with exact value semantics.', 'JSONHTML': 'Exact standalone HTML with offline disclosure/search and native-byte copy/download.', 'JSONBridges': 'Opt-in trusted integer construction and policy-explicit Double and Foundation Decimal projection.', - 'JSONNavigation': 'Exact typed DFS with physical and virtual scopes, explicit coverage and one cumulative budget.'} + 'JSONNavigation': 'Exact typed DFS with physical and virtual scopes, explicit coverage and one cumulative budget.', + 'JSONInspection': 'Complete or prefix-scoped array evidence, exact ordered columns and cumulative finite reports.'} class Links(HTMLParser): diff --git a/Scripts/ci.py b/Scripts/ci.py index fbdc1b4..4b341c6 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -13,6 +13,7 @@ SHARED_TESTS += '|qaStreaming|qaE2E(SharedPlan|RootImport|NDJSONImport|AwaitedSink|ExactWorkflow)' SHARED_TESTS += '|g1SharedPreparedGeneratorHasIndependentConcurrentInvocations|g3SharedTypedGeneratorHasIndependentConcurrentInvocations|g4SharedAdvancedGeneratorHasIndependentConcurrentInvocations|g5SharedPreparationSeparatesConcurrentFailureAndSuccessHealth' SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' +SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -21,6 +22,15 @@ def snapshot(): def commands(lane, output): # The qualification executable resolves output paths from its repository root. output = output.relative_to(ROOT) + if lane == 'inspection': + result = [['python3', '-B', 'Scripts/public_check.py'], + ['swift', 'test', '--filter', 'inspection'], + ['swift', 'test', '-c', 'release', '--filter', 'inspection'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection'], + ['swift', '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')]] + return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] if lane == 'schema-examples-qualification': return commands('schema-examples-advanced', ROOT / output) + [['python3', '-B', 'Scripts/qualify_schema_example_matrix.py', '--output', str(output / 'matrix')]] if lane == 'schema-examples-advanced': @@ -154,6 +164,8 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_distribution.py', '--output', str(output / 'distribution')], ['python3', '-B', 'Scripts/qualify_ergonomics.py'], ['python3', '-B', 'Scripts/qualify_navigation.py', '--output', str(output / 'navigation')], + ['python3', '-B', 'Scripts/qualify_inspection.py', '--output', str(output / 'inspection-consumer')], + ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -212,6 +224,10 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 + if 'inspection' in command: + expected = [DISCOVERY['root_functions']['JSONInspectionTests']] + if events is not None: + passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 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 @@ -249,12 +265,13 @@ def check_result(command, code, log, events=None): passed = data['observed'] == data['passed'] == expected and data['failed'] == data['policy'] == data['blocked'] == 0 if command == ['swift', 'package', 'dump-package'] and passed: d = json.loads(log) - passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'swift-json'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] - passed = passed and {p['name']: p['targets'] for p in d['products']} == dict({name: [name] for name in ['JSON', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation']}, **{'swift-json': ['JSONCLI']}) + passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONBridges', 'JSONHTML', 'JSONInspection', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'swift-json'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] + passed = passed and {p['name']: p['targets'] for p in d['products']} == dict({name: [name] for name in ['JSON', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation', 'JSONInspection']}, **{'swift-json': ['JSONCLI']}) targets = {t['name']: t for t in d['targets']} - passed = passed and set(targets) == {'JSON', 'JSONNavigation', 'JSONNavigationTests', 'JSONIntegrationTests', 'JSONBridges', 'JSONBridgesTests', 'JSONSchema', 'JSONSchemaExamples', 'JSONSchemaExamplesTests', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONBridges']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' + passed = passed and set(targets) == {'JSON', 'JSONNavigation', 'JSONNavigationTests', 'JSONInspection', 'JSONInspectionTests', 'JSONIntegrationTests', 'JSONBridges', 'JSONBridgesTests', 'JSONSchema', 'JSONSchemaExamples', 'JSONSchemaExamplesTests', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONBridges']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' passed = passed and targets['JSONNavigation']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONNavigationTests']['dependencies'] == [{'byName': ['JSONNavigation', None]}, {'byName': ['JSON', None]}] passed = passed and targets['JSONSchemaExamples']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONSchema', None]}] and targets['JSON']['dependencies'] == [] and targets['JSONSchema']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONRegexRuntime', None]}] + passed = passed and targets['JSONInspection']['dependencies'] == [{'byName': ['JSON', None]}, {'byName': ['JSONNavigation', None]}] and targets['JSONInspectionTests']['dependencies'] == [{'byName': ['JSONInspection', None]}, {'byName': ['JSONNavigation', None]}, {'byName': ['JSON', None]}] if command[:3] == ['python3', '-B', 'Scripts/qualify_schema_example_matrix.py']: passed = passed and 'SCHEMA_EXAMPLES_MATRIX passed schemas=64 certified=512 invalid=64 processes=2' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py']: @@ -279,6 +296,10 @@ def check_result(command, code, log, events=None): passed = passed and 'TESTING_EVENT_RECEIPTS passed cases=19' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_ergonomics.py']: passed = passed and 'ERGONOMICS_COMPILER passed commands=8 guide_fences=3' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: + passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_inspection.py']: + passed = passed and 'INSPECTION_COMPILER passed configurations=2 consumer_cases=12 address_cases=12 guide_fences=3 negative_probes=8' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation.py']: passed = passed and 'NAVIGATION_COMPILER passed configurations=2 consumer_cases=18 negative_probes=8' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/verify_html_assets.py']: @@ -293,7 +314,7 @@ def check_result(command, code, log, events=None): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['composition', 'navigation', '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=['composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) @@ -302,7 +323,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 == 'composition' and sys.platform == 'linux') else collect() + environment = collect_linux() if args.lane.startswith('linux') or (args.lane in ('composition', 'inspection') 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/documentation_catalog.py b/Scripts/documentation_catalog.py index abf396b..06c5267 100644 --- a/Scripts/documentation_catalog.py +++ b/Scripts/documentation_catalog.py @@ -7,7 +7,7 @@ import urllib.parse ROOT = pathlib.Path(__file__).resolve().parents[1] -PRODUCTS = ('JSON', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation') +PRODUCTS = ('JSON', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONBridges', 'JSONNavigation', 'JSONInspection') def public_symbols(graph_dir): diff --git a/Scripts/qualify_inspection.py b/Scripts/qualify_inspection.py new file mode 100644 index 0000000..adaf9be --- /dev/null +++ b/Scripts/qualify_inspection.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Qualify public inspection imports, semantics and executable DocC examples.""" +import argparse +import hashlib +import json +import pathlib +import re +import subprocess +import sys +from archive_candidate import files + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKS = {'coverage', 'observed-only', 'exact-keys', 'null-missing', 'exact-numbers', + 'one-admission', 'zero-prefix', 'capacity', 'access-fidelity', + 'source-coherence', 'inclusive-work', 'configuration-precedence'} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + output = ROOT / parser.parse_args().output + output.mkdir(parents=True, exist_ok=True) + inputs = [{'path': p.relative_to(ROOT).as_posix(), + 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()] + receipt = dict(status='running', source_inputs=inputs, commands=[], consumer_results={}) + + def save(): + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr + crash = result.returncode < 0 or any(x in text for x in + ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation']) + diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + passed = not crash and (result.returncode == 0 if expected is None else + result.returncode > 0 and any(expected in d for d in diagnostics)) + log = output / (name + '.log') + log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], + exit_code=result.returncode, status='passed' if passed else 'failed', compiler_crash=crash, + expected_diagnostic=expected, primary_diagnostics=diagnostics, + log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: + raise ValueError('Unexpected compiler/consumer outcome: ' + name) + return result.stdout + + def consumer(name, command): + text = run(name, command) + checks = re.findall(r'^INSPECTION_CHECK (\S+) passed$', text, re.M) + if len(checks) != 12 or set(checks) != CHECKS or 'INSPECTION_CONSUMER passed cases=12' not in text: + raise ValueError('Missing/duplicate inspection consumer checks: ' + name) + resource = re.search(r'^INSPECTION_RESOURCE work=(\d+) retained=(\d+) peak=(\d+) nodes=(\d+)$', text, re.M) + if resource is None or not all(int(x) > 0 for x in resource.groups()): + raise ValueError('Missing finite cumulative report: ' + name) + receipt['consumer_results'][name] = dict(cases=checks, work_units=int(resource[1]), + logical_retained_bytes=int(resource[2]), peak_logical_retained_bytes=int(resource[3]), + admitted_nodes=int(resource[4])) + save() + + try: + for config in ['debug', 'release']: + run(config + '-build', ['swift', 'build', '-c', config, '--target', 'JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift', 'build', '-c', config, '--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + objects = [] + for module in ['JSON', 'JSONNavigation', 'JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: + raise ValueError('Missing built objects: ' + module) + objects.extend(str(p) for p in selected) + common = ['swiftc', '-swift-version', '6', '-warnings-as-errors', '-package-name', + 'inspection_external_consumer', '-I', str(modules), + '-O' if config == 'release' else '-Onone'] + consumer(config, ['swift', 'run', '-c', config, '--package-path', + 'Tools/InspectionQualification', 'InspectionConsumer']) + guide = ROOT / 'Sources/JSONInspection/Documentation.docc/ArrayEvidence.md' + fences = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(fences) != 3: + raise ValueError('Changed three-fence guide inventory') + for index, fence in enumerate(fences): + file = output / ('guide-' + str(index) + '.swift') + file.write_text('import JSON\nimport JSONNavigation\nimport JSONInspection\n' + '@main struct Guide { static func main() throws {\n' + fence + '\n} }\n') + binary = output / ('guide-' + config + '-' + str(index)) + run(config + '-guide-' + str(index), common + ['-parse-as-library', str(file), *objects, '-o', str(binary)]) + run(config + '-guide-' + str(index) + '-run', [str(binary)]) + probes = [ + ('core-import-isolation', 'import JSON\nlet x = JSONShapeInspector.self\n', 'cannot find'), + ('capability-isolation', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find'), + ('reducer-isolation', 'import JSONNavigation\nlet x = JSONTraversalOwnedReducer.self\n', 'cannot find'), + ('evidence-is-readonly', 'import JSONInspection\nfunc change(_ x: inout JSONShapeKindCounts) { x.nulls = 7 }\n', 'setter is inaccessible'), + ] + for name, source, expected in probes: + file = output / (name + '.swift'); file.write_text(source) + run(config + '-' + name, common + ['-parse-as-library', '-c', str(file), + '-o', str(output / (config + '-' + name + '.o'))], expected) + consumer('address', ['swift', 'run', '-c', 'release', '--sanitize', 'address', + '--scratch-path', '.build/inspection-consumer-address', '--package-path', + 'Tools/InspectionQualification', 'InspectionConsumer']) + receipt['status'] = 'passed' + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + receipt.update(status='failed', error=str(error)) + save() + print('INSPECTION_COMPILER ' + receipt['status'] + + ' configurations=2 consumer_cases=12 address_cases=12 guide_fences=3 negative_probes=8') + return 0 if receipt['status'] == 'passed' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Scripts/qualify_navigation_reducer.py b/Scripts/qualify_navigation_reducer.py new file mode 100644 index 0000000..4d0f255 --- /dev/null +++ b/Scripts/qualify_navigation_reducer.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Experimental full-codegen capability lifetime matrix; a compiler crash fails.""" +import argparse,hashlib,json,os,pathlib,re,subprocess,sys +from archive_candidate import files +from qualify_release_1_1 import checkout_head +ROOT=pathlib.Path(__file__).resolve().parents[1];W=ROOT +parser=argparse.ArgumentParser();parser.add_argument('--output',required=True);args=parser.parse_args() +O=ROOT/args.output;O.mkdir(parents=True,exist_ok=True) +env=dict(os.environ) +identity=re.sub(r'[^a-zA-Z0-9_]', '_', ROOT.name.lower()) +if not identity or identity[0].isdigit(): identity='_'+identity +positive='''import JSON +import JSONNavigation +struct Reducer: JSONTraversalOwnedReducer { + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { try context.work(7); return .continue } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> UInt64 { try context.work(11); return summary.visitedCount } +} +func probe(_ walk: JSONTraversal) throws -> UInt64 { var reducer = Reducer(); return try walk.inspectOwned(reducer: &reducer).result } +''' +probes={ + 'positive':(positive,None), + 'return':('func illegal(_ context: inout JSONTraversalContext) -> JSONTraversalContext { consume context }','missing reinitialization of inout parameter'), + 'escape':('func illegal(_ context: inout JSONTraversalContext) -> () throws -> Void { { try context.checkpoint() } }',"escaping closure captures 'inout' parameter"), + 'send':('func illegal(_ context: inout JSONTraversalContext) -> @Sendable () throws -> Void { { try context.checkpoint() } }',"capture of 'context' with non-Sendable type"), + 'forge':('func illegal() throws { let scope = try JSONReadScope(.init(root: .null), limits: .balanced); _ = JSONTraversalContext(scope: scope) }','initializer is inaccessible'), + 'store':('final class Box { var value: JSONTraversalContext? }; func illegal(_ context: inout JSONTraversalContext) -> Box { let b = Box(); b.value = consume context; return b }','missing reinitialization of inout parameter'), + 'method-reference':('func illegal(_ context: inout JSONTraversalContext) -> () throws -> Void { context.checkpoint }',"cannot reference 'mutating' method as function value"), + 'task':('func illegal(_ context: inout JSONTraversalContext) { _ = Task { try context.checkpoint() } }',"escaping closure captures 'inout' parameter"), + 'global':('@MainActor var saved: JSONTraversalContext?; @MainActor func illegal(_ context: inout JSONTraversalContext) { saved = consume context }','missing reinitialization of inout parameter'), + 'copy':('func illegal(_ context: inout JSONTraversalContext) { let a = copy context; _ = a }',"'copy' cannot be applied to noncopyable types"), + 'external':('func illegal(_ context: inout JSONTraversalContext) throws { try context.checkpoint() }',"cannot find type 'JSONTraversalContext' in scope"), +} +# A source archive is intentionally outside Git; a container may also refuse +# repository ownership. Never borrow a parent checkout or alter Git trust/config. +# Exact input hashes remain authoritative and every compiler case still executes. +head=checkout_head(ROOT) +working_diff=None +if head is not None: + diff=subprocess.run(['git','diff','HEAD'],cwd=W,capture_output=True) + if diff.returncode == 0: working_diff=hashlib.sha256(diff.stdout).hexdigest() +inputs=[{'path':p.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()] +r={'status':'running','source_head':head,'working_diff_sha256':working_diff, + 'git_identity_status':'available' if head is not None else 'unavailable_or_not_root_checkout', + 'provenance_policy':'Only this root checkout; unavailable Git identity is null, never a parent SHA or a weakened safe.directory setting.', + 'source_inputs':inputs,'source_manifest_sha256':hashlib.sha256(json.dumps(inputs,sort_keys=True).encode()).hexdigest(), + 'commands':[]} +configs=['debug','release'] +for config in configs: + build=['swift','build','-c',config,'--target','JSONNavigation','-Xswiftc','-warnings-as-errors'] + p=subprocess.run(build,cwd=W,env=env,capture_output=True);(O/(config+'-build.log')).write_bytes(p.stdout+p.stderr) + if p.returncode: raise SystemExit(p.returncode) + for name,(source,expected) in probes.items(): + if name!='positive':source='import JSON\nimport JSONNavigation\n'+source+'\n' + file=O/(name+'.swift');file.write_text(source) + binpath=pathlib.Path(subprocess.check_output(['swift','build','-c',config,'--show-bin-path'],cwd=W,env=env,text=True).strip());mod=binpath/'Modules' if (binpath/'Modules').exists() else binpath;package='outside_consumer' if name=='external' else identity + cmd=['swiftc','-swift-version','6','-warnings-as-errors','-package-name',package,'-module-name','ReducerOwnershipProbe','-I',str(mod),'-O' if config=='release' else '-Onone','-parse-as-library','-c',str(file),'-o',str(O/(config+'-'+name+'.o'))] + p=subprocess.run(cmd,cwd=W,env=env,capture_output=True,timeout=60);data=p.stdout+p.stderr;log=O/(config+'-'+name+'.log');log.write_bytes(data) + crash=p.returncode<0 or any(t in data for t in [b'Stack dump:',b'Assertion failed',b'fatal error encountered during compilation',b'Found ownership error']) + # Match actual primary diagnostics, never echoed source text or an unrelated + # warning promoted by -warnings-as-errors (notably an unused throwing Task). + diagnostics=re.findall(r': error: ([^\n]+)',data.decode(errors='replace')) + passed=not crash and (p.returncode==0 if expected is None else p.returncode>0 and any(expected.lower() in d.lower() for d in diagnostics)) + r['commands'].append(dict(name=config+'-'+name,command=cmd,exit=p.returncode,source_sha256=hashlib.sha256(file.read_bytes()).hexdigest(),log=str(log.relative_to(ROOT)),log_sha256=hashlib.sha256(data).hexdigest(),compiler_crash=crash,expected=expected,primary_diagnostics=diagnostics,status='passed' if passed else 'failed')) + print(config,name,p.returncode,'PASS' if passed else 'FAIL',flush=True) +r['status']='passed' if all(c['status']=='passed' for c in r['commands']) else 'blocked' +(O/'receipt.json').write_text(json.dumps(r,indent=2)+'\n') + +print("REDUCER_CODEGEN "+r["status"]+" positive=2 negative=20") +raise SystemExit(0 if r["status"]=="passed" else 1) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 949484b..8089704 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -34,7 +34,8 @@ 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', 39, ['swift', 'test', '--filter', 'navigation']), + ('JSONNavigationTests', 47, ['swift', 'test', '--filter', 'navigation']), + ('JSONInspectionTests', 17, ['swift', 'test', '--filter', 'inspection']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, @@ -46,12 +47,14 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel self.assertFalse(check_result(command, 0, '', broken)[0]) policy = DISCOVERY for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): - self.assertEqual(policy[key]['JSONNavigationTests'], 39) + self.assertEqual(policy[key]['JSONNavigationTests'], 47) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) + self.assertEqual(policy[key]['JSONInspectionTests'], 17) self.assertEqual(policy[key]['JSONIntegrationTests'], 32) for key in ('shared_state_functions', 'portable_shared_state_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 1) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 4) + self.assertEqual(policy[key]['JSONInspectionTests'], 1) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def setUp(self): diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index ded0631..d5bd26a 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -68,7 +68,7 @@ def binary(scratch, config, name): if preamble and not re.fullmatch(r"Another instance of SwiftPM .*waiting until that process has finished execution\.\.\.", preamble): raise ValueError('Unexpected manifest preamble: ' + preamble) manifest = json.loads(manifest_text[start:]) - if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONBridges', 'JSONHTML', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'swift-json']: + if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONBridges', 'JSONHTML', 'JSONInspection', 'JSONNavigation', 'JSONPatch', 'JSONSchema', 'JSONSchemaExamples', 'JSONStreaming', 'swift-json']: raise ValueError('Changed public products/dependencies') def invoke(engine, path, mode='command', arguments=(), repo=True, resources=True): if engine == 'node': diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 53877e1..e13663e 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:1d8c854653435bb1055a13142514624f029d6782b003bcf43a62fc1eafba2401" + package static let revision = "sha256:f6d5bbb6deae7bf922c46b72f893412ef83461fa60fb83564db4d7f417587eb6" package static let kind = "source-sha256" } diff --git a/Sources/JSONInspection/Documentation.docc/ArrayEvidence.md b/Sources/JSONInspection/Documentation.docc/ArrayEvidence.md new file mode 100644 index 0000000..4c07bfe --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/ArrayEvidence.md @@ -0,0 +1,62 @@ +# Observe array evidence + +Select an existing array with exact typed components and distinguish source, +row and descendant coverage. + +## Examine a complete array + +```swift +let source = try JSON.parse(#"[{"x":null},{"y":1}]"#) +let report = try JSONShapeInspector(document: JSONDocument(root: source)).inspectArray() +precondition(report.result.rowCoverage == .complete) +precondition(report.result.traversal.completion == .restricted) +precondition(report.result.firstSeenColumns[0].present == 1) +precondition(report.result.firstSeenColumns[0].explicitNull == 1) +precondition(report.result.firstSeenColumns[0].missing == 1) +``` + +Rows are examined completely, while cell descendants are deliberately pruned. +Present includes explicit null. Missing uses observed object rows as denominator, +so scalar rows never create missing cells. Columns retain first-encounter source +order and decoded Unicode scalar identity, without Swift String normalization. + +## Sample without certifying a suffix + +```swift +let source = try JSON.parse(#"[{"x":1},{"x":2},false]"#) +let report = try JSONShapeInspector(document: JSONDocument(root: source)) + .inspectArray(sampling: .prefix(maximumRows: 2)) +precondition(report.result.rowCoverage == .prefix(examined: 2, total: 3)) +precondition(report.result.observedAllObjects == true) +precondition(report.result.traversal.completion == .visitorStopped) +precondition(report.result.sourceAdmissionComplete) +``` + +The full captured source is admitted once even for a zero prefix. Positive traits +describe only examined rows; the boolean suffix above disproves any whole-array +object claim. Empty evidence has nil positive traits. Structural rectangular width +says nothing about numeric cells, JSON Schema validity or projection admission. + +## Select and bound owned work + +```swift +let source = try JSON.parse(#"{"":[{"0":9007199254740993}]}"#) +let report = try JSONShapeInspector(document: JSONDocument(root: source)) + .inspectArray(at: [.key("")], maximumColumns: 1) +precondition(report.result.sourceSelection == [.key("")]) +precondition(report.statistics[.workUnits] > 0) +precondition(report.result.firstSeenColumns[0].key == JSONKey("0")) +``` + +Root [] differs from the empty key. Numeric keys differ from array indices. Missing +and wrong-kind selections throw core JSONAccessFailure. Negative sample/column +configuration fails before admission. The default column cap is 100,000 inclusive; +zero is valid until an observed object field would be added. Quota exhaustion, +overflow and cancellation throw, with no partial successful evidence. + +The inspector delegates to one original traversal read scope. Overlay v1 retains +256 logical bytes for summary, 192 per column plus exact UTF-8 key bytes, and core +path weights. Work includes row/member scans, exact key comparisons and finalization; +first-seen column lookup can be quadratic. Logical retained bytes are policy units, +not RSS or caller instruction quotas. Source spans require a coherent captured map; +a replacement root has no stale span. Every invocation owns independent mutable state. diff --git a/Sources/JSONInspection/Documentation.docc/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md new file mode 100644 index 0000000..41ab5b6 --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -0,0 +1,26 @@ +# ``JSONInspection`` + +Observe exact array evidence with explicit row coverage and one finite budget. + +## Overview + +Import JSONInspection separately from JSON. ``JSONShapeInspector`` inspects one +existing array in an immutable JSONDocument. It preserves exact keys and numbers, +and reports facts about examined rows. It does not infer a schema or admit a table +or numeric matrix. Sampling does not exempt any source from admission. + +## Topics + +### Inspect an array + +- +- ``JSONShapeInspector`` +- ``JSONShapeSampling`` +- ``JSONShapeArrayEvidence`` + +### Interpret the evidence + +- ``JSONShapeRowCoverage`` +- ``JSONShapeKindCounts`` +- ``JSONColumnObservation`` +- ``JSONShapeInspectionFailure`` diff --git a/Sources/JSONInspection/JSONShapeEvidence.swift b/Sources/JSONInspection/JSONShapeEvidence.swift new file mode 100644 index 0000000..67aa4b7 --- /dev/null +++ b/Sources/JSONInspection/JSONShapeEvidence.swift @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Deterministic row sampling, independent of mandatory full-source admission. +public enum JSONShapeSampling: Sendable { + /// Examine every existing row. + case complete + /// Examine at most the first nonnegative number of rows, in array order. + /// - Parameter maximumRows: Inclusive sample bound; zero establishes no positive trait. + case prefix(maximumRows: Int) +} + +/// Observed row coverage; this never certifies schema or descendant homogeneity. +public enum JSONShapeRowCoverage: Sendable, Equatable { + /// All array rows were examined, including a genuinely empty array. + case complete + /// Only the stated prefix was examined; no assertion describes its suffix. + /// - Parameters: + /// - examined: Number of observed rows. + /// - total: Number of rows in the admitted selected array. + case prefix(examined: Int, total: Int) +} + +/// Fixed six-kind counts. Missing is absence, never a seventh JSON value kind. +public struct JSONShapeKindCounts: Sendable, Equatable { + /// Object values observed. + public internal(set) var objects: UInt64 = 0 + /// Array values observed. + public internal(set) var arrays: UInt64 = 0 + /// Scalar-sequence strings observed. + public internal(set) var strings: UInt64 = 0 + /// Exact decimal values observed, without binary conversion. + public internal(set) var numbers: UInt64 = 0 + /// Boolean values observed. + public internal(set) var booleans: UInt64 = 0 + /// Explicit null values observed. + public internal(set) var nulls: UInt64 = 0 + + /// Returns a fixed kind's observed count. + /// - Parameter kind: One of the six canonical JSON kinds. + public subscript(kind: JSONTraversalKind) -> UInt64 { + switch kind { + case .object: objects; case .array: arrays; case .string: strings + case .number: numbers; case .boolean: booleans; case .null: nulls + } + } + internal mutating func record(_ value: JSON) { + switch value { + case .object: objects += 1; case .array: arrays += 1; case .string: strings += 1 + case .number: numbers += 1; case .boolean: booleans += 1; case .null: nulls += 1 + } + } +} + +/// An exact column seen in sampled object rows, in first-encounter insertion order. +public struct JSONColumnObservation: Sendable { + /// Decoded scalar identity; canonical-equivalent spellings remain separate keys. + public let key: JSONKey + /// Zero-based array row where this key first appeared. + public let firstRow: Int + /// Observed object rows containing this key, including explicit null. + public internal(set) var present: UInt64 = 0 + /// Present cells whose value is explicit JSON null. + public internal(set) var explicitNull: UInt64 = 0 + /// Observed object rows lacking this key; nonobject rows are excluded. + public internal(set) var missing: UInt64 = 0 + /// Kinds of present canonical cells; missing cells contribute no kind. + public internal(set) var kinds = JSONShapeKindCounts() +} + +/// Factual evidence about examined rows of one existing canonical array. +/// Prefix traits describe the prefix only. No field certifies a table, numeric +/// matrix, JSON Schema verdict, or unexamined suffix. +public struct JSONShapeArrayEvidence: Sendable { + /// Absolute typed location of the selected array in the captured document. + public let sourceSelection: [JSONLocationComponent] + /// Coherent original selected-array span when requested and available. + public let sourceSpan: JSONSourceSpan? + /// True on success: the entire captured root/source/map was admitted once. + public let sourceAdmissionComplete: Bool + /// Traversal coverage, separate from row coverage; row descendants are omitted. + public let traversal: JSONTraversalSummary + /// Existing number of rows, known from the admitted array. + public let totalRows: Int + /// Rows actually examined in increasing index order. + public let examinedRows: Int + /// Full or intentionally prefix-scoped row evidence. + public let rowCoverage: JSONShapeRowCoverage + /// Kinds of examined rows, without coercion. + public let rowKinds: JSONShapeKindCounts + /// Number of examined object rows; the column missing-count denominator. + public let observedObjectRows: UInt64 + /// Number of examined scalar, null or array rows. + public let observedNonObjectRows: UInt64 + /// Exact columns in first-seen order across examined object rows. + public let firstSeenColumns: [JSONColumnObservation] + /// Nil for no evidence; otherwise whether all examined rows are objects. + public let observedAllObjects: Bool? + /// One shared observed row kind, or nil for empty/mixed evidence. + public let observedUniformRowKind: JSONTraversalKind? + /// Minimum through maximum lengths among examined array rows, if any. + public let observedArrayLengthRange: ClosedRange? + /// Common width only when every examined row is an array of that length. + /// This says nothing about cell kinds, numeric conversion or an unseen suffix. + public let observedRectangularArrayWidth: Int? +} + +/// An explicit inspection capacity refusal, never successful partial evidence. +public enum JSONShapeInspectionFailure: Error, Sendable, Equatable { + /// The next first-seen key would exceed the inclusive column bound. + /// - Parameter limit: Configured nonnegative maximum column count. + case columnLimitExceeded(limit: Int) +} diff --git a/Sources/JSONInspection/JSONShapeInspector.swift b/Sources/JSONInspection/JSONShapeInspector.swift new file mode 100644 index 0000000..dc53297 --- /dev/null +++ b/Sources/JSONInspection/JSONShapeInspector.swift @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Read-only array evidence over the canonical traversal ledger. +/// Import the opt-in JSONInspection product. Each invocation admits the entire +/// captured source once and charges selection, classification and returned storage +/// cumulatively. Logical retained bytes describe package policy, not process RSS. +public struct JSONShapeInspector: Sendable { + /// Immutable source for every typed location and optional span. + public let document: JSONDocument + /// Finite core admission, work, cancellation and logical-retention policies. + public let limits: JSONLimits + /// Package qualification hook; no scope or meter is exposed. + package var checkpointObserver: (@Sendable (UInt64) -> Void)? + + /// Captures trusted immutable input; admission occurs when inspection starts. + /// - Parameters: + /// - document: Canonical source, optionally with coherent source mapping. + /// - limits: One finite profile for the whole inspection invocation. + public init(document: JSONDocument, limits: JSONLimits = .balanced) { + self.document = document; self.limits = limits + } + + /// Inspects an existing array without projecting or rewriting its values. + /// Negative sample/column configuration fails before source admission. Valid + /// sampling always admits the full captured source, including an unseen suffix. + /// - Parameters: + /// - at: Absolute exact key/index path; [] selects the root. + /// - sampling: Complete rows or a deterministic prefix, never inferred coverage. + /// - maximumColumns: Inclusive output-column bound, defaulting to 100,000. + /// - includeSourceSpan: Request a metered coherent selected-array value span. + /// - Returns: Observed-only evidence and the original cumulative operation report. + /// - Throws: Typed core selection/kind errors, invalid configuration, column + /// capacity, core resource exhaustion or cancellation; no partial report. + public func inspectArray(at: [JSONLocationComponent] = [], + sampling: JSONShapeSampling = .complete, + maximumColumns: Int = 100_000, + includeSourceSpan: Bool = false) + throws -> JSONOperationReport { + guard maximumColumns >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + if case .prefix(let maximumRows) = sampling, maximumRows < 0 { + throw JSONFailure(.configuration, code: .invalidLimits) + } + var reducer = ArrayReducer(sampling: sampling, maximumColumns: maximumColumns, + includeSourceSpan: includeSourceSpan) + var traversal = JSONTraversal(document: document, limits: limits) + traversal.checkpointObserver = checkpointObserver + return try traversal.inspectOwned(scope: .subtree(at: at), options: .init(depthLimit: 1), + reducer: &reducer) + } +} + +/// One fixed summary (256 logical bytes), 192 per column plus its key bytes, +/// and existing core path slots. Reserve before append; no String hash authority. +private struct ArrayReducer: JSONTraversalOwnedReducer { + let sampling: JSONShapeSampling + let maximumColumns: Int + let includeSourceSpan: Bool + var selection: [JSONLocationComponent] = [] + var span: JSONSourceSpan? + var total = 0, wanted = 0, examined = 0 + var kinds = JSONShapeKindCounts() + var columns: [JSONColumnObservation] = [] + var minimumArrayLength: Int?, maximumArrayLength: Int? + + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + if hit.relativeDepth == 0 { + total = try context.arrayCount(value, at: hit.absoluteLocation) + try context.retain(256) + selection = try context.retainLocation(hit.absoluteLocation) + if includeSourceSpan { span = try context.sourceSpan(at: hit.absoluteLocation) } + switch sampling { case .complete: wanted = total; case .prefix(let maximum): wanted = min(maximum, total) } + return wanted == 0 ? .skipDescendants : .continue + } + try context.work() + let row = examined; examined += 1; kinds.record(value) + switch value { + case .object(let object): + for ordinal in 0.. JSONShapeArrayEvidence { + for index in columns.indices { + try context.work() + columns[index].missing = kinds.objects - columns[index].present + } + try context.work(6) + var uniform: JSONTraversalKind? + for kind in JSONTraversalKind.allCases where examined > 0 && kinds[kind] == UInt64(examined) { + uniform = kind + } + let range: ClosedRange? + if let minimumArrayLength, let maximumArrayLength { range = minimumArrayLength...maximumArrayLength } + else { range = nil } + let rectangular = examined > 0 && kinds.arrays == UInt64(examined) + && minimumArrayLength == maximumArrayLength ? minimumArrayLength : nil + // Transfer the charged output slots; the reducer keeps no second vector. + let outputColumns = columns; columns = [] + let outputSelection = selection; selection = [] + return .init(sourceSelection: outputSelection, sourceSpan: span, sourceAdmissionComplete: true, + traversal: summary, totalRows: total, examinedRows: examined, + rowCoverage: examined == total ? .complete : .prefix(examined: examined, total: total), + rowKinds: kinds, observedObjectRows: kinds.objects, + observedNonObjectRows: UInt64(examined) - kinds.objects, firstSeenColumns: outputColumns, + observedAllObjects: examined == 0 ? nil : kinds.objects == UInt64(examined), + observedUniformRowKind: uniform, observedArrayLengthRange: range, + observedRectangularArrayWidth: rectangular) + } +} diff --git a/Sources/JSONNavigation/JSONTraversal.swift b/Sources/JSONNavigation/JSONTraversal.swift index 97228ed..f6ea1d2 100644 --- a/Sources/JSONNavigation/JSONTraversal.swift +++ b/Sources/JSONNavigation/JSONTraversal.swift @@ -14,6 +14,8 @@ public struct JSONTraversal: Sendable { public let limits: JSONLimits /// Invocation-local rendezvous for qualification, absent from public consumers. package var checkpointObserver: (@Sendable (UInt64) -> Void)? + /// Qualification-only post-invalidation facts; never exposes scope or state. + package var lifetimeObserver: (@Sendable (Bool, Bool) -> Void)? /// Captures a document without admission or scanning; each operation admits it. /// - Parameters: @@ -34,7 +36,8 @@ public struct JSONTraversal: Sendable { options: JSONTraversalOptions = .init(), _ visitor: (JSONTraversalHit, JSON) throws -> JSONTraversalAction) throws -> JSONOperationReport { - let report = try execute(scope: scope, options: options, collectionLimit: nil, visitor) + let report = try execute(scope: scope, options: options, collectionLimit: nil, + { hit, value, _ in try visitor(hit, value) }, finish: { collection, _ in collection }) return .init(result: report.result.summary, statistics: report.statistics) } @@ -48,7 +51,22 @@ public struct JSONTraversal: Sendable { public func collect(scope: JSONTraversalScope = .subtree(at: []), options: JSONTraversalOptions = .init(), maximumRecords: Int = 100_000) throws -> JSONOperationReport { - try execute(scope: scope, options: options, collectionLimit: maximumRecords) { _, _ in .continue } + try execute(scope: scope, options: options, collectionLimit: maximumRecords, + { _, _, _ in .continue }, finish: { collection, _ in collection }) + } + + /// A synchronous reducer cannot store or return the invocation capability. + package func inspectOwned( + scope: JSONTraversalScope = .subtree(at: []), + options: JSONTraversalOptions = .init(), reducer: inout Reducer) + throws -> JSONOperationReport { + try execute(scope: scope, options: options, collectionLimit: nil, { hit, value, context in + do { return try reducer.observe(hit, value: value, context: &context) } + catch { try context.recordFailure(error); throw error } + }, finish: { collection, context in + do { return try reducer.finish(collection.summary, context: &context) } + catch { try context.recordFailure(error); throw error } + }) } private struct Frame { @@ -63,13 +81,18 @@ public struct JSONTraversal: Sendable { let path: JSONReadPath } - private func execute(scope selection: JSONTraversalScope, options: JSONTraversalOptions, + private func execute(scope selection: JSONTraversalScope, options: JSONTraversalOptions, collectionLimit: Int?, - _ visitor: (JSONTraversalHit, JSON) throws -> JSONTraversalAction) - throws -> JSONOperationReport { + _ visitor: (JSONTraversalHit, JSON, inout JSONTraversalContext) throws -> JSONTraversalAction, + finish: (JSONTraversalCollection, inout JSONTraversalContext) throws -> Result) + throws -> JSONOperationReport { let scope = try JSONReadScope(document, limits: limits) - defer { scope.invalidate() } + defer { + scope.invalidate() + lifetimeObserver?(scope.document == nil, scope.state == nil) + } let state = try scope.check() + var context = JSONTraversalContext(scope: scope) state.meter.checkpointObserver = checkpointObserver guard options.depthLimit.map({ $0 >= 0 }) ?? true, collectionLimit.map({ $0 >= 0 }) ?? true else { @@ -157,7 +180,7 @@ public struct JSONTraversal: Sendable { try Self.retainRecord(hit, state: state) records.append(hit) } - action = try visitor(hit, value) + action = try visitor(hit, value, &context) _ = try scope.check() // cancellation/terminal health, including after .stop try state.meter.release(64) summary.emittedCount += 1 @@ -192,7 +215,8 @@ public struct JSONTraversal: Sendable { while !seen.isEmpty { seen.removeLast(); try state.meter.release(16); try state.meter.work() } while !roots.isEmpty { roots.removeLast(); try state.meter.release(64); try state.meter.work() } try state.meter.release(144) - return try scope.complete(JSONTraversalCollection(nodes: records, summary: summary)) + let result = try finish(JSONTraversalCollection(nodes: records, summary: summary), &context) + return try scope.complete(result) } /// Exact metered prefix comparison; no String Set, value equality or unmetered hashing. diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift new file mode 100644 index 0000000..e075725 --- /dev/null +++ b/Sources/JSONNavigation/JSONTraversalContext.swift @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Package-only synchronous capability. No state/meter reference is returned. +/// An inout capability and Copyable reducer state prevent owned-capability escape. +package struct JSONTraversalContext: ~Copyable { + private let scope: JSONReadScope + internal init(scope: JSONReadScope) { self.scope = scope } + + private func perform(at path: [JSONLocationComponent] = [], + _ body: (JSONCodingState) throws -> T) throws -> T { + do { return try scope.run(path, body) } + catch { + let state = try scope.check() + state.meter.recordFailure(error) + throw error + } + } + + package mutating func work(_ units: UInt64 = 1) throws { + try perform { try $0.meter.work(units) } + } + package mutating func retain(_ bytes: UInt64) throws { + try perform { try $0.meter.retain(bytes) } + } + package mutating func release(_ bytes: UInt64) throws { + try perform { try $0.meter.release(bytes) } + } + package mutating func checkpoint() throws { _ = try scope.check() } + package mutating func recordFailure(_ error: any Error) throws { + let state = try scope.check() // original terminal health takes precedence + state.meter.recordFailure(error) + } + package mutating func equalKeys(_ lhs: JSONKey, _ rhs: JSONKey) throws -> Bool { + try perform { try JSONComparison.textEquals(lhs.string, rhs.string, meter: &$0.meter) } + } + package mutating func arrayCount(_ value: JSON, at path: [JSONLocationComponent]) throws -> Int { + try perform(at: path) { state in + try state.meter.work() + guard case .array(let array) = value else { throw JSONFailure(.typeMismatch, code: .wrongKind) } + return array.count + } + } + /// Reserve an additional owned alias, not another text admission or key model. + package mutating func retainKey(_ key: JSONKey) throws { + try perform { state in + for _ in key.string.utf8 { try state.meter.work(); try state.meter.retain(1) } + } + } + package mutating func retainLocation(_ path: [JSONLocationComponent]) throws -> [JSONLocationComponent] { + try perform(at: path) { state in + for part in path { + try state.meter.work(); try state.meter.retain(JSONLogicalWeights.indexSlot) + if case .key(let key) = part { + for _ in key.string.utf8 { try state.meter.work(); try state.meter.retain(1) } + } + } + return path + } + } + package mutating func appending(_ part: JSONLocationComponent, to path: [JSONLocationComponent]) throws -> [JSONLocationComponent] { + let handle = try perform(at: path) { state in + try scope.append(part, to: path, state: state) + } + return try retainLocation(handle.components) + } + package mutating func sourceSpan(at path: [JSONLocationComponent]) throws -> JSONSourceSpan? { + try perform(at: path) { state in + try scope.document?.sourceMap?.span(at: path, kind: .value, meter: &state.meter) + } + } +} diff --git a/Sources/JSONNavigation/JSONTraversalOwnedReducer.swift b/Sources/JSONNavigation/JSONTraversalOwnedReducer.swift new file mode 100644 index 0000000..d785b3e --- /dev/null +++ b/Sources/JSONNavigation/JSONTraversalOwnedReducer.swift @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Experimental package-only strategy. The executor owns the one original scope. +/// Reducers remain Copyable; their ordinary data cannot own a noncopyable capability. +/// No reference to the native scope or meter is available through this interface. +package protocol JSONTraversalOwnedReducer { + associatedtype Result + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction + mutating func finish(_ summary: JSONTraversalSummary, + context: inout JSONTraversalContext) throws -> Result +} diff --git a/Tests/JSONInspectionTests/ShapeAcceptanceTests.swift b/Tests/JSONInspectionTests/ShapeAcceptanceTests.swift new file mode 100644 index 0000000..4851901 --- /dev/null +++ b/Tests/JSONInspectionTests/ShapeAcceptanceTests.swift @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +import JSONInspection + +private func inspector(_ text: String, limits: JSONLimits = .balanced) throws -> JSONShapeInspector { + .init(document: .init(root: try JSON.parse(text)), limits: limits) +} +private func inspectionFailure(_ body: () throws -> Void) -> JSONFailure? { + do { try body(); Issue.record("Expected an operational failure"); return nil } + catch let error as JSONAccessFailure { return error.cause } + catch let error as JSONFailure { return error } + catch { Issue.record("Unexpected failure: \(error)"); return nil } +} + +@Test func inspectionI01EmptyArrayHasNoPositiveTraits() throws { + let result = try inspector("[]").inspectArray(maximumColumns: 0).result + #expect(result.sourceAdmissionComplete && result.rowCoverage == .complete) + #expect(result.totalRows == 0 && result.examinedRows == 0 && result.firstSeenColumns.isEmpty) + #expect(JSONTraversalKind.allCases.allSatisfy { result.rowKinds[$0] == 0 }) + #expect(result.observedAllObjects == nil && result.observedUniformRowKind == nil) + #expect(result.observedArrayLengthRange == nil && result.observedRectangularArrayWidth == nil) +} + +@Test func inspectionI02WrongKindThrowsRatherThanEmptyEvidence() throws { + for text in ["null", "false", "0", "{}", #""text""#] { + let failure = inspectionFailure { _ = try inspector(text).inspectArray(sampling: .prefix(maximumRows: 0)) } + #expect(failure?.category == .typeMismatch && failure?.code == .wrongKind) + } +} + +@Test func inspectionI03StrictMissingRootAndEmptyKey() throws { + let source = try inspector(#"{"":[],"a":[0]}"#) + #expect(try source.inspectArray(at: [.key("")]).result.totalRows == 0) + #expect(inspectionFailure { _ = try source.inspectArray() }?.category == .typeMismatch) + #expect(inspectionFailure { _ = try source.inspectArray(at: [.key("missing")]) }?.category == .missingValue) + #expect(inspectionFailure { _ = try source.inspectArray(at: [.key("a"), .index(-1)]) }?.code == .invalidIndex) + #expect(inspectionFailure { _ = try source.inspectArray(at: [.key("a"), .index(1)]) }?.code == .missingTarget) +} + +@Test func inspectionI04CompleteObjectRowsRemainRowEvidence() throws { + let evidence = try inspector(#"[{"id":1,"name":null},{"id":2,"name":"two"}]"#).inspectArray().result + #expect(evidence.rowCoverage == .complete && evidence.examinedRows == 2) + #expect(evidence.rowKinds.objects == 2 && evidence.observedObjectRows == 2) + #expect(evidence.observedAllObjects == true && evidence.observedUniformRowKind == .object) + #expect(evidence.firstSeenColumns.map(\.key) == [JSONKey("id"), JSONKey("name")]) + #expect(evidence.firstSeenColumns[0].present == 2 && evidence.firstSeenColumns[0].kinds.numbers == 2) + #expect(evidence.firstSeenColumns[1].explicitNull == 1 && evidence.firstSeenColumns[1].missing == 0) + #expect(evidence.traversal.completion == .restricted) // Cell subtrees were not traversed. + #expect(evidence.traversal.visitedCount == 3) +} + +@Test func inspectionI05HeterogeneousRowsKeepSixExactKinds() throws { + let evidence = try inspector(#"[{"a":null},1,null,[],false,"s"]"#).inspectArray().result + for kind in JSONTraversalKind.allCases { #expect(evidence.rowKinds[kind] == 1) } + #expect(evidence.observedObjectRows == 1 && evidence.observedNonObjectRows == 5) + #expect(evidence.firstSeenColumns[0].missing == 0 && evidence.firstSeenColumns[0].explicitNull == 1) + #expect(evidence.observedAllObjects == false && evidence.observedUniformRowKind == nil) + #expect(evidence.observedArrayLengthRange == 0...0 && evidence.observedRectangularArrayWidth == nil) +} + +@Test func inspectionI06LateCounterexampleCannotBecomePrefixCertification() throws { + let source = try inspector(#"[{"a":0},{"a":1},false]"#) + let prefix = try source.inspectArray(sampling: .prefix(maximumRows: 2)).result + #expect(prefix.rowCoverage == .prefix(examined: 2, total: 3)) + #expect(prefix.observedAllObjects == true && prefix.observedUniformRowKind == .object) + #expect(prefix.traversal.completion == .visitorStopped) + let complete = try source.inspectArray().result + #expect(complete.rowCoverage == .complete && complete.observedAllObjects == false) + #expect(complete.rowKinds.booleans == 1) + #expect(try source.inspectArray(sampling: .prefix(maximumRows: 10)).result.rowCoverage == .complete) +} + +@Test func inspectionI07ZeroPrefixAndConfigurationPrecedence() throws { + let source = try inspector("[1,2]") + let empty = try source.inspectArray(sampling: .prefix(maximumRows: 0)).result + #expect(empty.totalRows == 2 && empty.examinedRows == 0) + #expect(empty.rowCoverage == .prefix(examined: 0, total: 2)) + #expect(empty.observedAllObjects == nil && empty.observedUniformRowKind == nil) + #expect(empty.firstSeenColumns.isEmpty && empty.traversal.completion == .restricted) + var limits = JSONLimits.balanced; limits[.nodes] = 0 + let constrained = JSONShapeInspector(document: source.document, limits: limits) + #expect(inspectionFailure { _ = try constrained.inspectArray(sampling: .prefix(maximumRows: -1)) }?.category == .configuration) + #expect(inspectionFailure { _ = try constrained.inspectArray(maximumColumns: -1) }?.category == .configuration) + #expect(inspectionFailure { _ = try constrained.inspectArray(sampling: .prefix(maximumRows: 0)) }?.category == .resource) +} + +@Test func inspectionI08FirstSeenOrderAndPermutationMetamorphism() throws { + let first = try inspector(#"[{"z":1,"a":2},{"b":3,"a":4},{"z":5,"c":6}]"#).inspectArray().result + #expect(first.firstSeenColumns.map(\.key) == ["z", "a", "b", "c"].map { JSONKey($0) }) + #expect(first.firstSeenColumns.map(\.firstRow) == [0, 0, 1, 2]) + #expect(first.firstSeenColumns.map(\.present) == [2, 2, 1, 1]) + #expect(first.firstSeenColumns.map(\.missing) == [1, 1, 2, 2]) + let permuted = try inspector(#"[{"b":3,"a":4},{"z":1,"a":2},{"z":5,"c":6}]"#).inspectArray().result + #expect(permuted.firstSeenColumns.map(\.key) == ["b", "a", "z", "c"].map { JSONKey($0) }) + #expect(first.rowKinds == permuted.rowKinds) + let reordered = try inspector(#"[{"a":2,"z":1},{"b":3,"a":4},{"z":5,"c":6}]"#).inspectArray().result + #expect(reordered.firstSeenColumns.map(\.key) == ["a", "z", "b", "c"].map { JSONKey($0) }) +} + +@Test func inspectionI09ScalarDistinctKeysNeverNormalize() throws { + let evidence = try inspector(#"[{"é":null,"e\u0301":1},{"e\u0301":2}]"#).inspectArray().result + let columns = evidence.firstSeenColumns + #expect(columns.count == 2 && columns[0].key != columns[1].key) + #expect(columns[0].key.string == columns[1].key.string) // Swift's canonical equivalence is not the key authority. + #expect(columns[0].present == 1 && columns[0].missing == 1 && columns[0].explicitNull == 1) + #expect(columns[1].present == 2 && columns[1].missing == 0) +} + +@Test func inspectionI10NumericKeyAndIndexKeepTypedSelection() throws { + let source = try inspector(#"{"0":[{"0":null}],"a":[[{"0":1}]]}"#) + let key = try source.inspectArray(at: [.key("0")]).result + #expect(key.sourceSelection == [.key("0")] && key.firstSeenColumns[0].key == JSONKey("0")) + let index = try source.inspectArray(at: [.key("a"), .index(0)]).result + #expect(index.sourceSelection == [.key("a"), .index(0)]) + #expect(inspectionFailure { _ = try source.inspectArray(at: [.index(0)]) }?.category == .typeMismatch) + #expect(inspectionFailure { _ = try source.inspectArray(at: [.key("a"), .key("0")]) }?.category == .typeMismatch) +} + +@Test func inspectionI11NullAndAbsenceHaveDifferentCounts() throws { + let column = try inspector(#"[{"a":null},{},{"a":false}]"#).inspectArray().result.firstSeenColumns[0] + #expect(column.present == 2 && column.explicitNull == 1 && column.missing == 1) + #expect(column.kinds.nulls == 1 && column.kinds.booleans == 1) + #expect(column.kinds.objects == 0 && column.kinds.numbers == 0) +} + +@Test func inspectionI12NonObjectsAreOutsideMissingDenominator() throws { + let result = try inspector(#"[{"a":1},{},42,null,false,[]]"#).inspectArray().result + #expect(result.observedObjectRows == 2 && result.observedNonObjectRows == 4) + #expect(result.firstSeenColumns[0].present == 1 && result.firstSeenColumns[0].missing == 1) + #expect(result.rowKinds.numbers == 1 && result.rowKinds.nulls == 1) +} + +@Test func inspectionI13ExactDecimalsAndLexemesRemainCanonical() throws { + let source = try inspector(#"[{"n":9007199254740993,"t":0.1,"p":0.0100}]"#) + let root = source.document.root + let result = try source.inspectArray().result + #expect(result.firstSeenColumns.allSatisfy { $0.kinds.numbers == 1 }) + let large = try JSONPointer(tokens: ["0", "n"]).resolve(in: root) + #expect(try large.numberValue?.integer(as: UInt64.self) == 9_007_199_254_740_993) + #expect(try JSONPointer(tokens: ["0", "t"]).resolve(in: root).numberValue == JSONNumber(parsing: "0.1")) + #expect(try JSONPointer(tokens: ["0", "p"]).resolve(in: root).numberValue?.originalLexeme == "0.0100") + #expect(source.document.root == root) +} + +@Test func inspectionI14RaggedAndNonnumericArrayRowsAreDescriptive() throws { + let ragged = try inspector("[[1,2],[3]]").inspectArray().result + #expect(ragged.observedUniformRowKind == .array && ragged.observedArrayLengthRange == 1...2) + #expect(ragged.observedRectangularArrayWidth == nil) + let source = try inspector(#"[["a",null],["b",{}]]"#) + #expect(try source.inspectArray().result.observedRectangularArrayWidth == 2) // Structural width only, no numeric claim. + let prefix = try inspector("[[1,2],[3]]").inspectArray(sampling: .prefix(maximumRows: 1)).result + #expect(prefix.observedRectangularArrayWidth == 2 && prefix.rowCoverage == .prefix(examined: 1, total: 2)) + #expect(try inspector("[[],[],0]").inspectArray().result.observedRectangularArrayWidth == nil) +} + +@Test func inspectionI15PrefixStillAdmitsFullSourceExactlyOnce() throws { + let root = JSON.object(try JSONObject(members: [ + JSONMember("outside", value: .array(Array(repeating: .null, count: 100))), + JSONMember("rows", value: .array([.null])) + ])) + let document = JSONDocument(root: root) + let ordinary = try JSONTraversal(document: document).visit(scope: .subtree(at: [.key("rows")]), + options: .init(depthLimit: 1)) { _, _ in .continue } + let report = try JSONShapeInspector(document: document).inspectArray(at: [.key("rows")], sampling: .prefix(maximumRows: 0)) + #expect(report.statistics[.nodes] == ordinary.statistics[.nodes] && report.statistics[.nodes] == 104) + #expect(report.result.examinedRows == 0 && report.result.sourceAdmissionComplete) + var limits = JSONLimits.balanced; limits[.nodes] = 2 + #expect(inspectionFailure { + _ = try JSONShapeInspector(document: document, limits: limits).inspectArray(at: [.key("rows")], sampling: .prefix(maximumRows: 0)) + }?.resource?.metric == .nodes) +} + +@Test func inspectionI16InnerColumnWorkQuotasAndCancellationAreTerminal() async throws { + let object = try JSONObject(members: (0..<20).map { JSONMember("long-column-key-\($0)", value: .null) }) + let document = JSONDocument(root: .array([.object(object)])) + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + let baseline = try JSONShapeInspector(document: document, limits: limits).inspectArray() + let keyBytes = object.reduce(UInt64(0)) { $0 + UInt64($1.key.string.utf8.count) } + #expect(baseline.statistics[.retainedBytes] == 256 + 20 * 192 + keyBytes) + #expect(try JSONShapeInspector(document: document).inspectArray(maximumColumns: 20).result.firstSeenColumns.count == 20) + do { _ = try JSONShapeInspector(document: document).inspectArray(maximumColumns: 0); Issue.record("Expected zero-column refusal") } + catch let error as JSONShapeInspectionFailure { #expect(error == .columnLimitExceeded(limit: 0)) } + var exact = limits; exact[.workUnits] = baseline.statistics[.workUnits] + #expect(try JSONShapeInspector(document: document, limits: exact).inspectArray().statistics[.workUnits] == exact[.workUnits]) + exact[.workUnits] -= 1 + #expect(inspectionFailure { _ = try JSONShapeInspector(document: document, limits: exact).inspectArray() }?.resource?.attemptedUsage == baseline.statistics[.workUnits]) + exact = limits; exact[.retainedBytes] = baseline.statistics.peakLogicalRetainedBytes + #expect(try JSONShapeInspector(document: document, limits: exact).inspectArray().result.firstSeenColumns.count == 20) + exact[.retainedBytes] -= 1 + #expect(inspectionFailure { _ = try JSONShapeInspector(document: document, limits: exact).inspectArray() }?.resource?.metric == .retainedBytes) + do { _ = try JSONShapeInspector(document: document).inspectArray(maximumColumns: 19); Issue.record("Expected column refusal") } + catch let error as JSONShapeInspectionFailure { #expect(error == .columnLimitExceeded(limit: 19)) } + let ordinary = try JSONTraversal(document: document, limits: limits).visit(options: .init(depthLimit: 1)) { _, _ in .skipDescendants } + let threshold = ordinary.statistics[.workUnits] + 100 + #expect(baseline.statistics[.workUnits] > threshold + 16) + let cancelled = Mutex(false) + let task = Task { + var source = JSONShapeInspector(document: document, limits: limits) + source.checkpointObserver = { work in + if work >= threshold { cancelled.withLock { $0 = true }; withUnsafeCurrentTask { $0?.cancel() } } + } + return try source.inspectArray() + } + do { _ = try await task.value; Issue.record("Cancellation cannot publish partial evidence") } + catch is CancellationError { } + #expect(cancelled.withLock { $0 }) +} + +@Test func inspectionI17CoverageSpansReplacementAndSharedInvocations() async throws { + let document = try JSONParser().parseDocument(Data(#"{"rows":[{"a":null},{"b":1}]}"#.utf8)) + let source = JSONShapeInspector(document: document) + let mapped = try source.inspectArray(at: [.key("rows")], includeSourceSpan: true) + #expect(mapped.result.sourceSpan == (try document.sourceMap?.span(at: [.key("rows")]))) + #expect(mapped.result.rowCoverage == .complete && mapped.result.traversal.completion == .restricted) + let replacement = JSONShapeInspector(document: document.replacingRoot(document.root)) + #expect(try replacement.inspectArray(at: [.key("rows")], includeSourceSpan: true).result.sourceSpan == nil) + let prefix = try source.inspectArray(at: [.key("rows")], sampling: .prefix(maximumRows: 1)) + #expect(prefix.result.rowCoverage == .prefix(examined: 1, total: 2) && prefix.result.traversal.completion == .visitorStopped) + try await withThrowingTaskGroup(of: JSONOperationReport.self) { group in + for _ in 0..<8 { group.addTask { try source.inspectArray(at: [.key("rows")], includeSourceSpan: true) } } + for try await report in group { + #expect(report.statistics[.workUnits] == mapped.statistics[.workUnits]) + #expect(report.statistics.peakLogicalRetainedBytes == mapped.statistics.peakLogicalRetainedBytes) + #expect(report.result.sourceSelection == [.key("rows")] && report.result.rowKinds == mapped.result.rowKinds) + } + } +} diff --git a/Tests/JSONNavigationTests/ReducerOwnershipTests.swift b/Tests/JSONNavigationTests/ReducerOwnershipTests.swift new file mode 100644 index 0000000..8942e89 --- /dev/null +++ b/Tests/JSONNavigationTests/ReducerOwnershipTests.swift @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation + +private enum ReducerMarker: Error { case replacement } +private struct ProbeReducer: JSONTraversalOwnedReducer { + enum Mode { case noop, charged, retained, caughtType, cancellation, path } + var mode = Mode.noop + var replaceFinalError = false + var hits: [JSONTraversalHit] = [] // Independent qualification oracle; caller storage. + var path: [JSONLocationComponent] = [] + var span: JSONSourceSpan? + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + hits.append(hit) + switch mode { + case .noop: break + case .charged: try context.work(7) + case .retained: try context.retain(96) + case .caughtType: + do { _ = try context.arrayCount(value, at: hit.absoluteLocation) } catch { } + case .cancellation: + withUnsafeCurrentTask { $0?.cancel() } + do { try context.checkpoint() } catch { } + throw ReducerMarker.replacement + case .path: + if hit.relativeDepth == 1 { + path = try context.appending(.key(""), to: hit.absoluteLocation) + } + } + return .continue + } + mutating func finish(_ summary: JSONTraversalSummary, + context: inout JSONTraversalContext) throws -> JSONTraversalSummary { + if mode == .charged { + do { try context.work(11) } + catch { if replaceFinalError { throw ReducerMarker.replacement }; throw error } + } + if mode == .path { span = try context.sourceSpan(at: path) } + return summary + } +} + +@Test func navigationReducerNoopPreservesEveryPublicStatistic() throws { + let walk = JSONTraversal(document: .init(root: try JSON.parse(#"{"rows":[{"id":1},null],"":[]}"#))) + let scopes: [JSONTraversalScope] = [.subtree(at: []), .subtree(at: [.key("rows")]), + .virtual(roots: [[.key("rows")], [.key("")]], overlap: .rejectAmbiguous), + .virtual(roots: [[], [.key("rows")]], overlap: .uniquePhysicalNodes)] + for scope in scopes { + var hits: [JSONTraversalHit] = [], reducer = ProbeReducer() + let ordinary = try walk.visit(scope: scope) { hit, _ in hits.append(hit); return .continue } + let owned = try walk.inspectOwned(scope: scope, reducer: &reducer) + #expect(hits == reducer.hits) + #expect(ordinary.result.visitedCount == owned.result.visitedCount) + #expect(ordinary.result.completion == owned.result.completion) + for metric in JSONResourceMetric.allCases { + #expect(ordinary.statistics[metric] == owned.statistics[metric]) + } + #expect(ordinary.statistics.peakLogicalRetainedBytes == owned.statistics.peakLogicalRetainedBytes) + } +} + +@Test func navigationReducerChargesObservationAndFinalizationOnce() throws { + let walk = JSONTraversal(document: .init(root: [0, 1, 2])) + let ordinary = try walk.visit { _, _ in .continue } + var reducer = ProbeReducer(mode: .charged) + let owned = try walk.inspectOwned(reducer: &reducer) + #expect(owned.result.visitedCount == 4) + #expect(owned.statistics[.nodes] == 4) + #expect(owned.statistics[.workUnits] == ordinary.statistics[.workUnits] + 39) +} + +@Test func navigationReducerInclusiveQuotaAndOriginalTerminalPriority() throws { + let document = JSONDocument(root: [0, 1, 2]) + func run(_ limits: JSONLimits, replace: Bool = false) throws -> JSONOperationReport { + var reducer = ProbeReducer(mode: .charged, replaceFinalError: replace) + return try JSONTraversal(document: document, limits: limits).inspectOwned(reducer: &reducer) + } + let baseline = try run(.balanced) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + #expect(try run(limits).statistics[.workUnits] == baseline.statistics[.workUnits]) + limits[.workUnits] -= 1 + for replace in [false, true] { + do { _ = try run(limits, replace: replace); Issue.record("Expected quota refusal") } + catch let error as JSONFailure { + #expect(error.resource?.metric == .workUnits && error.resource?.domain == .core) + #expect(error.resource?.attemptedUsage == baseline.statistics[.workUnits]) + } + } +} + +@Test func navigationReducerRetainedOutputsUseOriginalPeak() throws { + func run(_ limits: JSONLimits) throws -> JSONOperationReport { + var reducer = ProbeReducer(mode: .retained) + return try JSONTraversal(document: .init(root: .null), limits: limits).inspectOwned(reducer: &reducer) + } + let baseline = try run(.balanced) + #expect(baseline.statistics[.retainedBytes] == 96) + var limits = JSONLimits.balanced; limits[.retainedBytes] = baseline.statistics.peakLogicalRetainedBytes + #expect(try run(limits).result.visitedCount == 1) + limits[.retainedBytes] -= 1 + do { _ = try run(limits); Issue.record("Expected retention refusal") } + catch let error as JSONFailure { #expect(error.resource?.metric == .retainedBytes) } +} + +@Test func navigationReducerCaughtOwnedTypeErrorRemainsTerminal() throws { + var reducer = ProbeReducer(mode: .caughtType) + do { + _ = try JSONTraversal(document: .init(root: .null)).inspectOwned(reducer: &reducer) + Issue.record("Caught owned failure cannot return success") + } catch let error as JSONAccessFailure { + #expect(error.cause.code == .wrongKind && error.path.isEmpty) + } +} + +@Test func navigationReducerCancellationCannotBecomeReplacementError() async throws { + let task = Task { + var reducer = ProbeReducer(mode: .cancellation) + return try JSONTraversal(document: .init(root: [0, 1, 2])).inspectOwned(reducer: &reducer) + } + do { _ = try await task.value; Issue.record("Expected cancellation") } + catch is CancellationError { } +} + +@Test func navigationReducerPathsAndSpansKeepCoreAuthority() throws { + let document = try JSONParser().parseDocument(Data(#"{"rows":[{"":1}]}"#.utf8)) + var reducer = ProbeReducer(mode: .path) + _ = try JSONTraversal(document: document).inspectOwned(scope: .subtree(at: [.key("rows")]), reducer: &reducer) + #expect(reducer.path == [.key("rows"), .index(0), .key("")]) + #expect(try JSONPointer(tokens: ["rows", "0", ""]).resolve(in: document.root) == 1) + #expect(reducer.span == (try document.sourceMap?.span(at: reducer.path))) +} + +@Test func navigationReducerScopeReleasesRootSourceAndStateOnEveryExit() throws { + let observations = Mutex<[(Bool, Bool)]>([]) + let document = try JSONParser().parseDocument(Data("[0,1,2]".utf8)) + var walk = JSONTraversal(document: document) + walk.lifetimeObserver = { rootReleased, stateReleased in + observations.withLock { $0.append((rootReleased, stateReleased)) } + } + var reducer = ProbeReducer() + #expect(try walk.inspectOwned(reducer: &reducer).statistics[.retainedBytes] == 0) + reducer = ProbeReducer(mode: .caughtType) + do { _ = try walk.inspectOwned(reducer: &reducer); Issue.record("Expected type refusal") } + catch is JSONAccessFailure { } + let facts = observations.withLock { $0 } + #expect(facts.count == 2 && facts.allSatisfy { $0.0 && $0.1 }) + // The immutable traversal and caller still own their source; this checks only + // the invalidated invocation's optional root/source/state, not process RSS. +} diff --git a/Tools/InspectionQualification/Consumer.swift b/Tools/InspectionQualification/Consumer.swift new file mode 100644 index 0000000..a7319cb --- /dev/null +++ b/Tools/InspectionQualification/Consumer.swift @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONNavigation +import JSONInspection + +@main struct InspectionConsumer { + static func main() throws { + var checks = 0 + func check(_ id: String, _ condition: Bool) { + precondition(condition, id); checks += 1 + print("INSPECTION_CHECK \(id) passed") + } + let text = #"[{"é":1,"n":9007199254740993,"x":null},{"e\u0301":2,"n":0.0100},false]"# + let document = try JSONParser().parseDocument(Data(text.utf8)) + let inspector = JSONShapeInspector(document: document) + let full = try inspector.inspectArray(includeSourceSpan: true) + let prefix = try inspector.inspectArray(sampling: .prefix(maximumRows: 2)) + check("coverage", full.result.rowCoverage == .complete && full.result.traversal.completion == .restricted && prefix.result.rowCoverage == .prefix(examined: 2, total: 3) && prefix.result.traversal.completion == .visitorStopped) + check("observed-only", prefix.result.observedAllObjects == true && full.result.observedAllObjects == false && full.result.observedNonObjectRows == 1) + let columns = full.result.firstSeenColumns + check("exact-keys", columns.map(\.key) == [JSONKey("é"), JSONKey("n"), JSONKey("x"), JSONKey("e\u{301}")]) + check("null-missing", columns[2].present == 1 && columns[2].explicitNull == 1 && columns[2].missing == 1 && full.result.observedObjectRows == 2) + let exact = try document.root.read { try $0[0]["n"].requiredInteger(as: UInt64.self) } + let number = try document.root.read { try $0[1]["n"].requiredNumber() } + check("exact-numbers", exact == 9_007_199_254_740_993 && number.originalLexeme == "0.0100") + let admitted = try document.readWithReport { _ in true } + check("one-admission", full.statistics[.nodes] == admitted.statistics[.nodes] && prefix.statistics[.nodes] == admitted.statistics[.nodes] && prefix.result.sourceAdmissionComplete) + let zero = try inspector.inspectArray(sampling: .prefix(maximumRows: 0), maximumColumns: 0) + check("zero-prefix", zero.result.examinedRows == 0 && zero.result.observedAllObjects == nil && zero.result.observedUniformRowKind == nil) + var capacity = false + do { _ = try inspector.inspectArray(maximumColumns: 0) } + catch JSONShapeInspectionFailure.columnLimitExceeded(limit: 0) { capacity = true } + check("capacity", capacity) + var missing = false, wrong = false + do { _ = try inspector.inspectArray(at: [.index(8)]) } + catch let failure as JSONAccessFailure { missing = failure.cause.category == .missingValue && failure.path == [.index(8)] } + do { _ = try inspector.inspectArray(at: [.index(2)]) } + catch let failure as JSONAccessFailure { wrong = failure.cause.category == .typeMismatch && failure.path == [.index(2)] } + check("access-fidelity", missing && wrong) + let replaced = try JSONShapeInspector(document: document.replacingRoot(document.root)).inspectArray(includeSourceSpan: true) + check("source-coherence", full.result.sourceSpan != nil && replaced.result.sourceSpan == nil) + var limits = JSONLimits.balanced; limits[.workUnits] = prefix.statistics[.workUnits] + let exactWork = try JSONShapeInspector(document: document, limits: limits).inspectArray(sampling: .prefix(maximumRows: 2)) + limits[.workUnits] -= 1 + var refused = false + do { _ = try JSONShapeInspector(document: document, limits: limits).inspectArray(sampling: .prefix(maximumRows: 2)) } + catch let failure as JSONFailure { refused = failure.resource?.metric == .workUnits } + check("inclusive-work", refused && exactWork.statistics[.workUnits] == prefix.statistics[.workUnits]) + var bad = false + limits[.nodes] = 0 + do { _ = try JSONShapeInspector(document: document, limits: limits).inspectArray(sampling: .prefix(maximumRows: -1)) } + catch let failure as JSONFailure { bad = failure.category == .configuration && failure.code == .invalidLimits } + check("configuration-precedence", bad) + print("INSPECTION_RESOURCE work=\(prefix.statistics[.workUnits]) retained=\(prefix.statistics[.retainedBytes]) peak=\(prefix.statistics.peakLogicalRetainedBytes) nodes=\(prefix.statistics[.nodes])") + print("INSPECTION_CONSUMER passed cases=\(checks)") + } +} diff --git a/Tools/InspectionQualification/Package.swift b/Tools/InspectionQualification/Package.swift new file mode 100644 index 0000000..5220c4b --- /dev/null +++ b/Tools/InspectionQualification/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "InspectionQualification", platforms: [.macOS("27.0"), .iOS("27.0")], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "InspectionConsumer", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), + .product(name: "JSONNavigation", package: "Swift-JSON"), + .product(name: "JSONInspection", package: "Swift-JSON") + ], path: ".", exclude: ["README.md"], sources: ["Consumer.swift"])], swiftLanguageModes: [.v6]) diff --git a/Tools/InspectionQualification/README.md b/Tools/InspectionQualification/README.md new file mode 100644 index 0000000..709c7d3 --- /dev/null +++ b/Tools/InspectionQualification/README.md @@ -0,0 +1,7 @@ +# External inspection consumer + +Run `python3 -B Scripts/qualify_inspection.py --output .build/ci/inspection-consumer`. +The separate SwiftPM package imports public products without @testable. Twelve +named outcomes run in Debug/Release; the same source runs under ASan. Three DocC +examples compile and run in each configuration, and ordinary compiler errors prove +core-only import and package-capability isolation. I01-I17 remain the root tests.