From 360f3b8471e70b18db225d46f12cb6c4c96c8d78 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:39:09 +0200 Subject: [PATCH 1/3] Replay qualified NAV11 graph source for public promotion review Exact source tree b832f6554a0277ac1776ad84d81ba3296d2048f8 from private donor PR #67 (81485930f859131d0802691ca6370f482ba62911), replayed directly on current main. Public promotion remains gated by owner signature review; no NAV10 dependency or merge authorization. --- AGENTS.md | 3 +- .../Architecture/ExtractionManifest.json | 12 + .../NAV11/APICandidate.md | 31 + .../StructuralIntelligence/NAV11/GraphADR.md | 57 ++ .../NAV11/SourceAuthority.md | 22 + Documentation/CI/README.md | 13 + Documentation/Qualification/GraphCases.json | 43 ++ .../Qualification/TestDiscovery.json | 10 +- Package.swift | 2 +- README.md | 6 + Scripts/ci.py | 31 +- Scripts/graph_acceptance.py | 21 + Scripts/qualify_graph.py | 89 +++ Scripts/qualify_graph_context.py | 84 +++ Scripts/test_qualification_harness.py | 28 +- Scripts/wasm_support.py | 17 +- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/GraphExperiment.md | 17 + .../Documentation.docc/JSONInspection.md | 4 + .../Graph/JSONGraphProfile.swift | 42 ++ .../Graph/JSONGraphProjection.swift | 204 ++++++ .../JSONInspection/Graph/JSONGraphQuery.swift | 89 +++ .../JSONNavigation/JSONTraversalContext.swift | 31 +- .../GraphAcceptanceTests.swift | 382 ++++++++++ .../GraphFixtures/GRAPH-FIXTURE-ORACLE.json | 653 ++++++++++++++++++ .../GraphFixtures/g01-empty.json | 4 + .../GraphFixtures/g02-simple.json | 27 + .../GraphFixtures/g03-unicode-scalars.json | 16 + .../g04-empty-and-special-ids.json | 23 + .../GraphFixtures/g05-duplicate-id.json | 13 + .../g06-equal-payload-distinct-id.json | 17 + .../GraphFixtures/g07-node-id-missing.json | 8 + .../GraphFixtures/g08-node-id-null.json | 8 + .../GraphFixtures/g09-node-id-number.json | 8 + .../g10-node-row-not-object.json | 6 + .../g11-edge-source-missing.json | 12 + .../GraphFixtures/g12-edge-target-null.json | 13 + .../GraphFixtures/g13-edge-target-number.json | 13 + .../g14-edge-row-not-object.json | 10 + .../GraphFixtures/g15-dangling.json | 13 + .../GraphFixtures/g15b-dangling-empty.json | 9 + .../GraphFixtures/g16-duplicate-edge.json | 20 + .../GraphFixtures/g17-reverse-edges.json | 20 + .../GraphFixtures/g18-self-edge.json | 13 + .../GraphFixtures/g19-cycle.json | 27 + .../GraphFixtures/g20-isolate.json | 19 + .../g21-bfs-source-edge-order.json | 30 + .../GraphFixtures/g22-diamond.json | 34 + .../GraphFixtures/g23-numeric-object-key.json | 18 + .../GraphFixtures/g24-overlap.json | 7 + .../GraphFixtures/g25-wrong-nodes-array.json | 4 + .../GraphFixtures/g26-wrong-edges-array.json | 4 + .../GraphFixtures/generate_graph_fixtures.py | 88 +++ .../GraphFixtures/verify_graph_fixtures.py | 16 + 54 files changed, 2364 insertions(+), 29 deletions(-) create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md create mode 100644 Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md create mode 100644 Documentation/Qualification/GraphCases.json create mode 100644 Scripts/graph_acceptance.py create mode 100644 Scripts/qualify_graph.py create mode 100644 Scripts/qualify_graph_context.py create mode 100644 Sources/JSONInspection/Documentation.docc/GraphExperiment.md create mode 100644 Sources/JSONInspection/Graph/JSONGraphProfile.swift create mode 100644 Sources/JSONInspection/Graph/JSONGraphProjection.swift create mode 100644 Sources/JSONInspection/Graph/JSONGraphQuery.swift create mode 100644 Tests/JSONInspectionTests/GraphAcceptanceTests.swift create mode 100644 Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g01-empty.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g02-simple.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json create mode 100644 Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json create mode 100755 Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py create mode 100755 Tests/JSONInspectionTests/GraphFixtures/verify_graph_fixtures.py diff --git a/AGENTS.md b/AGENTS.md index 2aebea7..4ad90fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,4 +43,5 @@ Documentation/Architecture before changing behavior. Scalar scanning and immutable plan-local regex sharing are selected private optimizations. Source-attached N3 snapshots and P1 tree/sparse-table projections live in JSONInspection. -P2 graph/store/set/matrix/retrieval/persistence and JSON text-sequence framing remain deferred. +NAV11 directed simple graph is a package-only JSONInspection experiment; no public graph API/product or automatic merge. +Store/set/matrix/retrieval/persistence and JSON text-sequence framing remain separate deferred trains. diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 4a191c4..eec79bd 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -271,6 +271,18 @@ "attribution": "User-authored packet fixture input; original integration/tests Apache-2.0. Packet source/provenance retained separately.", "validation": "TypedConstructionTests.swift and public G3Acceptance consumer; strict native compiler/evaluator authority", "evidence": "Release1.6/G3 qualification and materialized per-case receipts" + }, + { + "repository": "ThorFuchs/Swift-JSON", + "commit": "1a0306d076a08c8c43281f91412a11cd744ff3f6", + "destination": "Sources/JSONInspection/Graph", + "disposition": "Original Apache-2.0 NAV11 experimental source-backed directed simple graph; no upstream graph code adapted. Canonical Navigation/context and JSON comparison remain authorities. Independent packet fixtures retained with SHA-bound oracle.", + "validation": [ + "Tests/JSONInspectionTests/GraphAcceptanceTests.swift", + "Scripts/qualify_graph_context.py", + "Scripts/qualify_graph.py" + ], + "evidence": "Scripts/ci.py source-bound native/Linux/safety/compiler receipts; private symbols only" } ], "original_implementation_notes": [ diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md new file mode 100644 index 0000000..e4c881c --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md @@ -0,0 +1,31 @@ +# Graph API proposal for later owner review + +All NAV11 types and operations are **package-only**, not application API. Product +count remains ten, dependencies remain empty, Navigation56/Inspection95 unchanged. +The proposed future public subset, contingent on separate review, is: + +- Immutable `JSONGraphProfile(nodesAt:edgesAt:nodeIDKey:edgeSourceKey:edgeTargetKey:)`. +- `JSONGraphProjection.build(document:profile:limits:maximumNodes:maximumEdges:)` + returning `JSONOperationReport`, defaults 10000/50000. +- Read-only captured document/profile/nodeCount/edgeCount and complete traversal + summary. IDs/CSR buffers are research metadata; promoting their raw arrays is + not proposed. Source-local node/edge ordinals have no cross-view identity. +- `nodeValue(at:limits:)`, `edgeValue(at:limits:)`, `nodeSpan(at:limits:)` and + `outgoingEdges(of:limits:maximumResults:)` with independent finite whole-source + admissions. Returned JSON aliases receive additional owned-value accounting. +- `breadthFirst(fromNode:limits:maximumFrontier:maximumVisited:maximumResults:)` + returning original node ordinals; defaults 10000 each, start included, complete + reachable component. Zero caps refuse a real start; an empty graph has no start. +- Typed semantic errors with original row/field paths; core missing/kind/resource + and cancellation errors stay intact. No partial-success variant exists. + +BFS stores queue history as returned output; frontier means pending entries, while +visited/result caps independently bound total enqueues/output. Graph metadata +aliases, visited flags and queue entries share the query's original traversal +ledger. Scalar-exact linear resolution and O(N²+NE+E²) construction are explicit +costs, not optimized lookup claims. No new JSON identity/path or serialization. + +Actual compiled spelling is in Sources/JSONInspection/Graph. Constructors remain +fileprivate. Compiler probes reject external type access, private initializers, +mutation, internal reducers, and context escape. No public DocC examples imply +these private declarations are available. No merge or promotion is authorized. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md b/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md new file mode 100644 index 0000000..16377f1 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md @@ -0,0 +1,57 @@ +# NAV11 directed simple graph experiment + +Package-only JSONInspection research, based on merged Navigation/Inspection; +no NAV10, new product, public symbol or graph JSON model. One original +`JSONTraversal.inspectOwned` invocation selects `[nodesAt, edgesAt]` with +`.rejectAmbiguous` and depth one. All strict root selections and overlap checks +precede callbacks; nodes are validated before edges, in their physical order. + +Negative capacities are configuration failures before admission. Existing core +precedence is retained: complete source admission precedes strict selection, +overlap and root-kind checks. No separate unmetered profile scan or second ledger +is added to move those checks ahead of admission. Missing roots preserve core +`missingMember`/`missingTarget`; wrong root kinds preserve typed core access errors. +Paths and explicit keys are immutable. Reusing a field spelling across roles is +intentional and valid (an edge's single field may explicitly name both ends of a +self-edge); no field inference or silent repair occurs. + +The exclusive context adds mutating delegates only: direct string member selection, +scalar-exact string comparison and matching alias release. JSONObject.lookup and +JSONComparison.textEquals remain authorities. Missing, null and wrong kind are +reported separately; strings own 32+UTF8 logical bytes until released or handed off. +Profile keys are explicitly key-admitted on that meter even for empty arrays; +the preexisting retainKey delegate gains an opt-in caller-input admission flag, +with its original default behavior/counters preserved. +No cursor, scope, ledger or noncopyable capability leaves the reducer. Original +2-positive/20-negative codegen tests are unchanged; helper escape probes and runtime +parity run separately in Debug and Release. A compiler crash blocks implementation. + +Graph metadata is source-local: first row ordinals, exact ID aliases, endpoint +ordinals and CSR offsets/edge ordinals in original edge order. Original source is +transferred once after complete validation/finalization. Defaults are 10,000 nodes, +50,000 edges; each independently configurable finite cap is inclusive and still +subject to all 49 core metrics. Linear exact scans may cost O(N²+NE+E²). + +Later value/span/catalogue/BFS calls start explicit independent full-source +operations. BFS has a head-index queue, visited-at-enqueue and source-edge order. +Defaults: 10,000 frontier entries, visited nodes and returned nodes. Queue history +remains charged until handoff; frontier capacity means live pending entries. +Results certify the complete reachable component including the start, not global +reachability. Failure/cancellation returns no partial result. + +Logical policy V1: 256 fixed builder bytes, 32+UTF8 per node ID alias, +16 per node record, 24 per edge record, 8 per offset and adjacency reference, +plus exact profile keys/paths and the original source. Queries admit graph metadata +aliases, queue/output slots (8 each), visited flags (1 each) and their source; +returned JSON aliases additionally receive core owned-retention accounting. +These are logical quotas, not allocator/RSS or caller-instruction guarantees. + +No public promotion, merge, release, timing or speed claim. Controlled performance +remains HOLD. Store, persistence, graph editing, weights, algorithms other than BFS, +query/CLI/HTML/network adapters and NAV10 batching remain separate work. + +BFS entry precedence is negative capacities, node bounds, whole source/metadata +admission, then enqueue's frontier/visited/result ceilings. Work/cancellation that +is observed first takes precedence. Edge rows validate source field then target +field before resolving source then target IDs, then compare ordered pairs. All +node rows precede all edge rows; a late duplicate node outranks a dangling edge. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md b/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md new file mode 100644 index 0000000..62504b6 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md @@ -0,0 +1,22 @@ +# NAV11 source authority + +Base main: 1a0306d076a08c8c43281f91412a11cd744ff3f6. NAV10 PR66/d566f28 is +independent and not a source dependency. Stable 1.6.1/e431f15 remains immutable. + +| Graph work | Existing delegate | Invocation boundary | +| --- | --- | --- | +| Complete root/source/map admission and failures | JSONReadScope, JSONAdmission, JSONWorkMeter | Original inspectOwned scope | +| Physical/virtual roots and exact overlap | JSONTraversal.execute, rejectAmbiguous | Same original scope, before callbacks | +| Row locations and bounded paths | JSONTraversalHit, JSONLocationComponent, context.appending | Same scope | +| Direct string member/presence/kind | JSONObject.lookup through mutating context delegate | No reader/new ledger | +| ID equality | JSONComparison.textEquals through context.equalStrings | Scalar-exact charged UTF8 comparison | +| Owned alias accounting, cancellation, terminal health | context.perform + scope.run/check | Original state, no escape | +| Adjacency and BFS | Package-only graph reducers | Checked work/retention in that invocation | +| Completed source handoff | context.transferDocumentOwnership | Once after complete valid construction | +| Later source value/span | JSONReadScope.select and sourceMap.span | Explicit new whole-source operation | +| Returned value alias | JSONAdmission.value(owned:true) | Same later-read ledger, additional output cost | + +No JSON, Pointer, value equality, schema, stream, Patch, HTML or CLI semantic change. +The new context methods are package-only and mutating; original ownership codegen +fixtures are byte-identical. ExtractionManifest records original Apache-2.0 code +and independent fixture provenance. Public symbol graphs must remain unchanged. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 2c1de90..2b8dc77 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -1,3 +1,16 @@ +# NAV11 experimental graph qualification + +The package graph/public API stays unchanged: nine libraries plus CLI; Navigation56 +and Inspection95. Graph symbols are package-only. Exact discovery is 108 Inspection +functions including G01–G32, with five Inspection sharing functions. All previous +identities, 49 core cap probes and 126 maintenance witnesses remain required. Native +and Linux add the graph context and package-boundary codegen/runtime qualifiers; +the original two-positive/twenty-negative ownership matrix is unchanged. Exact +identities and genuine primary diagnostics are checked, including crash rejection. + +The current source-bound receipts establish outcomes; the dated sections below +retain their original source scopes. Controlled performance remains HOLD. + # NAV8 Navigation V1 integration Current required discovery retains 779 native /763 Linux /675 WASI functions, diff --git a/Documentation/Qualification/GraphCases.json b/Documentation/Qualification/GraphCases.json new file mode 100644 index 0000000..4f9852a --- /dev/null +++ b/Documentation/Qualification/GraphCases.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "status": "experimental package-only", + "functions": [ + "JSONInspectionTests.graphG01EmptySourceAndNoValidStart()", + "JSONInspectionTests.graphG02IndependentOracleAndSourceOrder()", + "JSONInspectionTests.graphG03ScalarDistinctIDs()", + "JSONInspectionTests.graphG04SpecialIDsAreNotPaths()", + "JSONInspectionTests.graphG05DuplicateNodeLocations()", + "JSONInspectionTests.graphG06EqualPayloadsPreserveRows()", + "JSONInspectionTests.graphG07MissingNodeID()", + "JSONInspectionTests.graphG08NullNodeID()", + "JSONInspectionTests.graphG09WrongNodeIDKinds()", + "JSONInspectionTests.graphG10NonobjectNode()", + "JSONInspectionTests.graphG11MissingEdgeSource()", + "JSONInspectionTests.graphG12NullEdgeTarget()", + "JSONInspectionTests.graphG13WrongEndpointKind()", + "JSONInspectionTests.graphG14NonobjectEdge()", + "JSONInspectionTests.graphG15DanglingAndEmpty()", + "JSONInspectionTests.graphG16DuplicateOrderedEdge()", + "JSONInspectionTests.graphG17ReverseEdgesAreDistinct()", + "JSONInspectionTests.graphG18SelfEdgeIsVisitedOnce()", + "JSONInspectionTests.graphG19CycleTerminates()", + "JSONInspectionTests.graphG20IsolatesRemainSourceNodes()", + "JSONInspectionTests.graphG21SourceEdgeNeighborOrder()", + "JSONInspectionTests.graphG22DiamondEnqueuesOnce()", + "JSONInspectionTests.graphG23TypedPathsAndPointerDifferential()", + "JSONInspectionTests.graphG24OverlapAndDuplicateRootsFailBeforeRows()", + "JSONInspectionTests.graphG25WholeSourceAdmissionAndFreshQueries()", + "JSONInspectionTests.graphG26InclusiveWorkAndRetention()", + "JSONInspectionTests.graphG27EveryCapacityAndCheckedOverflow()", + "JSONInspectionTests.graphG28CancellationStopsBuildAndBFS()", + "JSONInspectionTests.graphG29FailurePrecedenceAndTerminalLatch()", + "JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()", + "JSONInspectionTests.graphG31CapturedRootAndSourceMapCoherence()", + "JSONInspectionTests.graphG32CheckedMetadataAndExistingPublicSemantics()" + ], + "fixture_cases": 27, + "context_positive_codegen": 2, + "context_negative_codegen": 8, + "graph_positive_codegen": 2, + "graph_negative_codegen": 18 +} diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 6f5fc8a..444f23b 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 108 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -35,7 +35,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 5 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -76,7 +76,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 108 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,7 +89,7 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 108 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -103,7 +103,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 5 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/Package.swift b/Package.swift index dcdd32c..e8928a2 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,7 @@ let package = Package( // Opt-in N1 product candidate; promotion does not change core navigation semantics. .target(name: "JSONNavigation", dependencies: ["JSON"]), .target(name: "JSONInspection", dependencies: ["JSON", "JSONNavigation"]), - .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"], resources: [.copy("ProjectionFixtures")]), + .testTarget(name: "JSONInspectionTests", dependencies: ["JSONInspection", "JSONNavigation", "JSON"], resources: [.copy("ProjectionFixtures"), .copy("GraphFixtures")]), .testTarget(name: "JSONNavigationTests", dependencies: ["JSONNavigation", "JSON"], exclude: ["Fixtures"]), .target(name: "JSONBridges", dependencies: ["JSON"]), .testTarget(name: "JSONIntegrationTests", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch"]), diff --git a/README.md b/README.md index 91ad1db..8e50b7d 100644 --- a/README.md +++ b/README.md @@ -716,3 +716,9 @@ location/value/span lookup is an explicitly separate full-source-admitted operat Ordinals are snapshot-local trusted positions, not durable identity. Logical retention is not RSS; G4 controlled performance remains HOLD, with no speed claim. These additions are absent from published 1.6.1 and do not publish 1.7.0. + +NAV11 experiments with a package-only, exact source-backed directed simple graph +inside JSONInspection. Graph construction and BFS are not public API and are not +part of stable 1.6.1. The [experimental contract](Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md) +records finite capacities, source/error precedence and one-ledger ownership. There +is no graph product, performance claim or automatic promotion. diff --git a/Scripts/ci.py b/Scripts/ci.py index 096ea07..7274d5e 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -5,6 +5,7 @@ from archive_candidate import files from preflight import collect, collect_linux from testing_events import read_events, ROOT_PARAMETER_CASES +from graph_acceptance import check_graph_cases, check_graph_receipt from projection_acceptance import check_projection_cases, check_public_projection_receipt from test_discovery import DISCOVERY, ROOT_COUNTS, SHARED_COUNTS, PARAMETER_INVOCATIONS, check_restored @@ -16,7 +17,7 @@ SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' -SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics' +SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics|graphG30ConcurrentViewsHaveIndependentHealth' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -27,12 +28,14 @@ def commands(lane, output): output = output.relative_to(ROOT) if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], - ['swift', 'test', '--filter', 'inspection|snapshot|projectionP'], - ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP'], - ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP'], + ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|graphG'], + ['swift', 'test', '-c', 'release', '--filter', 'inspection|snapshot|projectionP|graphG'], + ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/inspection-address', '--filter', 'inspection|snapshot|projectionP|graphG'], ['swift', 'build', '--target', 'JSONInspection', '--scratch-path', '.build/inspection-strict', '-Xswiftc', '-strict-memory-safety', '-Xswiftc', '-warnings-as-errors'], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], + ['python3', '-B', 'Scripts/qualify_graph_context.py', '--output', str(output / 'graph-context')], + ['python3', '-B', 'Scripts/qualify_graph.py', '--output', str(output / 'graph-compiler')], ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')]] return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] @@ -174,6 +177,8 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')], ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], + ['python3', '-B', 'Scripts/qualify_graph_context.py', '--output', str(output / 'graph-context')], + ['python3', '-B', 'Scripts/qualify_graph.py', '--output', str(output / 'graph-compiler')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -232,20 +237,20 @@ def check_result(command, code, log, events=None): expected = [DISCOVERY['root_functions']['JSONNavigationTests']] if events is not None: passed = passed and events['module_function_counts'] == {'JSONNavigationTests': expected[0]} and events['parameter_case_invocations'] == 0 - if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP']): + if any(f in command for f in ['inspection', 'inspection|snapshot|projectionP|graphG']): expected = [DISCOVERY['root_functions']['JSONInspectionTests']] if events is not None: - passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 and check_projection_cases(events) + passed = passed and events['module_function_counts'] == {'JSONInspectionTests': expected[0]} and events['parameter_case_invocations'] == 0 and check_projection_cases(events) and check_graph_cases(events) passed = passed and sorted(counts) == expected if 'Tools/CompositionQualification' in command and events is not None: passed = passed and events['module_function_counts'] == {'CompositionScaleTests': DISCOVERY['composition_external_functions']} and events['parameter_case_invocations'] == 0 if 'qa' in command and events is not None: passed = passed and events['module_function_counts'] == DISCOVERY['qa_functions'] and check_restored(events) if 'thread' in command and 'JSONSchemaExamplesTests' not in command and events is not None: - passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) + passed = passed and events['module_function_counts'] == SHARED_COUNTS and check_projection_cases(events, shared=True) and check_graph_cases(events, shared=True) if '--filter' not in command and '--test-product' not in command and not any(x.startswith('Tools/') for x in command): if events is not None: - passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) + passed = passed and events['module_function_counts'] == ROOT_COUNTS and check_restored(events) and check_projection_cases(events) and check_graph_cases(events) passed = passed and (events['parameterized_functions'] == ROOT_PARAMETER_CASES if events is not None else sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log)) == PARAMETER_INVOCATIONS) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) @@ -304,6 +309,9 @@ def check_result(command, code, log, events=None): passed = passed and 'TESTING_EVENT_RECEIPTS passed cases=19' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_ergonomics.py']: passed = passed and 'ERGONOMICS_COMPILER passed commands=8 guide_fences=3' in log.splitlines() + if command[:3] in (['python3', '-B', 'Scripts/qualify_graph_context.py'], ['python3', '-B', 'Scripts/qualify_graph.py']): + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_graph_receipt(data, context='qualify_graph_context.py' in command[2]) if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_snapshot.py']: @@ -328,14 +336,17 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=17' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=18' in log.splitlines() return passed, counts def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--lane', choices=['composition', 'navigation', 'inspection', 'native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli', 'schema-examples', 'schema-examples-safety', 'schema-examples-determinism', 'schema-examples-construction', 'schema-examples-advanced', 'schema-examples-qualification', 'coverage', 'linux-coverage', 'linux-qa', 'wasm-qa', 'archive'], required=True) parser.add_argument('--output') + parser.add_argument('--skip-host-measurements', action='store_true', help='For WASI qualification: leave optional host timings explicitly NOT RUN.') args = parser.parse_args() + if args.skip_host_measurements and args.lane not in ('wasm', 'wasm-qa'): + parser.error('--skip-host-measurements applies only to WASI lanes') output = ROOT / (args.output or '.build/ci/' + args.lane) output.mkdir(parents=True, exist_ok=True) inputs = snapshot() @@ -352,6 +363,8 @@ def save(): return 2 receipt['status'] = 'running' for index, command in enumerate(commands(args.lane, output)): + if args.skip_host_measurements and command[:3] == ['python3', '-B', 'Scripts/wasm_support.py']: + command += ['--skip-host-measurements'] plugin_path = os.environ.get('SWIFT_JSON_TESTING_PLUGIN_PATH') if command[:2] == ['swift', 'test'] and plugin_path: command += ['-Xswiftc', '-plugin-path', '-Xswiftc', plugin_path] diff --git a/Scripts/graph_acceptance.py b/Scripts/graph_acceptance.py new file mode 100644 index 0000000..57c6f9a --- /dev/null +++ b/Scripts/graph_acceptance.py @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Exact graph identities and genuine compiler outcomes; no broad exit-zero gate.""" +import json,pathlib +ROOT=pathlib.Path(__file__).resolve().parents[1] +POLICY=json.loads((ROOT/'Documentation/Qualification/GraphCases.json').read_text()) +EXPECTED=frozenset(POLICY['functions']) +SHARED=frozenset({'JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()'}) +def check_graph_cases(events,shared=False): + names=[n for n in events['function_names'] if n.startswith('JSONInspectionTests.graphG')] + return len(names)==len(set(names)) and set(names)==(SHARED if shared else EXPECTED) +def check_graph_receipt(data,context=False): + commands=data.get('commands',[]);expected_count=18 if context else 28;negative_count=8 if context else 18 + if data.get('status')!='passed' or len(commands)!=expected_count or len({c.get('name') for c in commands})!=expected_count:return False + negatives=[c for c in commands if c.get('expected_diagnostic')] + if len(negatives)!=negative_count:return False + for c in commands: + if c.get('status')!='passed' or c.get('compiler_crash') is not False:return False + if c.get('expected_diagnostic'): + if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'].lower() in d.lower() for d in c.get('primary_diagnostics',[])):return False + elif c.get('exit_code')!=0:return False + return True diff --git a/Scripts/qualify_graph.py b/Scripts/qualify_graph.py new file mode 100644 index 0000000..6e64e12 --- /dev/null +++ b/Scripts/qualify_graph.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Full-codegen proof of experimental graph invariants and package-only boundaries.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT = pathlib.Path(__file__).resolve().parents[1] +POSITIVE = r'''import JSON +import JSONNavigation +import JSONInspection +@main struct GraphConsumer { + static func check(_ value: Bool) { precondition(value) } + static func main() throws { + let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let document = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"é","n":9007199254740993},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#)) + let built = try JSONGraphProjection.build(document: document, profile: profile) + check(built.result.nodeCount == 2 && built.result.edgeCount == 1) + print("PRIVATE_GRAPH exact passed") + check(try built.result.breadthFirst(fromNode: 0).result == [0,1]) + print("PRIVATE_GRAPH bfs passed") + check(try built.result.outgoingEdges(of: 0).result == [0]) + print("PRIVATE_GRAPH adjacency passed") + check(try built.result.nodeValue(at: 0).result.objectValue?["n"]?.numberValue?.integer(as: UInt64.self) == 9007199254740993) + print("PRIVATE_GRAPH values passed") + let baseline = try JSONTraversal(document: document).visit { _,_ in .continue } + check(built.statistics[.nodes] == baseline.statistics[.nodes] && built.statistics[.retainedBytes] > 0) + print("PRIVATE_GRAPH admission passed") + } +} +''' +PROBES = [ + ('positive', POSITIVE, None, False), + ('external-graph', 'import JSONInspection\nlet x = JSONGraphProjection.self\n', 'cannot find', True), + ('external-profile', 'import JSONInspection\nlet x = JSONGraphProfile.self\n', 'cannot find', True), + ('external-failure', 'import JSONInspection\nlet x = JSONGraphFailure.self\n', 'cannot find', True), + ('external-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find', True), + ('external-context', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find', True), + ('core-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find', True), + ('immutable-ids', 'import JSONInspection\nfunc f(_ x: inout JSONGraphProjection) { x.nodeIDs.removeAll() }\n', "'nodeIDs' is a 'let' constant", False), + ('forged-graph', 'import JSON\nimport JSONNavigation\nimport JSONInspection\nfunc f(_ d: JSONDocument, _ p: JSONGraphProfile, _ s: JSONTraversalSummary) { _ = JSONGraphProjection(document: d, profile: p, nodeIDs: [], edges: [], offsets: [], adjacency: [], traversal: s) }\n', 'initializer is inaccessible', False), + ('private-reducer', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find', False), +] +CHECKS = {'exact', 'bfs', 'adjacency', 'values', 'admission'} + +def main(): + parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--output', required=True) + out = ROOT / parser.parse_args().output; out.mkdir(parents=True, exist_ok=True) + receipt = dict(status='running', scope='same-package graph proof; all graph declarations remain non-public', + source_inputs=[dict(path=p.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files()], commands=[]) + def save(): (out / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr; diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + crash = result.returncode < 0 or any(s in text for s in ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation', 'Found ownership error']) + passed = not crash and (result.returncode == 0 if expected is None else result.returncode > 0 and any(expected.lower() in d.lower() for d in diagnostics)) + log = out / (name + '.log'); log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], exit_code=result.returncode, + status='passed' if passed else 'failed', compiler_crash=crash, expected_diagnostic=expected, + primary_diagnostics=diagnostics, log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: raise ValueError('Unexpected graph compiler outcome: ' + name) + return result.stdout + try: + identity = re.sub(r'[^a-zA-Z0-9_]', '_', ROOT.name.lower()) + if not identity or identity[0].isdigit(): identity = '_' + identity + for config in ['debug', 'release']: + run(config + '-build', ['swift','build','-c',config,'--target','JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift','build','-c',config,'--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + common = ['swiftc','-swift-version','6','-warnings-as-errors','-I',str(modules),'-Onone' if config == 'debug' else '-O','-parse-as-library'] + for name, text, diagnostic, external in PROBES: + source = out / (name + '.swift'); source.write_text(text) + run(config + '-' + name, common + ['-package-name','graph_external' if external else identity, + '-module-name','GraphCompilerProbe','-c',str(source),'-o',str(out / (config + '-' + name + '.o'))], diagnostic) + objects = [] + for module in ['JSON','JSONNavigation','JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: raise ValueError('Missing objects: ' + module) + objects.extend(str(p) for p in selected) + binary = out / ('private-' + config) + run(config + '-link', common + ['-package-name',identity,str(out/'positive.swift'),*objects,'-o',str(binary)]) + output = run(config + '-run', [str(binary)]) + checks = re.findall(r'^PRIVATE_GRAPH (\S+) passed$', output, re.M) + if len(checks) != 5 or set(checks) != CHECKS: raise ValueError('Private projection consumer identity/count drift') + receipt['status'] = 'passed' + except (ValueError, OSError, subprocess.TimeoutExpired) as error: receipt.update(status='failed', error=str(error)) + save(); print('GRAPH_COMPILER ' + receipt['status'] + ' commands=28 positive_codegen=2 negative_codegen=18 private_cases=5/config') + return 0 if receipt['status'] == 'passed' else 1 +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_graph_context.py b/Scripts/qualify_graph_context.py new file mode 100644 index 0000000..13619df --- /dev/null +++ b/Scripts/qualify_graph_context.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Code-generation and original-meter proof for the graph's exclusive delegates.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT=pathlib.Path(__file__).resolve().parents[1] +POSITIVE='''import JSON +import JSONNavigation +struct Probe: JSONTraversalOwnedReducer { + var strings: [String] = [] + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { + if hit.relativeDepth == 0 { try context.retainKey("id", admittingCallerInput: true); try context.release(2); _ = try context.arrayCount(value, at: hit.absoluteLocation); return .continue } + let field = try context.directStringMember(value, key: "id", at: hit.absoluteLocation) + precondition(field.kind == .string) + if let text = field.text { let equal = try context.equalStrings(text, text); precondition(equal); strings.append(text) } + return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> Int { + for text in strings { try context.releaseStringAlias(text) } + try context.work(7); return strings.count + } +} +@main struct Main { + static func main() throws { + let document = JSONDocument(root: try JSON.parse(#"{"n":[{"id":"é"}],"e":[{"id":"e\\u0301"}]}"#)) + let walk = JSONTraversal(document: document) + let roots: JSONTraversalScope = .virtual(roots: [[.key("n")],[.key("e")]], overlap: .rejectAmbiguous) + let base = try walk.visit(scope: roots, options: .init(depthLimit: 1)) { _,_ in .continue } + var reducer = Probe() + let owned = try walk.inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &reducer) + precondition(owned.result == 2) + precondition(owned.statistics[.nodes] == base.statistics[.nodes]) + precondition(owned.statistics[.workUnits] > base.statistics[.workUnits]) + precondition(owned.statistics[.retainedBytes] == base.statistics[.retainedBytes]) + precondition(reducer.strings.count == 2) + var exact = Probe(); var limits = JSONLimits.balanced + limits[.workUnits] = owned.statistics[.workUnits] + _ = try JSONTraversal(document: document, limits: limits).inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &exact) + limits[.workUnits] -= 1 + do { var short = Probe(); _ = try JSONTraversal(document: document, limits: limits).inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &short); preconditionFailure() } + catch let error as JSONFailure { precondition(error.category == .resource) } + print("GRAPH_CONTEXT parity passed nodes=\\(owned.statistics[.nodes]) work=\\(owned.statistics[.workUnits]) retained=\\(owned.statistics[.retainedBytes])") + } +} +''' +PROBES={ + 'positive':(POSITIVE,None), + 'bound':('func f(_ c: inout JSONTraversalContext) { _ = c.directStringMember }', "cannot reference 'mutating' method as function value"), + 'escape':('func f(_ c: inout JSONTraversalContext) -> () throws -> Bool { { try c.equalStrings("a", "a") } }', "escaping closure captures 'inout' parameter"), + 'task':('func f(_ c: inout JSONTraversalContext) { _ = Task { try c.releaseStringAlias("a") } }', "escaping closure captures 'inout' parameter"), + 'store':('final class Box { var c: JSONTraversalContext? }; func f(_ c: inout JSONTraversalContext) -> Box { let b = Box(); b.c = consume c; return b }', 'missing reinitialization of inout parameter'), +} +def main(): + ap=argparse.ArgumentParser();ap.add_argument('--output',required=True);out=ROOT/ap.parse_args().output;out.mkdir(parents=True,exist_ok=True) + receipt={'status':'running','source_inputs':[{'path':p.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()],'commands':[]} + def save(): (out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + def run(name,args,expected=None): + p=subprocess.run(args,cwd=ROOT,capture_output=True,text=True,timeout=900);data=p.stdout+p.stderr + diagnostics=re.findall(r': error: ([^\n]+)',data);crash=p.returncode<0 or any(x in data for x in ['Stack dump:','Assertion failed','Found ownership error','fatal error encountered during compilation']) + passed=not crash and (p.returncode==0 if expected is None else p.returncode>0 and any(expected.lower() in d.lower() for d in diagnostics)) + log=out/(name+'.log');log.write_text(data) + receipt['commands'].append({'name':name,'command':args,'exit_code':p.returncode,'status':'passed' if passed else 'failed','expected_diagnostic':expected,'primary_diagnostics':diagnostics,'compiler_crash':crash,'log':log.relative_to(ROOT).as_posix(),'log_sha256':hashlib.sha256(log.read_bytes()).hexdigest()});save() + if not passed: raise ValueError(name) + return p.stdout + try: + identity=re.sub('[^a-zA-Z0-9_]','_',ROOT.name.lower()) + if identity[0].isdigit():identity='_'+identity + for config in ['debug','release']: + run(config+'-build',['swift','build','-c',config,'--target','JSONNavigation']) + bp=pathlib.Path(run(config+'-binpath',['swift','build','-c',config,'--show-bin-path']).strip());mods=bp/'Modules' if (bp/'Modules').is_dir() else bp + common=['swiftc','-swift-version','6','-warnings-as-errors','-package-name',identity,'-I',str(mods),'-O' if config=='release' else '-Onone','-parse-as-library'] + for name,(body,expected) in PROBES.items(): + f=out/(name+'.swift');f.write_text(body if name=='positive' else 'import JSON\nimport JSONNavigation\n'+body+'\n') + run(config+'-'+name,common+['-c',str(f),'-o',str(out/(config+'-'+name+'.o'))],expected) + objects=[] + for module in ['JSON','JSONNavigation']: + aggregate=bp/(module+'.o');objects+= [str(aggregate)] if aggregate.exists() else [str(p) for p in sorted((bp/(module+'.build')).glob('*.o'))] + exe=out/('parity-'+config);run(config+'-link',common+[str(out/'positive.swift'),*objects,'-o',str(exe)]) + output=run(config+'-run',[str(exe)]) + if len(re.findall(r'^GRAPH_CONTEXT parity passed nodes=\d+ work=\d+ retained=\d+$',output,re.M))!=1:raise ValueError('Missing parity witness') + receipt['status']='passed' + except (ValueError,OSError,subprocess.TimeoutExpired) as e:receipt.update(status='failed',error=str(e)) + save();print('GRAPH_CONTEXT '+receipt['status']+' positive=2 negative=8 runtime=2');return 0 if receipt['status']=='passed' else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 843ec53..73360f0 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -12,6 +12,7 @@ from coverage import executable_objects, shared_objects, validate_export, without_schema_objects from test_discovery import DISCOVERY, ROOT_COUNTS, PARAMETER_INVOCATIONS, check_root, check_restored, RESTORED_FUNCTIONS from ci import check_result, commands +from graph_acceptance import EXPECTED as GRAPH_FUNCTIONS, check_graph_cases, check_graph_receipt from projection_acceptance import EXPECTED as PROJECTION_FUNCTIONS, PUBLIC_CHECKS, check_projection_cases, check_public_projection_receipt class HarnessTests(unittest.TestCase): @@ -61,11 +62,11 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel # Combined-source checks must retain both independently owned optional products. for module, count, command in [ ('JSONNavigationTests', 47, ['swift', 'test', '--filter', 'navigation']), - ('JSONInspectionTests', 76, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP']), + ('JSONInspectionTests', 108, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|graphG']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, - parameter_case_invocations=0, function_names=sorted(PROJECTION_FUNCTIONS) if module == 'JSONInspectionTests' else []) + parameter_case_invocations=0, function_names=sorted(PROJECTION_FUNCTIONS | GRAPH_FUNCTIONS) if module == 'JSONInspectionTests' else []) self.assertTrue(check_result(command, 0, '', events)[0]) for broken in [dict(events, bundle_counts=[count-1]), dict(events, module_function_counts={'WrongModule': count}), @@ -75,12 +76,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 47) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONInspectionTests'], 76) + self.assertEqual(policy[key]['JSONInspectionTests'], 108) 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'], 4) + self.assertEqual(policy[key]['JSONInspectionTests'], 5) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): @@ -95,6 +96,23 @@ def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(s self.assertTrue(check_projection_cases({'function_names': shared}, shared=True)) self.assertFalse(check_projection_cases({'function_names': []}, shared=True)) + def test_graph_identity_and_compiler_receipts_reject_false_success(self): + names=sorted(GRAPH_FUNCTIONS) + self.assertEqual(len(names),32) + self.assertTrue(check_graph_cases({'function_names':names})) + for broken in [names[:-1],names+names[:1],['WrongModule.'+names[0].split('.',1)[1]]+names[1:]]: + self.assertFalse(check_graph_cases({'function_names':broken})) + for context,count,negatives in [(True,18,8),(False,28,18)]: + commands=[dict(name=str(n),status='passed',compiler_crash=False,exit_code=1 if n diff --git a/Sources/JSONInspection/Graph/JSONGraphProfile.swift b/Sources/JSONInspection/Graph/JSONGraphProfile.swift new file mode 100644 index 0000000..3ab48e4 --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphProfile.swift @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Experimental explicit direct-field profile; all locations are absolute/typed. +package struct JSONGraphProfile: Sendable { + package let nodesAt: [JSONLocationComponent] + package let edgesAt: [JSONLocationComponent] + package let nodeIDKey: JSONKey + package let edgeSourceKey: JSONKey + package let edgeTargetKey: JSONKey + package init(nodesAt: [JSONLocationComponent], edgesAt: [JSONLocationComponent], + nodeIDKey: JSONKey, edgeSourceKey: JSONKey, edgeTargetKey: JSONKey) { + self.nodesAt = nodesAt; self.edgesAt = edgesAt; self.nodeIDKey = nodeIDKey + self.edgeSourceKey = edgeSourceKey; self.edgeTargetKey = edgeTargetKey + } +} + +/// Graph semantic refusals retain exact original locations, never normalized IDs. +package enum JSONGraphFailure: Error, Sendable, Equatable { + case nodeLimitExceeded(limit: Int) + case edgeLimitExceeded(limit: Int) + case rowNotObject(at: [JSONLocationComponent], kind: JSONTraversalKind) + case idMissing(at: [JSONLocationComponent]) + case idNull(at: [JSONLocationComponent]) + case idWrongKind(at: [JSONLocationComponent], kind: JSONTraversalKind) + case duplicateNodeID(first: [JSONLocationComponent], second: [JSONLocationComponent]) + case danglingEndpoint(at: [JSONLocationComponent]) + case duplicateDirectedEdge(first: [JSONLocationComponent], second: [JSONLocationComponent]) + case nodeOutOfBounds(Int) + case edgeOutOfBounds(Int) + case frontierLimitExceeded(limit: Int) + case visitedLimitExceeded(limit: Int) + case resultLimitExceeded(limit: Int) +} + +/// Trusted row ordinals in this captured source, not durable graph identifiers. +package struct JSONGraphEdgeRecord: Sendable, Equatable { + package let row: Int + package let source: Int + package let target: Int +} diff --git a/Sources/JSONInspection/Graph/JSONGraphProjection.swift b/Sources/JSONInspection/Graph/JSONGraphProjection.swift new file mode 100644 index 0000000..23658bb --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphProjection.swift @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Experimental immutable source-attached directed simple graph. Not public API. +package struct JSONGraphProjection: Sendable { + package let document: JSONDocument + package let profile: JSONGraphProfile + package let nodeIDs: [String] + package let edges: [JSONGraphEdgeRecord] + package let offsets: [Int] + package let adjacency: [Int] + package let traversal: JSONTraversalSummary + package var nodeCount: Int { nodeIDs.count } + package var edgeCount: Int { edges.count } + fileprivate init(document: JSONDocument, profile: JSONGraphProfile, nodeIDs: [String], + edges: [JSONGraphEdgeRecord], offsets: [Int], adjacency: [Int], + traversal: JSONTraversalSummary) { + self.document = document; self.profile = profile; self.nodeIDs = nodeIDs + self.edges = edges; self.offsets = offsets; self.adjacency = adjacency; self.traversal = traversal + } + + /// Complete source admission, nodes, edges and adjacency share one original meter. + package static func build(document: JSONDocument, profile: JSONGraphProfile, + limits: JSONLimits = .balanced, maximumNodes: Int = 10_000, + maximumEdges: Int = 50_000) throws -> JSONOperationReport { + guard maximumNodes >= 0, maximumEdges >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + var reducer = GraphBuilder(profile: profile, limits: limits, maximumNodes: maximumNodes, maximumEdges: maximumEdges) + return try JSONTraversal(document: document, limits: limits).inspectOwned( + scope: .virtual(roots: [profile.nodesAt, profile.edgesAt], overlap: .rejectAmbiguous), + options: .init(depthLimit: 1), reducer: &reducer) + } + + /// A new whole-source operation; returns original physical outgoing edge ordinals. + package func outgoingEdges(of node: Int, limits: JSONLimits = .balanced, maximumResults: Int = 50_000) + throws -> JSONOperationReport<[Int]> { + guard maximumResults >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + try checkNode(node) + var reducer = GraphQuery(graph: self, query: .outgoing(node), limits: limits, + maximumFrontier: 0, maximumVisited: 0, maximumResults: maximumResults) + return try JSONTraversal(document: document, limits: limits) + .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) + } + + /// Complete reachable component, including start; never a partial success. + package func breadthFirst(fromNode node: Int, limits: JSONLimits = .balanced, + maximumFrontier: Int = 10_000, maximumVisited: Int = 10_000, + maximumResults: Int = 10_000) throws -> JSONOperationReport<[Int]> { + guard maximumFrontier >= 0, maximumVisited >= 0, maximumResults >= 0 else { + throw JSONFailure(.configuration, code: .invalidLimits) + } + try checkNode(node) + var reducer = GraphQuery(graph: self, query: .breadthFirst(node), limits: limits, + maximumFrontier: maximumFrontier, maximumVisited: maximumVisited, + maximumResults: maximumResults) + return try JSONTraversal(document: document, limits: limits) + .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) + } + + package func nodeValue(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + try checkNode(node) + return try value(at: profile.nodesAt, row: node, limits: limits) + } + package func edgeValue(at edge: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + guard edge >= 0, edge < edgeCount else { throw JSONGraphFailure.edgeOutOfBounds(edge) } + return try value(at: profile.edgesAt, row: edge, limits: limits) + } + /// Source-map lookup under a new explicit full-source admission. + package func nodeSpan(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + try checkNode(node) + let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } + let state = try scope.check(); let path = try rowPath(profile.nodesAt, row: node, scope: scope, state: state) + let span = try document.sourceMap?.span(at: path.components, kind: .value, meter: &state.meter) + return try scope.complete(span) + } + private func checkNode(_ node: Int) throws { + guard node >= 0, node < nodeCount else { throw JSONGraphFailure.nodeOutOfBounds(node) } + } + private func value(at root: [JSONLocationComponent], row: Int, limits: JSONLimits) throws -> JSONOperationReport { + let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } + let state = try scope.check(); let path = try rowPath(root, row: row, scope: scope, state: state) + let value = try scope.run(path.components) { try scope.select(path.components, state: $0) } + // A returned JSON alias remains owned through handoff, independently of input. + try JSONAdmission.value(value, meter: &state.meter, owned: true) + return try scope.complete(value) + } + private func rowPath(_ root: [JSONLocationComponent], row: Int, scope: JSONReadScope, + state: JSONCodingState) throws -> JSONReadPath { + var path = JSONReadPath(scope: scope, components: [], bytes: 0) + for part in root { try state.meter.work(); path = try scope.append(part, to: path.components, state: state) } + return try scope.append(.index(row), to: path.components, state: state) + } +} + +internal enum GraphWorkStage: Sendable, Equatable { case nodeIdentity, endpointIdentity, edgePair, adjacency, breadthFirst } + +internal struct GraphBuilder: JSONTraversalOwnedReducer { + /// Qualification-only phase marker; no scope/capability is exposed. + var workObserver: (@Sendable (GraphWorkStage) -> Void)? = nil + let profile: JSONGraphProfile + let limits: JSONLimits + let maximumNodes: Int + let maximumEdges: Int + var ids: [String] = [] + var edges: [JSONGraphEdgeRecord] = [] + var nodeTotal = 0, edgeTotal = 0 + + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work() + if hit.relativeDepth == 0 { + let count = try context.arrayCount(value, at: hit.absoluteLocation) + if hit.scopeRootIndex == 0 { + guard count <= maximumNodes else { throw JSONGraphFailure.nodeLimitExceeded(limit: maximumNodes) } + nodeTotal = count + _ = try ProjectionAccounting.next(count, limits: limits) + try context.retain(256) + _ = try context.retainLocation(profile.nodesAt); _ = try context.retainLocation(profile.edgesAt) + for key in [profile.nodeIDKey, profile.edgeSourceKey, profile.edgeTargetKey] { + try context.work(); try context.retain(32); try context.retainKey(key, admittingCallerInput: true) + } + } else { + guard count <= maximumEdges else { throw JSONGraphFailure.edgeLimitExceeded(limit: maximumEdges) } + edgeTotal = count + } + return .continue + } + guard case .object = value else { throw JSONGraphFailure.rowNotObject(at: try context.retainLocation(hit.absoluteLocation), kind: hit.kind) } + if hit.scopeRootIndex == 0 { + let id = try string(value, key: profile.nodeIDKey, at: hit.absoluteLocation, context: &context) + for prior in ids.indices { + try context.work() + workObserver?(.nodeIdentity) + if try context.equalStrings(ids[prior], id) { + throw JSONGraphFailure.duplicateNodeID( + first: try context.appending(.index(prior), to: profile.nodesAt), + second: try context.retainLocation(hit.absoluteLocation)) + } + } + _ = try ProjectionAccounting.next(ids.count, limits: limits) + try context.retain(16); ids.append(id) + } else { + // Required fields are checked source then target before resolving either. + let source = try string(value, key: profile.edgeSourceKey, at: hit.absoluteLocation, context: &context) + let target = try string(value, key: profile.edgeTargetKey, at: hit.absoluteLocation, context: &context) + let from = try resolve(source, key: profile.edgeSourceKey, at: hit.absoluteLocation, context: &context) + let to = try resolve(target, key: profile.edgeTargetKey, at: hit.absoluteLocation, context: &context) + for prior in edges.indices { + try context.work() + workObserver?(.edgePair) + if edges[prior].source == from, edges[prior].target == to { + throw JSONGraphFailure.duplicateDirectedEdge( + first: try context.appending(.index(prior), to: profile.edgesAt), + second: try context.retainLocation(hit.absoluteLocation)) + } + } + _ = try ProjectionAccounting.next(edges.count, limits: limits) + try context.retain(24); edges.append(.init(row: edges.count, source: from, target: to)) + try context.releaseStringAlias(source); try context.releaseStringAlias(target) + } + return .skipDescendants + } + private func string(_ value: JSON, key: JSONKey, at row: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> String { + let member = try context.directStringMember(value, key: key, at: row) + if let text = member.text { return text } + let path = try context.appending(.key(key), to: row) + guard let kind = member.kind else { throw JSONGraphFailure.idMissing(at: path) } + if kind == .null { throw JSONGraphFailure.idNull(at: path) } + throw JSONGraphFailure.idWrongKind(at: path, kind: kind) + } + private func resolve(_ text: String, key: JSONKey, at row: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> Int { + for index in ids.indices { + try context.work() + workObserver?(.endpointIdentity) + if try context.equalStrings(ids[index], text) { return index } + } + throw JSONGraphFailure.danglingEndpoint(at: try context.appending(.key(key), to: row)) + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> JSONGraphProjection { + guard ids.count == nodeTotal, edges.count == edgeTotal else { throw JSONFailure(.configuration, code: .invalidOperation) } + var offsets: [Int] = [], adjacency: [Int] = [] + try context.work(); try context.retain(8); offsets.append(0) + // Deterministic source-ordered CSR, deliberately bounded linear scans. + for node in ids.indices { + try context.work() + for edge in edges.indices { + workObserver?(.adjacency) + try context.work() + if edges[edge].source == node { + _ = try ProjectionAccounting.next(adjacency.count, limits: limits) + try context.retain(8); adjacency.append(edge) + } + } + _ = try ProjectionAccounting.next(offsets.count, limits: limits) + try context.retain(8); offsets.append(adjacency.count) + } + try context.work() + let document = try context.transferDocumentOwnership() + return .init(document: document, profile: profile, nodeIDs: ids, edges: edges, + offsets: offsets, adjacency: adjacency, traversal: summary) + } +} diff --git a/Sources/JSONInspection/Graph/JSONGraphQuery.swift b/Sources/JSONInspection/Graph/JSONGraphQuery.swift new file mode 100644 index 0000000..e23bc48 --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphQuery.swift @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +internal struct GraphQuery: JSONTraversalOwnedReducer { + var workObserver: (@Sendable (GraphWorkStage) -> Void)? = nil + enum Query { case outgoing(Int), breadthFirst(Int) } + let graph: JSONGraphProjection + let query: Query + let limits: JSONLimits + let maximumFrontier: Int + let maximumVisited: Int + let maximumResults: Int + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work(); return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> [Int] { + let aliases = try retainAliases(context: &context) + var result: [Int] = [] + switch query { + case .outgoing(let node): + for position in graph.offsets[node].. UInt64 { + var total: UInt64 = 0 + func retain(_ bytes: UInt64, context: inout JSONTraversalContext) throws { + try context.retain(bytes) + // Each successful retain is bounded below UInt64.max by the core. + total += bytes + } + try context.work(); try retain(256, context: &context) + for path in [graph.profile.nodesAt, graph.profile.edgesAt] { + for part in path { + try context.work(); try retain(32, context: &context) + if case .key(let key) = part { + for _ in key.string.utf8 { try context.work(); try retain(1, context: &context) } + } + } + } + for key in [graph.profile.nodeIDKey, graph.profile.edgeSourceKey, graph.profile.edgeTargetKey] { + try context.work(); try retain(32, context: &context) + for _ in key.string.utf8 { try context.work(); try retain(1, context: &context) } + } + for id in graph.nodeIDs { + try context.work(); try retain(48, context: &context) + for _ in id.utf8 { try context.work(); try retain(1, context: &context) } + } + for _ in graph.edges { try context.work(); try retain(24, context: &context) } + for _ in graph.offsets { try context.work(); try retain(8, context: &context) } + for _ in graph.adjacency { try context.work(); try retain(8, context: &context) } + return total + } +} diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift index a08e58e..4c9289a 100644 --- a/Sources/JSONNavigation/JSONTraversalContext.swift +++ b/Sources/JSONNavigation/JSONTraversalContext.swift @@ -34,6 +34,34 @@ package struct JSONTraversalContext: ~Copyable { package mutating func equalKeys(_ lhs: JSONKey, _ rhs: JSONKey) throws -> Bool { try perform { try JSONComparison.textEquals(lhs.string, rhs.string, meter: &$0.meter) } } + /// Scalar-exact comparison charged to the original admitted scope. + package mutating func equalStrings(_ lhs: String, _ rhs: String) throws -> Bool { + try perform { try JSONComparison.textEquals(lhs, rhs, meter: &$0.meter) } + } + /// Direct-member string selection. Nil kind means absent; null and other + /// kinds remain distinguishable. A returned string owns 32+UTF8 logical bytes. + /// No JSONKey hashing, second admission, cursor or ledger is created. + package mutating func directStringMember(_ value: JSON, key: JSONKey, + at path: [JSONLocationComponent]) throws + -> (text: String?, kind: JSONTraversalKind?) { + try perform(at: path) { state in + try JSONAdmission.text(key.string, key: true, meter: &state.meter) + try state.meter.work() + guard case .object(let object) = value else { throw JSONFailure(.typeMismatch, code: .wrongKind) } + guard let member = try object.lookup(key.string, meter: &state.meter) else { return (nil, nil) } + guard case .string(let text) = member else { return (nil, JSONTraversalKind(member)) } + try state.meter.retain(32) + for _ in text.utf8 { try state.meter.work(); try state.meter.retain(1) } + return (text, .string) + } + } + /// Relinquish a directStringMember alias after its last owned use. + package mutating func releaseStringAlias(_ text: String) throws { + try perform { state in + for _ in text.utf8 { try state.meter.work(); try state.meter.release(1) } + try state.meter.release(32) + } + } package mutating func arrayCount(_ value: JSON, at path: [JSONLocationComponent]) throws -> Int { try perform(at: path) { state in try state.meter.work() @@ -42,8 +70,9 @@ package struct JSONTraversalContext: ~Copyable { } } /// Reserve an additional owned alias, not another text admission or key model. - package mutating func retainKey(_ key: JSONKey) throws { + package mutating func retainKey(_ key: JSONKey, admittingCallerInput: Bool = false) throws { try perform { state in + if admittingCallerInput { try JSONAdmission.text(key.string, key: true, meter: &state.meter) } for _ in key.string.utf8 { try state.meter.work(); try state.meter.retain(1) } } } diff --git a/Tests/JSONInspectionTests/GraphAcceptanceTests.swift b/Tests/JSONInspectionTests/GraphAcceptanceTests.swift new file mode 100644 index 0000000..f98d2e3 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphAcceptanceTests.swift @@ -0,0 +1,382 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Synchronization +import Testing +import JSON +import JSONNavigation +@testable import JSONInspection + +private let graphProfile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], + nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +private func graphDocument(_ name: String) throws -> JSONDocument { + // Match the existing P1 loader: WASI Foundation named lookup truncates extensions. + let root = try #require(Bundle.module.resourceURL) + let url = URL(fileURLWithPath: root.path + "/GraphFixtures/" + name + ".json") + return try JSONParser().parseDocument(Data(contentsOf: url)) +} +private func graphBuild(_ name: String, profile: JSONGraphProfile = graphProfile) throws -> JSONOperationReport { + try JSONGraphProjection.build(document: graphDocument(name), profile: profile) +} +private func graphFailure(_ body: () throws -> Void) -> JSONGraphFailure? { + do { try body(); Issue.record("Expected graph refusal"); return nil } + catch let error as JSONGraphFailure { return error } + catch { Issue.record("Unexpected error: \(error)"); return nil } +} +private func graphCore(_ body: () throws -> Void) -> JSONFailure? { + do { try body(); Issue.record("Expected core refusal"); return nil } + catch let error as JSONFailure { return error } + catch let error as JSONAccessFailure { return error.cause } + catch { Issue.record("Unexpected error: \(error)"); return nil } +} +@Test func graphG01EmptySourceAndNoValidStart() throws { + let graph = try graphBuild("g01-empty").result + #expect(graph.nodeCount == 0 && graph.edgeCount == 0 && graph.offsets == [0]) + #expect(graph.document.sourceBytes != nil) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0) } == .nodeOutOfBounds(0)) +} +@Test func graphG02IndependentOracleAndSourceOrder() throws { + struct Oracle: Decodable { let cases: [Case] } + struct Case: Decodable { let name: String; let expected: Expected } + struct Expected: Decodable { + let kind: String; let ids_codepoints: [[UInt32]]?; let edge_pairs: [[Int]]? + let outgoing_edge_ordinals: [[Int]]?; let bfs_from_node0: [Int]? + } + let bundle = try #require(Bundle.module.resourceURL) + let url = URL(fileURLWithPath: bundle.path + "/GraphFixtures/GRAPH-FIXTURE-ORACLE.json") + let oracle = try JSONValueDecoder().decode(Oracle.self, from: JSON.parse(Data(contentsOf: url))) + var evaluated = 0 + for entry in oracle.cases where entry.expected.kind == "valid" { + let profile = entry.name == "g23-numeric-object-key" ? JSONGraphProfile( + nodesAt: [.key("0"), .key("nodes")], edgesAt: [.key("0"), .key("edges")], + nodeIDKey: "key/id", edgeSourceKey: "src~", edgeTargetKey: "dst") : graphProfile + let graph = try graphBuild(entry.name, profile: profile).result + #expect(graph.nodeIDs.map { $0.unicodeScalars.map(\.value) } == entry.expected.ids_codepoints) + #expect(graph.edges.map { [$0.source, $0.target] } == entry.expected.edge_pairs) + var outgoing: [[Int]] = [] + for node in 0.. 0 { #expect(try graph.breadthFirst(fromNode: 0).result == entry.expected.bfs_from_node0) } + let document = graph.document + #expect(try JSONGraphProjection.build(document: document, profile: profile, maximumNodes: graph.nodeCount, maximumEdges: graph.edgeCount).result.edges == graph.edges) + if graph.nodeCount > 0 { + #expect(graphFailure { _ = try JSONGraphProjection.build(document: document, profile: profile, maximumNodes: graph.nodeCount - 1) } == .nodeLimitExceeded(limit: graph.nodeCount - 1)) + let reached = try #require(entry.expected.bfs_from_node0) + var seen = Set([0]), queue = [0], head = 0, peak = 1 + while head < queue.count { + let node = queue[head]; head += 1 + for pair in try #require(entry.expected.edge_pairs) where pair[0] == node { + if seen.insert(pair[1]).inserted { queue.append(pair[1]); peak = max(peak, queue.count - head) } + } + } + #expect(queue == reached) + #expect(try graph.breadthFirst(fromNode: 0, maximumFrontier: peak, maximumVisited: reached.count, maximumResults: reached.count).result == reached) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumFrontier: peak - 1) } == .frontierLimitExceeded(limit: peak - 1)) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumVisited: reached.count - 1) } == .visitedLimitExceeded(limit: reached.count - 1)) + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumResults: reached.count - 1) } == .resultLimitExceeded(limit: reached.count - 1)) + } + if graph.edgeCount > 0 { + #expect(graphFailure { _ = try JSONGraphProjection.build(document: document, profile: profile, maximumEdges: graph.edgeCount - 1) } == .edgeLimitExceeded(limit: graph.edgeCount - 1)) + } + evaluated += 1 + } + #expect(evaluated == 12) + var refused = 0 + for entry in oracle.cases where entry.expected.kind != "valid" { + if entry.expected.kind == "overlap" { + let profile = JSONGraphProfile(nodesAt: [.key("data")], edgesAt: [.key("data")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + #expect(throws: JSONTraversalFailure.ambiguousRoots(first: 0, second: 1)) { _ = try graphBuild(entry.name, profile: profile) } + } else if entry.expected.kind.hasSuffix("not_array") { + #expect(graphCore { _ = try graphBuild(entry.name) }?.code == .wrongKind) + } else { + let failure = graphFailure { _ = try graphBuild(entry.name) } + switch (entry.expected.kind, failure) { + case ("duplicate_node_id", .duplicateNodeID), ("node_id_missing", .idMissing), + ("edge_source_missing", .idMissing), ("node_id_null", .idNull), + ("edge_target_null", .idNull), ("node_id_wrong_kind", .idWrongKind), + ("edge_target_wrong_kind", .idWrongKind), ("node_row_wrong_kind", .rowNotObject), + ("edge_row_wrong_kind", .rowNotObject), ("dangling_endpoint", .danglingEndpoint), + ("duplicate_directed_edge", .duplicateDirectedEdge): break + default: Issue.record("Independent refusal mismatch: \(entry.name) \(String(describing: failure))") + } + } + refused += 1 + } + #expect(refused == 15) + print("GRAPH_ORACLE passed valid=12 refused=15") + +} +@Test func graphG03ScalarDistinctIDs() throws { + let graph = try graphBuild("g03-unicode-scalars").result + #expect(graph.nodeCount == 2 && graph.edges[0].target == 1) + #expect(graph.nodeIDs[0] == graph.nodeIDs[1]) // Swift canonical equivalence is intentionally different. + #expect(Array(graph.nodeIDs[0].utf8) != Array(graph.nodeIDs[1].utf8)) +} +@Test func graphG04SpecialIDsAreNotPaths() throws { + let graph = try graphBuild("g04-empty-and-special-ids").result + #expect(graph.nodeIDs.contains("")); #expect(graph.nodeCount == 3) + for node in 0..= 6 { fired.withLock { $0 = true }; withUnsafeCurrentTask { $0?.cancel() } } + } + } + let observer: @Sendable (GraphWorkStage) -> Void = { current in + if current == phase { armed.withLock { $0 = true } } + } + if phase == .breadthFirst { + let captured = try JSONGraphProjection.build(document: large, profile: graphProfile).result + var query = GraphQuery(workObserver: observer, graph: captured, query: .breadthFirst(0), limits: limits, + maximumFrontier: 12, maximumVisited: 12, maximumResults: 12) + #expect(throws: CancellationError.self) { _ = try walk.inspectOwned(options: .init(depthLimit: 0), reducer: &query) } + } else { + var reducer = GraphBuilder(workObserver: observer, profile: graphProfile, limits: limits, maximumNodes: 12, maximumEdges: 11) + #expect(throws: CancellationError.self) { + _ = try walk.inspectOwned(scope: .virtual(roots: [graphProfile.nodesAt, graphProfile.edgesAt], overlap: .rejectAmbiguous), options: .init(depthLimit: 1), reducer: &reducer) + } + } + } + try await job.value + #expect(fired.withLock { $0 } && cleared.withLock { $0 }) + print("GRAPH_CANCELLATION phase=\(phase) checkpoints=\(ticks.withLock { $0 }) root-and-state-released=true") + } + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + #expect(throws: CancellationError.self) { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile) } + #expect(throws: CancellationError.self) { _ = try graph.breadthFirst(fromNode: 0) } + } + await task.value + #expect(try graph.breadthFirst(fromNode: 0).result == [0, 1, 2, 3]) +} +@Test func graphG29FailurePrecedenceAndTerminalLatch() throws { + let doc = JSONDocument(root: try JSON.parse(#"{"nodes":[false,{"id":"A"},{"id":"A"}],"edges":[{"from":"X","to":"Y"}]}"#)) + #expect(graphCore { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile, maximumNodes: -1) }?.code == .invalidLimits) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile) } == .rowNotObject(at: [.key("nodes"), .index(0)], kind: .boolean)) + var limits = JSONLimits.balanced; limits[.workUnits] = 0 + #expect(graphCore { _ = try JSONGraphProjection.build(document: doc, profile: graphProfile, limits: limits) }?.category == .resource) + let prefix = (0..<32).map { "{\"id\":\"n\($0)\"}" }.joined(separator: ",") + let late = JSONDocument(root: try JSON.parse("{\"nodes\":[" + prefix + ",{\"id\":\"n0\"}],\"edges\":[{\"from\":\"X\",\"to\":\"Y\"}]}")) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: late, profile: graphProfile) } == .duplicateNodeID(first: [.key("nodes"), .index(0)], second: [.key("nodes"), .index(32)])) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: late, profile: graphProfile, maximumNodes: 32) } == .nodeLimitExceeded(limit: 32)) + let admitted = try JSONTraversal(document: late).visit(options: .init(depthLimit: 0)) { _,_ in .continue } + limits = .balanced; limits[.workUnits] = admitted.statistics[.workUnits] + 100 + #expect(graphCore { _ = try JSONGraphProjection.build(document: late, profile: graphProfile, limits: limits) }?.category == .resource) + let fields = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"A"}],"edges":[{"from":"MISSING","to":null}]}"#)) + #expect(graphFailure { _ = try JSONGraphProjection.build(document: fields, profile: graphProfile) } == .idNull(at: [.key("edges"), .index(0), .key("to")])) + struct Catcher: JSONTraversalOwnedReducer { + enum Replacement: Error { case caller } + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { + do { try context.work(UInt64.max) } catch {} + throw Replacement.caller + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> Bool { true } + } + var reducer = Catcher() + #expect(graphCore { _ = try JSONTraversal(document: doc).inspectOwned(reducer: &reducer) }?.category == .resource) +} +@Test func graphG30ConcurrentViewsHaveIndependentHealth() async throws { + let doc = try graphDocument("g22-diamond") + let graph = try JSONGraphProjection.build(document: doc, profile: graphProfile).result + try await withThrowingTaskGroup(of: [Int].self) { group in + for _ in 0..<12 { + group.addTask { + #expect(graphFailure { _ = try graph.breadthFirst(fromNode: 0, maximumResults: 1) } == .resultLimitExceeded(limit: 1)) + return try JSONGraphProjection.build(document: doc, profile: graphProfile).result.breadthFirst(fromNode: 0).result + } + } + for try await result in group { #expect(result == [0, 1, 2, 3]) } + } +} +@Test func graphG31CapturedRootAndSourceMapCoherence() throws { + let original = try graphDocument("g02-simple"), graph = try JSONGraphProjection.build(document: original, profile: graphProfile).result + let replaced = original.replacingRoot(try JSON.parse(#"{"nodes":[{"id":"NEW"}],"edges":[]}"#)) + let new = try JSONGraphProjection.build(document: replaced, profile: graphProfile).result + #expect(try graph.nodeSpan(at: 0).result != nil && new.nodeSpan(at: 0).result == nil) + #expect(graph.nodeIDs == ["A", "B", "C"] && new.nodeIDs == ["NEW"]) + #expect(graph.document.sourceBytes == original.sourceBytes) +} +@Test func graphG32CheckedMetadataAndExistingPublicSemantics() throws { + let graph = try graphBuild("g02-simple").result + #expect(graphFailure { _ = try graph.nodeValue(at: -1) } == .nodeOutOfBounds(-1)) + #expect(graphFailure { _ = try graph.edgeValue(at: graph.edgeCount) } == .edgeOutOfBounds(graph.edgeCount)) + #expect(try graph.edgeValue(at: 0).result == JSONPointer(parsing: "/edges/0").resolve(in: graph.document.root)) + #expect(try JSONTraversal(document: graph.document).visit { _, _ in .continue }.statistics[.retainedBytes] == 0) + #expect(try JSONShapeInspector(document: graph.document).inspectArray(at: [.key("nodes")]).result.totalRows == 3) +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json b/Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json new file mode 100644 index 0000000..b613cb8 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/GRAPH-FIXTURE-ORACLE.json @@ -0,0 +1,653 @@ +{ + "disclaimer": "Independent test-data oracle, NOT Swift-JSON validation", + "cases": [ + { + "name": "g01-empty", + "file": "g01-empty.json", + "sha256": "1ddf1d31190da6e68449f240aefa5e9b87896f675196008d26c8ea7431ec8b4c", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [], + "edge_pairs": [], + "outgoing_edge_ordinals": [], + "bfs_from_node0": [] + } + }, + { + "name": "g02-simple", + "file": "g02-simple.json", + "sha256": "97ce5fa7ec116a89a48606897295bbc2934fdc927b429a213d13e3474694f100", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 0, + 2 + ], + [ + 1, + 2 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0, + 1 + ], + [ + 2 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1, + 2 + ] + } + }, + { + "name": "g03-unicode-scalars", + "file": "g03-unicode-scalars.json", + "sha256": "47cf0ab06c0b72d663d8a7d807f46752414e34cb422ef526a4e155a60e194182", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 233 + ], + [ + 101, + 769 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g04-empty-and-special-ids", + "file": "g04-empty-and-special-ids.json", + "sha256": "45a06cd463be2eee805f145d4a386ab8f76d64bf53503aa5086df16724c02791", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [], + [ + 97, + 47, + 98, + 126, + 99 + ], + [ + 48 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 2, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [], + [ + 1 + ] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g05-duplicate-id", + "file": "g05-duplicate-id.json", + "sha256": "ebb7b9720976636fc4521f6b66c1e92544bae4ab1dfb244c4988af42cc56eb24", + "profile": {}, + "expected": { + "kind": "duplicate_node_id", + "node_index": 1, + "first_index": 0 + } + }, + { + "name": "g06-equal-payload-distinct-id", + "file": "g06-equal-payload-distinct-id.json", + "sha256": "f740b12b7a01d74c2b86b729e56c82abc5b7ef7be2e8672313286bf18e295ddf", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ] + ], + "edge_pairs": [], + "outgoing_edge_ordinals": [ + [], + [] + ], + "bfs_from_node0": [ + 0 + ] + } + }, + { + "name": "g07-node-id-missing", + "file": "g07-node-id-missing.json", + "sha256": "3473660136a3b01aa468cbc5a1c1f1f093ae0d9ac476292a9c44f79e296376f1", + "profile": {}, + "expected": { + "kind": "node_id_missing", + "node_index": 0 + } + }, + { + "name": "g08-node-id-null", + "file": "g08-node-id-null.json", + "sha256": "ad53d92263bb921749296c96f50470495a3758646e3777a4039c15a2dd4f548c", + "profile": {}, + "expected": { + "kind": "node_id_null", + "node_index": 0 + } + }, + { + "name": "g09-node-id-number", + "file": "g09-node-id-number.json", + "sha256": "311961bc71d065686c2be6ddc4b3bae46c1ffab1c5906fc4c0b8c22af13e3d37", + "profile": {}, + "expected": { + "kind": "node_id_wrong_kind", + "node_index": 0 + } + }, + { + "name": "g10-node-row-not-object", + "file": "g10-node-row-not-object.json", + "sha256": "d76618e34d1bc43d9de376b72d2a2f8cc4ed2d449b6fdc8537c901147fba4212", + "profile": {}, + "expected": { + "kind": "node_row_wrong_kind", + "node_index": 0 + } + }, + { + "name": "g11-edge-source-missing", + "file": "g11-edge-source-missing.json", + "sha256": "77e0e2a123ee05f3d842b00181e2463042ac9580169c6571d79146b1aebf988c", + "profile": {}, + "expected": { + "kind": "edge_source_missing", + "edge_index": 0 + } + }, + { + "name": "g12-edge-target-null", + "file": "g12-edge-target-null.json", + "sha256": "b873ad0efe1efe13215a283ec202c55407aeb1d754e8aba79dd38f72c3529244", + "profile": {}, + "expected": { + "kind": "edge_target_null", + "edge_index": 0 + } + }, + { + "name": "g13-edge-target-number", + "file": "g13-edge-target-number.json", + "sha256": "2bbbedb9d505d32ca01228c5b63ba71ee8510299371757ad741cc17c09a5e49a", + "profile": {}, + "expected": { + "kind": "edge_target_wrong_kind", + "edge_index": 0 + } + }, + { + "name": "g14-edge-row-not-object", + "file": "g14-edge-row-not-object.json", + "sha256": "21de4ba95ef61f4856d0eee1a02d0c17694b50cc291378fdcb05efbf6e721e01", + "profile": {}, + "expected": { + "kind": "edge_row_wrong_kind", + "edge_index": 0 + } + }, + { + "name": "g15-dangling", + "file": "g15-dangling.json", + "sha256": "c3a9c699115be44b60da5d165544ea9a4f9d443685422f5ab99fdee59a2d8c54", + "profile": {}, + "expected": { + "kind": "dangling_endpoint", + "edge_index": 0 + } + }, + { + "name": "g15b-dangling-empty", + "file": "g15b-dangling-empty.json", + "sha256": "074f22438edb27a566aba8491502909f894e0aefdf5dac04dd589758b0844d20", + "profile": {}, + "expected": { + "kind": "dangling_endpoint", + "edge_index": 0 + } + }, + { + "name": "g16-duplicate-edge", + "file": "g16-duplicate-edge.json", + "sha256": "76bffe5363e00f24cb4e6951eff04214d11170682e80e8e53cbd7ab1c321419e", + "profile": {}, + "expected": { + "kind": "duplicate_directed_edge", + "edge_index": 1, + "first_index": 0 + } + }, + { + "name": "g17-reverse-edges", + "file": "g17-reverse-edges.json", + "sha256": "62bc74cf4934b0c2f82d322b8f28c563dd70c1745df6661aaa1389caef55c780", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 1, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [ + 1 + ] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g18-self-edge", + "file": "g18-self-edge.json", + "sha256": "2bd68dc1cd578b9ac7b922c4fb2f13d2f50bfa6c4068af18966deb59e0205e8d", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ] + ], + "edge_pairs": [ + [ + 0, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ] + ], + "bfs_from_node0": [ + 0 + ] + } + }, + { + "name": "g19-cycle", + "file": "g19-cycle.json", + "sha256": "d0c0a81d2a87dfe714b963957f6b7305cdf2942df58ea2a2548aa450d1e0d349", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 1, + 2 + ], + [ + 2, + 0 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [ + 1 + ], + [ + 2 + ] + ], + "bfs_from_node0": [ + 0, + 1, + 2 + ] + } + }, + { + "name": "g20-isolate", + "file": "g20-isolate.json", + "sha256": "5137753f500607e15e29d3fc5510b62db8e23c10b647a776419972db3768f098", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 73, + 83, + 79 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [], + [] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g21-bfs-source-edge-order", + "file": "g21-bfs-source-edge-order.json", + "sha256": "73166cac74801e95d7954bcd7b39b1be9e990bd7bb5d730800a1d8c7d53b77ed", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ], + [ + 68 + ] + ], + "edge_pairs": [ + [ + 0, + 3 + ], + [ + 0, + 1 + ], + [ + 0, + 2 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0, + 1, + 2 + ], + [], + [], + [] + ], + "bfs_from_node0": [ + 0, + 3, + 1, + 2 + ] + } + }, + { + "name": "g22-diamond", + "file": "g22-diamond.json", + "sha256": "8b8b2934a1bdd720621c8045c54dde4e6eee9a97ce4b90020b0404d4ff26cedf", + "profile": {}, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 65 + ], + [ + 66 + ], + [ + 67 + ], + [ + 68 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ], + [ + 0, + 2 + ], + [ + 1, + 3 + ], + [ + 2, + 3 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0, + 1 + ], + [ + 2 + ], + [ + 3 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1, + 2, + 3 + ] + } + }, + { + "name": "g23-numeric-object-key", + "file": "g23-numeric-object-key.json", + "sha256": "ae3d0720da528a24240b629891b1d2d47928efa901fbb3d7c29417affb942385", + "profile": { + "nodes_at": [ + { + "key": "0" + }, + { + "key": "nodes" + } + ], + "edges_at": [ + { + "key": "0" + }, + { + "key": "edges" + } + ], + "node_id_key": "key/id", + "edge_source_key": "src~", + "edge_target_key": "dst" + }, + "expected": { + "kind": "valid", + "ids_codepoints": [ + [ + 48 + ], + [ + 65 + ] + ], + "edge_pairs": [ + [ + 0, + 1 + ] + ], + "outgoing_edge_ordinals": [ + [ + 0 + ], + [] + ], + "bfs_from_node0": [ + 0, + 1 + ] + } + }, + { + "name": "g24-overlap", + "file": "g24-overlap.json", + "sha256": "74011485b9b4c4cf9fe70d264ffe7db4ba3796034968c0773b3213f395475e93", + "profile": { + "nodes_at": [ + { + "key": "data" + } + ], + "edges_at": [ + { + "key": "data" + } + ] + }, + "expected": { + "kind": "overlap" + } + }, + { + "name": "g25-wrong-nodes-array", + "file": "g25-wrong-nodes-array.json", + "sha256": "e91a8449e7af8b0555eaeb0b90087a580c72ae239dfb875c1c138dd128e9a8b1", + "profile": {}, + "expected": { + "kind": "nodes_not_array" + } + }, + { + "name": "g26-wrong-edges-array", + "file": "g26-wrong-edges-array.json", + "sha256": "96f22c1ec9ff60642dc39f4188c0955ceeb01da3a7bf9570cbe885ca59f012c1", + "profile": {}, + "expected": { + "kind": "edges_not_array" + } + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g01-empty.json b/Tests/JSONInspectionTests/GraphFixtures/g01-empty.json new file mode 100644 index 0000000..ecb3718 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g01-empty.json @@ -0,0 +1,4 @@ +{ + "nodes": [], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g02-simple.json b/Tests/JSONInspectionTests/GraphFixtures/g02-simple.json new file mode 100644 index 0000000..72cefa8 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g02-simple.json @@ -0,0 +1,27 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "C" + }, + { + "from": "B", + "to": "C" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json b/Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json new file mode 100644 index 0000000..1d0f4ab --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g03-unicode-scalars.json @@ -0,0 +1,16 @@ +{ + "nodes": [ + { + "id": "é" + }, + { + "id": "é" + } + ], + "edges": [ + { + "from": "é", + "to": "é" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json b/Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json new file mode 100644 index 0000000..230b184 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g04-empty-and-special-ids.json @@ -0,0 +1,23 @@ +{ + "nodes": [ + { + "id": "" + }, + { + "id": "a/b~c" + }, + { + "id": "0" + } + ], + "edges": [ + { + "from": "", + "to": "a/b~c" + }, + { + "from": "0", + "to": "" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json b/Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json new file mode 100644 index 0000000..f74f5cb --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g05-duplicate-id.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A", + "name": "x" + }, + { + "id": "A", + "name": "y" + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json b/Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json new file mode 100644 index 0000000..709b3b0 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g06-equal-payload-distinct-id.json @@ -0,0 +1,17 @@ +{ + "nodes": [ + { + "id": "A", + "same": { + "x": 1 + } + }, + { + "id": "B", + "same": { + "x": 1 + } + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json b/Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json new file mode 100644 index 0000000..6892396 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g07-node-id-missing.json @@ -0,0 +1,8 @@ +{ + "nodes": [ + { + "name": "A" + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json b/Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json new file mode 100644 index 0000000..4cdcf9a --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g08-node-id-null.json @@ -0,0 +1,8 @@ +{ + "nodes": [ + { + "id": null + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json b/Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json new file mode 100644 index 0000000..d15d147 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g09-node-id-number.json @@ -0,0 +1,8 @@ +{ + "nodes": [ + { + "id": 22 + } + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json b/Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json new file mode 100644 index 0000000..4a98b06 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g10-node-row-not-object.json @@ -0,0 +1,6 @@ +{ + "nodes": [ + "A" + ], + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json b/Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json new file mode 100644 index 0000000..d3fa950 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g11-edge-source-missing.json @@ -0,0 +1,12 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json b/Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json new file mode 100644 index 0000000..95b6210 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g12-edge-target-null.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": null + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json b/Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json new file mode 100644 index 0000000..b6e64fb --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g13-edge-target-number.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": 3 + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json b/Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json new file mode 100644 index 0000000..1872870 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g14-edge-row-not-object.json @@ -0,0 +1,10 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + 3 + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json b/Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json new file mode 100644 index 0000000..f70a7b2 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g15-dangling.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json b/Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json new file mode 100644 index 0000000..d7ec452 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g15b-dangling-empty.json @@ -0,0 +1,9 @@ +{ + "nodes": [], + "edges": [ + { + "from": "A", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json b/Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json new file mode 100644 index 0000000..2aa7a8d --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g16-duplicate-edge.json @@ -0,0 +1,20 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "B" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json b/Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json new file mode 100644 index 0000000..8fb4dfc --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g17-reverse-edges.json @@ -0,0 +1,20 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "B", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json b/Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json new file mode 100644 index 0000000..140674c --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g18-self-edge.json @@ -0,0 +1,13 @@ +{ + "nodes": [ + { + "id": "A" + } + ], + "edges": [ + { + "from": "A", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json b/Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json new file mode 100644 index 0000000..44c5e1c --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g19-cycle.json @@ -0,0 +1,27 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "B", + "to": "C" + }, + { + "from": "C", + "to": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json b/Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json new file mode 100644 index 0000000..c3c485c --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g20-isolate.json @@ -0,0 +1,19 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "ISO" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json b/Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json new file mode 100644 index 0000000..01322f0 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g21-bfs-source-edge-order.json @@ -0,0 +1,30 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + }, + { + "id": "D" + } + ], + "edges": [ + { + "from": "A", + "to": "D" + }, + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "C" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json b/Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json new file mode 100644 index 0000000..88d162e --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g22-diamond.json @@ -0,0 +1,34 @@ +{ + "nodes": [ + { + "id": "A" + }, + { + "id": "B" + }, + { + "id": "C" + }, + { + "id": "D" + } + ], + "edges": [ + { + "from": "A", + "to": "B" + }, + { + "from": "A", + "to": "C" + }, + { + "from": "B", + "to": "D" + }, + { + "from": "C", + "to": "D" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json b/Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json new file mode 100644 index 0000000..b985501 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g23-numeric-object-key.json @@ -0,0 +1,18 @@ +{ + "0": { + "nodes": [ + { + "key/id": "0" + }, + { + "key/id": "A" + } + ], + "edges": [ + { + "src~": "0", + "dst": "A" + } + ] + } +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json b/Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json new file mode 100644 index 0000000..7e521c7 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g24-overlap.json @@ -0,0 +1,7 @@ +{ + "data": [ + { + "id": "A" + } + ] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json b/Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json new file mode 100644 index 0000000..a649225 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g25-wrong-nodes-array.json @@ -0,0 +1,4 @@ +{ + "nodes": {}, + "edges": [] +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json b/Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json new file mode 100644 index 0000000..69e5bff --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/g26-wrong-edges-array.json @@ -0,0 +1,4 @@ +{ + "nodes": [], + "edges": {} +} diff --git a/Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py b/Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py new file mode 100755 index 0000000..e95e9d2 --- /dev/null +++ b/Tests/JSONInspectionTests/GraphFixtures/generate_graph_fixtures.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Independent design-fixture generator. NOT a Swift-JSON semantic or runtime qualification.""" +from pathlib import Path +import json, hashlib +base=Path(__file__).resolve().parent +cases={ + 'g01-empty':({'nodes':[],'edges':[]},{},'valid'), + 'g02-simple':({'nodes':[{'id':'A'},{'id':'B'},{'id':'C'}], 'edges':[{'from':'A','to':'B'},{'from':'A','to':'C'},{'from':'B','to':'C'}]}, {},'valid'), + 'g03-unicode-scalars':({'nodes':[{'id':'é'},{'id':'e\u0301'}],'edges':[{'from':'é','to':'e\u0301'}]}, {},'valid'), + 'g04-empty-and-special-ids':({'nodes':[{'id':''},{'id':'a/b~c'},{'id':'0'}], 'edges':[{'from':'','to':'a/b~c'},{'from':'0','to':''}]},{},'valid'), + 'g05-duplicate-id':({'nodes':[{'id':'A','name':'x'},{'id':'A','name':'y'}],'edges':[]},{},'duplicate_node_id'), + 'g06-equal-payload-distinct-id':({'nodes':[{'id':'A','same':{'x':1}},{'id':'B','same':{'x':1}}],'edges':[]},{},'valid'), + 'g07-node-id-missing':({'nodes':[{'name':'A'}],'edges':[]},{},'node_id_missing'), + 'g08-node-id-null':({'nodes':[{'id':None}],'edges':[]},{},'node_id_null'), + 'g09-node-id-number':({'nodes':[{'id':22}],'edges':[]},{},'node_id_wrong_kind'), + 'g10-node-row-not-object':({'nodes':['A'],'edges':[]},{},'node_row_wrong_kind'), + 'g11-edge-source-missing':({'nodes':[{'id':'A'}],'edges':[{'to':'A'}]},{},'edge_source_missing'), + 'g12-edge-target-null':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':None}]},{},'edge_target_null'), + 'g13-edge-target-number':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':3}]},{},'edge_target_wrong_kind'), + 'g14-edge-row-not-object':({'nodes':[{'id':'A'}],'edges':[3]},{},'edge_row_wrong_kind'), + 'g15-dangling':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':'B'}]},{},'dangling_endpoint'), + 'g15b-dangling-empty':({'nodes':[],'edges':[{'from':'A','to':'A'}]},{},'dangling_endpoint'), + 'g16-duplicate-edge':({'nodes':[{'id':'A'},{'id':'B'}],'edges':[{'from':'A','to':'B'},{'from':'A','to':'B'}]},{},'duplicate_directed_edge'), + 'g17-reverse-edges':({'nodes':[{'id':'A'},{'id':'B'}],'edges':[{'from':'A','to':'B'},{'from':'B','to':'A'}]},{},'valid'), + 'g18-self-edge':({'nodes':[{'id':'A'}],'edges':[{'from':'A','to':'A'}]},{},'valid'), + 'g19-cycle':({'nodes':[{'id':'A'},{'id':'B'},{'id':'C'}],'edges':[{'from':'A','to':'B'},{'from':'B','to':'C'},{'from':'C','to':'A'}]},{},'valid'), + 'g20-isolate':({'nodes':[{'id':'A'},{'id':'B'},{'id':'ISO'}],'edges':[{'from':'A','to':'B'}]},{},'valid'), + 'g21-bfs-source-edge-order':({'nodes':[{'id':x} for x in ['A','B','C','D']],'edges':[{'from':'A','to':'D'},{'from':'A','to':'B'},{'from':'A','to':'C'}]},{},'valid'), + 'g22-diamond':({'nodes':[{'id':x} for x in ['A','B','C','D']],'edges':[{'from':'A','to':'B'},{'from':'A','to':'C'},{'from':'B','to':'D'},{'from':'C','to':'D'}]},{},'valid'), + 'g23-numeric-object-key':({'0':{'nodes':[{'key/id':'0'},{'key/id':'A'}],'edges':[{'src~':'0','dst':'A'}]}},{'nodes_at':[{'key':'0'},{'key':'nodes'}],'edges_at':[{'key':'0'},{'key':'edges'}],'node_id_key':'key/id','edge_source_key':'src~','edge_target_key':'dst'},'valid'), + 'g24-overlap':({'data':[{'id':'A'}]},{'nodes_at':[{'key':'data'}],'edges_at':[{'key':'data'}]},'overlap'), + 'g25-wrong-nodes-array':({'nodes':{},'edges':[]},{},'nodes_not_array'), + 'g26-wrong-edges-array':({'nodes':[],'edges':{}},{},'edges_not_array'), +} +def tok_path(root,segs): + val=root + for seg in segs: + if 'key' in seg: + if not isinstance(val,dict):raise TypeError('path_expected_object') + val=val[seg['key']] + else: + if not isinstance(val,list):raise TypeError('path_expected_array') + val=val[seg['index']] + return val +def reference(obj, prof): + np=prof.get('nodes_at',[{'key':'nodes'}]);ep=prof.get('edges_at',[{'key':'edges'}]);node=prof.get('node_id_key','id'); fr=prof.get('edge_source_key','from');to=prof.get('edge_target_key','to') + if np==ep or (len(np)<=len(ep) and np==ep[:len(np)]) or (len(ep)<=len(np) and ep==np[:len(ep)]):return {'kind':'overlap'} + try:nodes=tok_path(obj,np);edges=tok_path(obj,ep) + except Exception:return {'kind':'missing_or_invalid_selection'} + if not isinstance(nodes,list):return {'kind':'nodes_not_array'} + if not isinstance(edges,list):return {'kind':'edges_not_array'} + ids=[] + for i,n in enumerate(nodes): + if not isinstance(n,dict):return {'kind':'node_row_wrong_kind','node_index':i} + if node not in n:return {'kind':'node_id_missing','node_index':i} + if n[node] is None:return {'kind':'node_id_null','node_index':i} + if not isinstance(n[node],str):return {'kind':'node_id_wrong_kind','node_index':i} + if n[node] in ids:return {'kind':'duplicate_node_id','node_index':i,'first_index':ids.index(n[node])} + ids.append(n[node]) + pairs=[];outgoing=[[] for _ in nodes] + for eindex,e in enumerate(edges): + if not isinstance(e,dict):return {'kind':'edge_row_wrong_kind','edge_index':eindex} + for field,field_name in ((fr,'edge_source'),(to,'edge_target')): + if field not in e:return {'kind':field_name+'_missing','edge_index':eindex} + if e[field] is None:return {'kind':field_name+'_null','edge_index':eindex} + if not isinstance(e[field],str):return {'kind':field_name+'_wrong_kind','edge_index':eindex} + if e[fr] not in ids or e[to] not in ids:return {'kind':'dangling_endpoint','edge_index':eindex} + pair=(ids.index(e[fr]),ids.index(e[to])) + if pair in pairs:return {'kind':'duplicate_directed_edge','edge_index':eindex,'first_index':pairs.index(pair)} + pairs.append(pair);outgoing[pair[0]].append(eindex) + visited=[False]*len(ids);order=[];queue=[] + if nodes: + queue=[0];visited[0]=True;head=0 + while head Date: Mon, 19 Oct 2026 20:45:08 +0200 Subject: [PATCH 2/3] Expose the reviewed source-bound graph API with public acceptance proofs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote exactly the owner-approved 34 declarations inside JSONInspection; retain private CSR/reducers and original single-meter semantics. Add sixteen public cases, separate-package consumer, three executable DocC examples and strict 26-negative compiler receipts. Preserve G01–G32, pinned fixtures, original ownership matrices, 49 core caps and 126 maintenance identities. No new product, merge, release or performance claim. --- AGENTS.md | 4 +- .../Architecture/ExtractionManifest.json | 17 +- .../NAV11/APICandidate.md | 17 ++ Documentation/CI/README.md | 14 ++ Documentation/Qualification/GraphCases.json | 41 ++++- .../Qualification/TestDiscovery.json | 10 +- README.md | 19 +++ Scripts/ci.py | 11 +- Scripts/graph_acceptance.py | 21 ++- Scripts/qualify_graph.py | 10 +- Scripts/qualify_public_graph.py | 111 ++++++++++++ Scripts/test_qualification_harness.py | 31 +++- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- .../Documentation.docc/GraphExperiment.md | 2 + .../Documentation.docc/JSONInspection.md | 7 + .../Documentation.docc/SourceBoundGraphs.md | 62 +++++++ .../Graph/JSONGraphProfile.swift | 52 ++++-- .../Graph/JSONGraphProjection.swift | 70 ++++++-- .../GraphPublicAcceptanceTests.swift | 158 ++++++++++++++++++ Tools/GraphPublicQualification/Package.swift | 4 + .../Sources/GraphPublicConsumer/Main.swift | 137 +++++++++++++++ 21 files changed, 748 insertions(+), 52 deletions(-) create mode 100644 Scripts/qualify_public_graph.py create mode 100644 Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md create mode 100644 Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift create mode 100644 Tools/GraphPublicQualification/Package.swift create mode 100644 Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift diff --git a/AGENTS.md b/AGENTS.md index 4ad90fd..4bfd841 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,5 +43,7 @@ Documentation/Architecture before changing behavior. Scalar scanning and immutable plan-local regex sharing are selected private optimizations. Source-attached N3 snapshots and P1 tree/sparse-table projections live in JSONInspection. -NAV11 directed simple graph is a package-only JSONInspection experiment; no public graph API/product or automatic merge. +NAV11 directed simple graph has an owner-reviewed public candidate API inside JSONInspection; no new product or automatic merge. Store/set/matrix/retrieval/persistence and JSON text-sequence framing remain separate deferred trains. + +NAV11 public graph candidate adds only the approved immutable profile, checked source-bound graph and typed failures inside JSONInspection. CSR arrays, reducers and meters remain non-public; no new product or graph serialization/mutation. Future merge/release remain separately human-gated. diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index eec79bd..2336449 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -324,5 +324,20 @@ "framework_boundary": "Conditional CoreTransferable/UniformTypeIdentifiers plus a narrow explicit main-actor Apple PDF boundary using WebKit, CoreText, CoreGraphics and AppKit/UIKit paragraph styles. Core JSON imports none of these; no new root dependency.", "rtf": "Original bounded strict-token emitter; AppKit/UIKit native decoding only in tests and qualification consumers, not generation.", "pdf": "Original invocation-owned lifecycle, finite adapter accounting and visible-range pagination using Apple SDK APIs. PDFKit is independent qualification only; no third-party renderer adapted." - } + }, + "original_components": [ + { + "component": "NAV11 owner-reviewed public graph candidate", + "license": "Apache-2.0", + "source": "Original Swift-JSON public access/documentation adapters over qualified NAV11 graph donor PR #67 / 81485930f859131d0802691ca6370f482ba62911", + "paths": [ + "Sources/JSONInspection/Graph", + "Tools/GraphPublicQualification", + "Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift", + "Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md" + ], + "validation": "G01–G32 retained; GP01–GP16 plus Debug/Release/ASan public consumer, 26 codegen negatives and three DocC examples; execution receipts required", + "dependencies": "No external runtime dependencies; existing JSON and JSONNavigation authorities only" + } + ] } diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md index e4c881c..8055869 100644 --- a/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md @@ -29,3 +29,20 @@ Actual compiled spelling is in Sources/JSONInspection/Graph. Constructors remain fileprivate. Compiler probes reject external type access, private initializers, mutation, internal reducers, and context escape. No public DocC examples imply these private declarations are available. No merge or promotion is authorized. + +## Public successor review + +The owner approved the compiler-derived 34-declaration public subset for the draft +candidate, subject to qualification. The exact source spelling is authoritative: +public JSONGraphProfile (immutable explicit paths/keys and initializer), +JSONGraphProjection (checked factory, immutable document/profile/traversal/counts +and five independent query operations), and JSONGraphFailure with fourteen typed cases. +No existing public declaration changes. CSR buffers, ID staging, edge records, +reducers and the noncopyable context remain inaccessible externally. The old +public-type-invisibility negatives are replaced by internal builder/query/stage +negatives; initializer, storage, mutation and ownership rejections remain. + +Sixteen public-only scenarios and a distinct-package consumer supplement all +G01–G32 and 27 independently pinned fixtures. Three executable DocC examples and +26 external full-codegen rejections are mandatory. This is signature approval +for a draft, not merge/release approval or a claim of unexecuted platform passes. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 2b8dc77..66f544e 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -308,3 +308,17 @@ The focused composition lane also runs on the Linux architecture jobs and Native Its separate-package discovery is additive; all root and 49-cap expectations remain intact. The qualification harness now has 15 regression functions, retaining its original 14. See [composition qualification](../Architecture/CompositionQualification.md) for finite capacity outcomes. + +## NAV11 public graph candidate + +The owner-reviewed successor keeps all 32 original graph identities and adds 16 +public scenarios (124 Inspection functions). Required roots become 827 native, +811 Linux and 723 WASI, with unchanged 325/288 parameter invocations. Shared-state +expectations become 53 native/52 Linux. The public graph qualifier executes 16 +consumer cases in Debug/Release/ASan, three guide blocks/configuration and 26 +ordinary external full-codegen rejections. The original graph boundary matrix +retains 18 negatives, replacing only obsolete invisible-public-type cases with +internal builder/query/stage access rejections. Reducer 2+/20-, helper 2+/8-, all +49 core caps and 126 maintenance identities remain. Harness cardinality is 19. +These are requirements; actual source-bound receipts establish results. No merge +or release follows automatically. Controlled G4 remains HOLD without speed claims. diff --git a/Documentation/Qualification/GraphCases.json b/Documentation/Qualification/GraphCases.json index 4f9852a..2caae38 100644 --- a/Documentation/Qualification/GraphCases.json +++ b/Documentation/Qualification/GraphCases.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "status": "experimental package-only", + "status": "public API candidate; owner review and full current-source qualification required", "functions": [ "JSONInspectionTests.graphG01EmptySourceAndNoValidStart()", "JSONInspectionTests.graphG02IndependentOracleAndSourceOrder()", @@ -39,5 +39,42 @@ "context_positive_codegen": 2, "context_negative_codegen": 8, "graph_positive_codegen": 2, - "graph_negative_codegen": 18 + "graph_negative_codegen": 18, + "public_functions": [ + "JSONInspectionTests.graphGP01OptInSurface()", + "JSONInspectionTests.graphGP02TypedRootAndEmptyKeys()", + "JSONInspectionTests.graphGP03SourceOrderBFS()", + "JSONInspectionTests.graphGP04SelfLoopAndCycle()", + "JSONInspectionTests.graphGP05DuplicateIDPaths()", + "JSONInspectionTests.graphGP06ScalarDistinctIDs()", + "JSONInspectionTests.graphGP07OrderedPairs()", + "JSONInspectionTests.graphGP08DanglingEndpoint()", + "JSONInspectionTests.graphGP09MissingNullWrongKind()", + "JSONInspectionTests.graphGP10BoundsAndZeroCaps()", + "JSONInspectionTests.graphGP11IndependentWholeSourceBudget()", + "JSONInspectionTests.graphGP12OwnedValueAndSourceHandoff()", + "JSONInspectionTests.graphGP13CancellationAndHealth()", + "JSONInspectionTests.graphGP14ConcurrentQueries()", + "JSONInspectionTests.graphGP15SourceMapCoherence()", + "JSONInspectionTests.graphGP16UnselectedAdmission()" + ], + "public_consumer_checks": [ + "GP01", + "GP02", + "GP03", + "GP04", + "GP05", + "GP06", + "GP07", + "GP08", + "GP09", + "GP10", + "GP11", + "GP12", + "GP13", + "GP14", + "GP15", + "GP16" + ], + "public_negative_codegen": 26 } diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 444f23b..1575ed4 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 108 + "JSONInspectionTests": 124 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -35,7 +35,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 5 + "JSONInspectionTests": 6 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -76,7 +76,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 108 + "JSONInspectionTests": 124 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,7 +89,7 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 108 + "JSONInspectionTests": 124 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -103,7 +103,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 5 + "JSONInspectionTests": 6 }, "export_functions": { "JSONSchemaTests": 1, diff --git a/README.md b/README.md index 8e50b7d..7a8aff6 100644 --- a/README.md +++ b/README.md @@ -722,3 +722,22 @@ inside JSONInspection. Graph construction and BFS are not public API and are not part of stable 1.6.1. The [experimental contract](Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md) records finite capacities, source/error precedence and one-ledger ownership. There is no graph product, performance claim or automatic promotion. + +## Source-bound graph candidate (unreleased) + +This development candidate extends JSONInspection with an explicit directed simple +graph view of two disjoint physical arrays. An immutable `JSONGraphProfile` names +exact typed paths and direct string ID/endpoint keys. Checked construction rejects +duplicate scalar-exact IDs, dangling endpoints and repeated directed pairs; self +edges, cycles, reverse edges and isolates remain valid data. BFS returns the complete +reachable component in source-edge order or throws. Node/edge values and original +node spans stay attached to the captured source. + +Each later read or query has its own explicit finite whole-source admission. +Build defaults to 10,000 nodes/50,000 edges; BFS defaults to 10,000 frontier, +visited and result entries. Storage and reducers stay private. Construction can +require quadratic work, and repeated reads re-admit the source. Logical retained +bytes are not RSS; controlled G4 performance remains HOLD with no speed claim. +See the [compiled graph guide](Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md) +and [standalone public consumer](Tools/GraphPublicQualification). This candidate +adds no product and does not establish a published release or merge approval. diff --git a/Scripts/ci.py b/Scripts/ci.py index 7274d5e..43cb0d5 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -5,7 +5,7 @@ from archive_candidate import files from preflight import collect, collect_linux from testing_events import read_events, ROOT_PARAMETER_CASES -from graph_acceptance import check_graph_cases, check_graph_receipt +from graph_acceptance import check_graph_cases, check_graph_receipt, check_public_graph_receipt from projection_acceptance import check_projection_cases, check_public_projection_receipt from test_discovery import DISCOVERY, ROOT_COUNTS, SHARED_COUNTS, PARAMETER_INVOCATIONS, check_restored @@ -17,7 +17,7 @@ SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' -SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics|graphG30ConcurrentViewsHaveIndependentHealth' +SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics|graphG30ConcurrentViewsHaveIndependentHealth|graphGP14ConcurrentQueries' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -36,6 +36,7 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], ['python3', '-B', 'Scripts/qualify_graph_context.py', '--output', str(output / 'graph-context')], ['python3', '-B', 'Scripts/qualify_graph.py', '--output', str(output / 'graph-compiler')], + ['python3', '-B', 'Scripts/qualify_public_graph.py', '--output', str(output / 'public-graph-consumer')], ['python3', '-B', 'Scripts/qualify_projections.py', '--output', str(output / 'projection-codegen')], ['python3', '-B', 'Scripts/qualify_public_projections.py', '--output', str(output / 'public-projection-consumer')]] return [c + ['--disable-xctest'] if sys.platform == 'linux' and c[:2] == ['swift', 'test'] else c for c in result] @@ -179,6 +180,7 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_navigation_reducer.py', '--output', str(output / 'owned-codegen')], ['python3', '-B', 'Scripts/qualify_graph_context.py', '--output', str(output / 'graph-context')], ['python3', '-B', 'Scripts/qualify_graph.py', '--output', str(output / 'graph-compiler')], + ['python3', '-B', 'Scripts/qualify_public_graph.py', '--output', str(output / 'public-graph-consumer')], ['swift', 'test', '--package-path', 'Tools/MobileQualification'], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/MobileQualification', 'MobileQualification'], ['swift', 'test', '--package-path', 'Tools/MCPReadiness'], @@ -309,6 +311,9 @@ def check_result(command, code, log, events=None): passed = passed and 'TESTING_EVENT_RECEIPTS passed cases=19' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_ergonomics.py']: passed = passed and 'ERGONOMICS_COMPILER passed commands=8 guide_fences=3' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_public_graph.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_public_graph_receipt(data) if command[:3] in (['python3', '-B', 'Scripts/qualify_graph_context.py'], ['python3', '-B', 'Scripts/qualify_graph.py']): data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) passed = passed and check_graph_receipt(data, context='qualify_graph_context.py' in command[2]) @@ -336,7 +341,7 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_html_browser.py']: passed = passed and 'JSONHTML_BROWSER passed engines=3 cases=76 skips=0' in log.splitlines() if 'Scripts/test_qualification_harness.py' in command: - passed = passed and 'HARNESS_TESTS passed cases=18' in log.splitlines() + passed = passed and 'HARNESS_TESTS passed cases=19' in log.splitlines() return passed, counts def main(): diff --git a/Scripts/graph_acceptance.py b/Scripts/graph_acceptance.py index 57c6f9a..42f721f 100644 --- a/Scripts/graph_acceptance.py +++ b/Scripts/graph_acceptance.py @@ -3,8 +3,8 @@ import json,pathlib ROOT=pathlib.Path(__file__).resolve().parents[1] POLICY=json.loads((ROOT/'Documentation/Qualification/GraphCases.json').read_text()) -EXPECTED=frozenset(POLICY['functions']) -SHARED=frozenset({'JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()'}) +EXPECTED=frozenset(POLICY['functions']) | frozenset(POLICY['public_functions']) +SHARED=frozenset({'JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()', 'JSONInspectionTests.graphGP14ConcurrentQueries()'}) def check_graph_cases(events,shared=False): names=[n for n in events['function_names'] if n.startswith('JSONInspectionTests.graphG')] return len(names)==len(set(names)) and set(names)==(SHARED if shared else EXPECTED) @@ -19,3 +19,20 @@ def check_graph_receipt(data,context=False): if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'].lower() in d.lower() for d in c.get('primary_diagnostics',[])):return False elif c.get('exit_code')!=0:return False return True + +PUBLIC_CHECKS = frozenset(POLICY['public_consumer_checks']) +def check_public_graph_receipt(data): + commands=data.get('commands',[]);results=data.get('consumer_results',{}) + if data.get('status')!='passed' or len(commands)!=45 or len({c.get('name') for c in commands})!=45 or set(results)!={'debug','release','address'}:return False + negatives=[c for c in commands if c.get('expected_diagnostic')] + if len(negatives)!=26:return False + for c in commands: + if c.get('status')!='passed' or c.get('compiler_crash') is not False:return False + if c.get('expected_diagnostic'): + if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'] in d for d in c.get('primary_diagnostics',[])):return False + elif c.get('exit_code')!=0:return False + for row in results.values(): + names=row.get('cases',[]) + if len(names)!=16 or len(set(names))!=16 or set(names)!=PUBLIC_CHECKS:return False + if not all(isinstance(row.get(k),int) and row[k]>0 for k in ['work_units','logical_retained_bytes','peak_logical_retained_bytes','admitted_nodes']):return False + return True diff --git a/Scripts/qualify_graph.py b/Scripts/qualify_graph.py index 6e64e12..f111fa5 100644 --- a/Scripts/qualify_graph.py +++ b/Scripts/qualify_graph.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 -"""Full-codegen proof of experimental graph invariants and package-only boundaries.""" +"""Full-codegen proof of graph invariants and non-public storage/reducer boundaries.""" import argparse, hashlib, json, pathlib, re, subprocess from archive_candidate import files ROOT = pathlib.Path(__file__).resolve().parents[1] @@ -29,9 +29,9 @@ ''' PROBES = [ ('positive', POSITIVE, None, False), - ('external-graph', 'import JSONInspection\nlet x = JSONGraphProjection.self\n', 'cannot find', True), - ('external-profile', 'import JSONInspection\nlet x = JSONGraphProfile.self\n', 'cannot find', True), - ('external-failure', 'import JSONInspection\nlet x = JSONGraphFailure.self\n', 'cannot find', True), + ('external-builder', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find', True), + ('external-query', 'import JSONInspection\nlet x = GraphQuery.self\n', 'cannot find', True), + ('external-stage', 'import JSONInspection\nlet x = GraphWorkStage.self\n', 'cannot find', True), ('external-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find', True), ('external-context', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find', True), ('core-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find', True), @@ -44,7 +44,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--output', required=True) out = ROOT / parser.parse_args().output; out.mkdir(parents=True, exist_ok=True) - receipt = dict(status='running', scope='same-package graph proof; all graph declarations remain non-public', + receipt = dict(status='running', scope='same-package graph proof; graph public surface plus private storage/reducers; same-package invariant proof', source_inputs=[dict(path=p.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files()], commands=[]) def save(): (out / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') def run(name, command, expected=None): diff --git a/Scripts/qualify_public_graph.py b/Scripts/qualify_public_graph.py new file mode 100644 index 0000000..f2abd0f --- /dev/null +++ b/Scripts/qualify_public_graph.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Qualify public graph imports, semantics and three executable DocC examples.""" +import argparse +import hashlib +import json +import pathlib +import re +import subprocess +import sys +from archive_candidate import files + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKS = {'GP%02d' % i for i in range(1,17)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + output = ROOT / parser.parse_args().output + output.mkdir(parents=True, exist_ok=True) + shared = (ROOT / 'Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift').read_text().split('\n@Test')[0].replace('import Testing\n', '') + actual = (ROOT / 'Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift').read_text() + if actual.split('\n@main struct')[0] != shared: + raise ValueError('Public consumer scenario bytes differ from the sixteen pinned root cases') + inputs = [{'path': p.relative_to(ROOT).as_posix(), + 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()] + receipt = dict(status='running', source_inputs=inputs, commands=[], consumer_results={}) + + def save(): + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr + crash = result.returncode < 0 or any(x in text for x in + ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation']) + diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + passed = not crash and (result.returncode == 0 if expected is None else + result.returncode > 0 and any(expected in d for d in diagnostics)) + log = output / (name + '.log') + log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], + exit_code=result.returncode, status='passed' if passed else 'failed', compiler_crash=crash, + expected_diagnostic=expected, primary_diagnostics=diagnostics, + log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: + raise ValueError('Unexpected compiler/consumer outcome: ' + name) + return result.stdout + + def consumer(name, command): + text = run(name, command) + checks = re.findall(r'^GRAPH_PUBLIC_CHECK (\S+) passed$', text, re.M) + if len(checks) != 16 or set(checks) != CHECKS or 'GRAPH_PUBLIC_CONSUMER passed cases=16' not in text: + raise ValueError('Missing/duplicate graph consumer checks: ' + name) + resource = re.search(r'^GRAPH_PUBLIC_RESOURCE work=(\d+) retained=(\d+) peak=(\d+) nodes=(\d+)$', text, re.M) + if resource is None or not all(int(x) > 0 for x in resource.groups()): + raise ValueError('Missing finite cumulative report: ' + name) + receipt['consumer_results'][name] = dict(cases=checks, work_units=int(resource[1]), + logical_retained_bytes=int(resource[2]), peak_logical_retained_bytes=int(resource[3]), + admitted_nodes=int(resource[4])) + save() + + try: + for config in ['debug', 'release']: + run(config + '-build', ['swift', 'build', '-c', config, '--target', 'JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift', 'build', '-c', config, '--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + objects = [] + for module in ['JSON', 'JSONNavigation', 'JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: + raise ValueError('Missing built objects: ' + module) + objects.extend(str(p) for p in selected) + common = ['swiftc', '-swift-version', '6', '-warnings-as-errors', '-package-name', + 'graph_external_consumer', '-I', str(modules), + '-O' if config == 'release' else '-Onone'] + consumer(config, ['swift', 'run', '-c', config, '--package-path', + 'Tools/GraphPublicQualification', 'GraphPublicConsumer']) + guide = ROOT / 'Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md' + fences = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(fences) != 3: + raise ValueError('Changed three-fence guide inventory') + for index, fence in enumerate(fences): + file = output / ('guide-' + str(index) + '.swift') + file.write_text('import Foundation\nimport JSON\nimport JSONNavigation\nimport JSONInspection\n' + '@main struct Guide { static func main() throws {\n' + fence + '\n} }\n') + binary = output / ('guide-' + config + '-' + str(index)) + run(config + '-guide-' + str(index), common + ['-parse-as-library', str(file), *objects, '-o', str(binary)]) + run(config + '-guide-' + str(index) + '-run', [str(binary)]) + probes = [('core-import-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find'), ('scope-isolation', 'import JSON\nlet x = JSONReadScope.self\n', 'cannot find'), ('capability-isolation', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find'), ('forged-graph', 'import JSON\nimport JSONNavigation\nimport JSONInspection\nfunc f(_ d: JSONDocument, _ p: JSONGraphProfile, _ s: JSONTraversalSummary) { _ = JSONGraphProjection(document: d, profile: p, nodeIDs: [], edges: [], offsets: [], adjacency: [], traversal: s) }\n', 'initializer is inaccessible'), ('immutable-profile', 'import JSONInspection\nfunc f(_ p: inout JSONGraphProfile) { p.nodesAt.removeAll() }\n', "'let' constant"), ('immutable-count', 'import JSONInspection\nfunc f(_ p: inout JSONGraphProjection) { p.nodeCount = 0 }\n', 'get-only property'), ('private-ids', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.nodeIDs }\n', 'is inaccessible'), ('private-edges', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.edges }\n', 'is inaccessible'), ('private-offsets', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.offsets }\n', 'is inaccessible'), ('private-adjacency', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.adjacency }\n', 'is inaccessible'), ('private-builder', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find'), ('private-query', 'import JSONInspection\nlet x = GraphQuery.self\n', 'cannot find'), ('private-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find')] + for name, source, expected in probes: + file = output / (name + '.swift'); file.write_text(source) + run(config + '-' + name, common + ['-parse-as-library', '-c', str(file), + '-o', str(output / (config + '-' + name + '.o'))], expected) + consumer('address', ['swift', 'run', '-c', 'release', '--sanitize', 'address', + '--scratch-path', '.build/graph-public-consumer-address', '--package-path', + 'Tools/GraphPublicQualification', 'GraphPublicConsumer']) + receipt['status'] = 'passed' + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + receipt.update(status='failed', error=str(error)) + save() + print('PUBLIC_GRAPH_COMPILER ' + receipt['status'] + + ' configurations=2 consumer_cases=16 address_cases=16 guide_fences=3 negative_probes=26') + return 0 if receipt['status'] == 'passed' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 73360f0..0a12338 100644 --- a/Scripts/test_qualification_harness.py +++ b/Scripts/test_qualification_harness.py @@ -12,7 +12,7 @@ from coverage import executable_objects, shared_objects, validate_export, without_schema_objects from test_discovery import DISCOVERY, ROOT_COUNTS, PARAMETER_INVOCATIONS, check_root, check_restored, RESTORED_FUNCTIONS from ci import check_result, commands -from graph_acceptance import EXPECTED as GRAPH_FUNCTIONS, check_graph_cases, check_graph_receipt +from graph_acceptance import EXPECTED as GRAPH_FUNCTIONS, check_graph_cases, check_graph_receipt, check_public_graph_receipt, PUBLIC_CHECKS as GRAPH_PUBLIC_CHECKS from projection_acceptance import EXPECTED as PROJECTION_FUNCTIONS, PUBLIC_CHECKS, check_projection_cases, check_public_projection_receipt class HarnessTests(unittest.TestCase): @@ -62,7 +62,7 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel # Combined-source checks must retain both independently owned optional products. for module, count, command in [ ('JSONNavigationTests', 47, ['swift', 'test', '--filter', 'navigation']), - ('JSONInspectionTests', 108, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|graphG']), + ('JSONInspectionTests', 124, ['swift', 'test', '--filter', 'inspection|snapshot|projectionP|graphG']), ('JSONSchemaExamplesTests', 98, ['swift', 'test', '--test-product', 'JSONSchemaExamplesTests']), ]: events = dict(bundle_counts=[count], module_function_counts={module: count}, @@ -76,12 +76,12 @@ def test_composition_discovery_and_capacity_limits_are_not_blanket_successes(sel for key in ('root_functions', 'portable_root_functions', 'wasi_root_functions'): self.assertEqual(policy[key]['JSONNavigationTests'], 47) self.assertEqual(policy[key]['JSONSchemaExamplesTests'], 98) - self.assertEqual(policy[key]['JSONInspectionTests'], 108) + self.assertEqual(policy[key]['JSONInspectionTests'], 124) 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'], 5) + self.assertEqual(policy[key]['JSONInspectionTests'], 6) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): @@ -98,7 +98,7 @@ def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(s def test_graph_identity_and_compiler_receipts_reject_false_success(self): names=sorted(GRAPH_FUNCTIONS) - self.assertEqual(len(names),32) + self.assertEqual(len(names),48) self.assertTrue(check_graph_cases({'function_names':names})) for broken in [names[:-1],names+names[:1],['WrongModule.'+names[0].split('.',1)[1]]+names[1:]]: self.assertFalse(check_graph_cases({'function_names':broken})) @@ -113,6 +113,23 @@ def test_graph_identity_and_compiler_receipts_reject_false_success(self): broken=copy.deepcopy(valid);broken['commands'][0].update(change) self.assertFalse(check_graph_receipt(broken,context)) + def test_public_graph_receipt_rejects_incomplete_or_incidental_success(self): + commands=[dict(name=str(i),status='passed',compiler_crash=False,exit_code=1 if i<26 else 0, + expected_diagnostic='inaccessible' if i<26 else None,primary_diagnostics=['inaccessible'] if i<26 else []) for i in range(45)] + valid=dict(status='passed',commands=commands,consumer_results={config:dict(cases=sorted(GRAPH_PUBLIC_CHECKS),work_units=1,logical_retained_bytes=1,peak_logical_retained_bytes=1,admitted_nodes=1) for config in ['debug','release','address']}) + self.assertTrue(check_public_graph_receipt(valid)) + broken=copy.deepcopy(valid);broken['commands'].pop();self.assertFalse(check_public_graph_receipt(broken)) + for change in [dict(compiler_crash=True),dict(exit_code=0),dict(primary_diagnostics=['unrelated'])]: + broken=copy.deepcopy(valid);broken['commands'][0].update(change);self.assertFalse(check_public_graph_receipt(broken)) + for config in ['debug','release','address']: + for mode in ['missing','duplicate','renamed','zero']: + broken=copy.deepcopy(valid);row=broken['consumer_results'][config] + if mode=='missing':row['cases'].pop() + elif mode=='duplicate':row['cases'][0]=row['cases'][1] + elif mode=='renamed':row['cases'][0]='unexpected' + else:row['work_units']=0 + self.assertFalse(check_public_graph_receipt(broken)) + def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) @@ -204,8 +221,8 @@ def test_exact_discovery_rejects_missing_duplicate_and_changed_parameters(self): identity['function_names'][0] = 'JSONTests.unrelatedReplacement()' self.assertFalse(check_restored(identity)) harness = ['python3', '-B', 'Scripts/test_qualification_harness.py'] - self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=18')[0]) - for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=16', '']: + self.assertTrue(check_result(harness, 0, 'HARNESS_TESTS passed cases=19')[0]) + for stale in ['HARNESS_TESTS passed cases=10', 'HARNESS_TESTS passed cases=11', 'HARNESS_TESTS passed cases=12', 'HARNESS_TESTS passed cases=13', 'HARNESS_TESTS passed cases=14', 'HARNESS_TESTS passed cases=15', 'HARNESS_TESTS passed cases=16', 'HARNESS_TESTS passed cases=17', 'HARNESS_TESTS passed cases=18', '']: self.assertFalse(check_result(harness, 0, stale)[0]) lifetime = ['python3', '-B', 'Scripts/qualify_schema_example_lifetime.py'] marker = 'SCHEMA_EXAMPLES_CORPUS passed mode=owned scopes=128 values=6 exact=1' diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index 28c9018..c5e3271 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:5e2605482c87b004531aa1777500fe3936f7e7dd0a40d917019e7687324b2e75" + package static let revision = "sha256:480e22d7a4e7c8011e9d1197af921600fc5bc858a700b297aa86f87b2f90730a" package static let kind = "source-sha256" } diff --git a/Sources/JSONInspection/Documentation.docc/GraphExperiment.md b/Sources/JSONInspection/Documentation.docc/GraphExperiment.md index f9c217c..a436018 100644 --- a/Sources/JSONInspection/Documentation.docc/GraphExperiment.md +++ b/Sources/JSONInspection/Documentation.docc/GraphExperiment.md @@ -15,3 +15,5 @@ Construction uses the original Navigation ledger. Later catalogue, source and BF queries are explicit independent full-source operations. Logical retained-byte quotas do not cap process RSS or caller computation. Public API promotion, merge and release require a separate owner decision. Controlled timing remains HOLD. + +This page records the original package-only experiment. The owner-reviewed successor candidate exposes the narrow checked API described in ; storage and reducers remain non-public. No release is implied. diff --git a/Sources/JSONInspection/Documentation.docc/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md index 02a5a90..6f0a53c 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -43,3 +43,10 @@ or numeric matrix. Sampling does not exempt any source from admission. ### Experimental scope - + +### Source-bound directed graphs + +- +- ``JSONGraphProfile`` +- ``JSONGraphProjection`` +- ``JSONGraphFailure`` diff --git a/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md b/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md new file mode 100644 index 0000000..47c7de7 --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md @@ -0,0 +1,62 @@ +# Build a source-bound directed graph + +An explicit profile names two disjoint physical arrays and their direct string fields. +Build validates every row and publishes only a complete directed simple graph. +ID and field-key identity uses exact decoded Unicode scalars. Arrays retain source +order. Duplicate IDs, dangling endpoints and repeated directed pairs throw; reverse +edges, self-edges, cycles and isolated nodes are valid data. + +## Capture the complete source + +```swift +let graphSource = try JSONParser().parseDocument(Data(#"{"nodes":[{"id":"A","n":9007199254740993},{"id":"B"}],"edges":[{"from":"A","to":"B"}]}"#.utf8)) +let graphProfile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +let graphBuild = try JSONGraphProjection.build(document: graphSource, profile: graphProfile) +precondition(graphBuild.result.nodeCount == 2 && graphBuild.result.edgeCount == 1) +precondition(graphBuild.statistics[.workUnits] > 0) +``` + +## Traverse a complete reachable component + +```swift +let graph = try JSONGraphProjection.build(document: .init(root: JSON.parse(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#)), profile: .init(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to")).result +let outgoing = try graph.outgoingEdges(of: 0).result +let reachable = try graph.breadthFirst(fromNode: 0).result +precondition(outgoing == [0]) +precondition(reachable == [0, 1]) +``` + +## Preserve semantic failures + +```swift +let duplicate = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#)) +let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +do { + _ = try JSONGraphProjection.build(document: duplicate, profile: profile) + preconditionFailure("Duplicate ID was accepted") +} catch JSONGraphFailure.duplicateNodeID(let first, let second) { + precondition(first == [.key("nodes"), .index(0)]) + precondition(second == [.key("nodes"), .index(1)]) +} +``` + +Every later value, span, outgoing-edge or BFS query is a new independent whole-source +admission under the supplied finite limits. Returned JSON aliases remain charged. +No constructor budget is silently reused. Build defaults to 10,000 nodes and 50,000 +edges; outgoing queries default to 50,000 results; BFS defaults to 10,000 live pending +nodes, 10,000 visited nodes and 10,000 results. All caps are inclusive. Zero caps +refuse nonempty work; cancellation, quotas and typed semantic failures throw without +successful partial artifacts. Missing, null and wrong kind remain distinguishable. +Core selection errors retain their typed cause; operational errors are not graph facts. + +Ordinals name positions in this immutable captured source, not durable graph IDs. +There is no replacement-source argument. Replacing a document root drops obsolete +source spans; an existing graph keeps its original source and map. Node spans can be +nil for a source without a map. Storage, reducers and mutable meters are private. + +BFS uses source-edge order and visited-at-enqueue, including the start. Its output +certifies the reachable component, not global connectivity, acyclicity or schema validity. +Exact construction uses bounded linear scans and can cost quadratic work; repeated +queries re-admit the whole source. Logical retention is not RSS. Controlled G4 +performance remains HOLD, with no speed claim. No new product, graph serializer, +mutation, topology, weighted edges or persistent identity is supplied. diff --git a/Sources/JSONInspection/Graph/JSONGraphProfile.swift b/Sources/JSONInspection/Graph/JSONGraphProfile.swift index 3ab48e4..4e197db 100644 --- a/Sources/JSONInspection/Graph/JSONGraphProfile.swift +++ b/Sources/JSONInspection/Graph/JSONGraphProfile.swift @@ -2,14 +2,28 @@ import JSON import JSONNavigation -/// Experimental explicit direct-field profile; all locations are absolute/typed. -package struct JSONGraphProfile: Sendable { - package let nodesAt: [JSONLocationComponent] - package let edgesAt: [JSONLocationComponent] - package let nodeIDKey: JSONKey - package let edgeSourceKey: JSONKey - package let edgeTargetKey: JSONKey - package init(nodesAt: [JSONLocationComponent], edgesAt: [JSONLocationComponent], +/// Explicit physical arrays and direct string fields for a source-bound directed simple graph. +/// +/// Paths use exact typed components; roles are never guessed. Explicit field reuse is valid. +public struct JSONGraphProfile: Sendable { + /// Absolute typed location of the complete node array. + public let nodesAt: [JSONLocationComponent] + /// Absolute typed location of the complete edge array, disjoint from the node array. + public let edgesAt: [JSONLocationComponent] + /// Exact direct key of each node’s required string identifier. + public let nodeIDKey: JSONKey + /// Exact direct key of each edge’s required source string identifier. + public let edgeSourceKey: JSONKey + /// Exact direct key of each edge’s required target string identifier. + public let edgeTargetKey: JSONKey + /// Defines roles without interpreting path strings, normalizing keys or validating a source. + /// - Parameters: + /// - nodesAt: Absolute typed node-array location. + /// - edgesAt: Absolute typed edge-array location; build rejects overlap. + /// - nodeIDKey: Direct required string ID key in node rows. + /// - edgeSourceKey: Direct required string source ID key in edge rows. + /// - edgeTargetKey: Direct required string target ID key in edge rows. + public init(nodesAt: [JSONLocationComponent], edgesAt: [JSONLocationComponent], nodeIDKey: JSONKey, edgeSourceKey: JSONKey, edgeTargetKey: JSONKey) { self.nodesAt = nodesAt; self.edgesAt = edgesAt; self.nodeIDKey = nodeIDKey self.edgeSourceKey = edgeSourceKey; self.edgeTargetKey = edgeTargetKey @@ -17,26 +31,40 @@ package struct JSONGraphProfile: Sendable { } /// Graph semantic refusals retain exact original locations, never normalized IDs. -package enum JSONGraphFailure: Error, Sendable, Equatable { +public enum JSONGraphFailure: Error, Sendable, Equatable { + /// The node array exceeds the explicit inclusive node cap. case nodeLimitExceeded(limit: Int) + /// The edge array exceeds the explicit inclusive edge cap. case edgeLimitExceeded(limit: Int) + /// An original node or edge row is not an object. case rowNotObject(at: [JSONLocationComponent], kind: JSONTraversalKind) + /// A required direct ID or endpoint field is absent. case idMissing(at: [JSONLocationComponent]) + /// A required direct ID or endpoint field is explicitly null. case idNull(at: [JSONLocationComponent]) + /// A required direct ID or endpoint field is not a string. case idWrongKind(at: [JSONLocationComponent], kind: JSONTraversalKind) + /// Two node IDs have identical decoded scalar sequences; both row paths are retained. case duplicateNodeID(first: [JSONLocationComponent], second: [JSONLocationComponent]) + /// An edge endpoint has no exact matching node ID; its field path is retained. case danglingEndpoint(at: [JSONLocationComponent]) + /// Two original edge rows have the same ordered endpoint pair. case duplicateDirectedEdge(first: [JSONLocationComponent], second: [JSONLocationComponent]) + /// The requested node ordinal is outside this captured graph. case nodeOutOfBounds(Int) + /// The requested edge ordinal is outside this captured graph. case edgeOutOfBounds(Int) + /// BFS would exceed its live pending-node frontier cap. case frontierLimitExceeded(limit: Int) + /// BFS would exceed its total visited-node cap. case visitedLimitExceeded(limit: Int) + /// A query would exceed its complete-result cap. case resultLimitExceeded(limit: Int) } /// Trusted row ordinals in this captured source, not durable graph identifiers. package struct JSONGraphEdgeRecord: Sendable, Equatable { - package let row: Int - package let source: Int - package let target: Int + public let row: Int + public let source: Int + public let target: Int } diff --git a/Sources/JSONInspection/Graph/JSONGraphProjection.swift b/Sources/JSONInspection/Graph/JSONGraphProjection.swift index 23658bb..316044c 100644 --- a/Sources/JSONInspection/Graph/JSONGraphProjection.swift +++ b/Sources/JSONInspection/Graph/JSONGraphProjection.swift @@ -2,17 +2,27 @@ import JSON import JSONNavigation -/// Experimental immutable source-attached directed simple graph. Not public API. -package struct JSONGraphProjection: Sendable { - package let document: JSONDocument - package let profile: JSONGraphProfile +/// An immutable directed simple graph attached to one completely admitted source document. +/// +/// Build validates every node, edge and adjacency slot before publication. Source-local +/// ordinals preserve physical row order; they are not durable or cross-graph identities. +/// Later queries independently admit the whole captured source under their explicit limits. +/// Construction uses bounded exact scans and may require quadratic work. +public struct JSONGraphProjection: Sendable { + /// The original captured source, including its coherent source map when supplied. + public let document: JSONDocument + /// The explicit immutable selection and field roles validated at build. + public let profile: JSONGraphProfile package let nodeIDs: [String] package let edges: [JSONGraphEdgeRecord] package let offsets: [Int] package let adjacency: [Int] - package let traversal: JSONTraversalSummary - package var nodeCount: Int { nodeIDs.count } - package var edgeCount: Int { edges.count } + /// Complete construction coverage for both selected arrays. + public let traversal: JSONTraversalSummary + /// Number of completely validated original node rows. + public var nodeCount: Int { nodeIDs.count } + /// Number of completely validated original edge rows. + public var edgeCount: Int { edges.count } fileprivate init(document: JSONDocument, profile: JSONGraphProfile, nodeIDs: [String], edges: [JSONGraphEdgeRecord], offsets: [Int], adjacency: [Int], traversal: JSONTraversalSummary) { @@ -21,7 +31,15 @@ package struct JSONGraphProjection: Sendable { } /// Complete source admission, nodes, edges and adjacency share one original meter. - package static func build(document: JSONDocument, profile: JSONGraphProfile, + /// - Parameters: + /// - document: Immutable complete source; unselected subtrees are admitted too. + /// - profile: Strict disjoint typed array roots and exact direct string field keys. + /// - limits: One cumulative finite budget for source, validation, metadata and transfer. + /// - maximumNodes: Inclusive node-row cap; defaults to 10,000. + /// - maximumEdges: Inclusive edge-row cap; defaults to 50,000. + /// - Returns: A complete source-owning graph and its construction statistics. + /// - Throws: Typed graph/core selection errors, resource failure or cancellation; never a partial graph. + public static func build(document: JSONDocument, profile: JSONGraphProfile, limits: JSONLimits = .balanced, maximumNodes: Int = 10_000, maximumEdges: Int = 50_000) throws -> JSONOperationReport { guard maximumNodes >= 0, maximumEdges >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } @@ -32,7 +50,12 @@ package struct JSONGraphProjection: Sendable { } /// A new whole-source operation; returns original physical outgoing edge ordinals. - package func outgoingEdges(of node: Int, limits: JSONLimits = .balanced, maximumResults: Int = 50_000) + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: New independent whole-source and query budget. + /// - maximumResults: Inclusive returned edge-ordinal cap; defaults to 50,000. + /// - Returns: Every outgoing original edge-row ordinal in source order, or throws. + public func outgoingEdges(of node: Int, limits: JSONLimits = .balanced, maximumResults: Int = 50_000) throws -> JSONOperationReport<[Int]> { guard maximumResults >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } try checkNode(node) @@ -43,7 +66,16 @@ package struct JSONGraphProjection: Sendable { } /// Complete reachable component, including start; never a partial success. - package func breadthFirst(fromNode node: Int, limits: JSONLimits = .balanced, + /// Visited-at-enqueue and source-edge order make cycles/self-edges deterministic. + /// Queue history remains owned output; the frontier cap counts only pending nodes. + /// - Parameters: + /// - node: Original start node ordinal, included in the result. + /// - limits: New independent whole-source, metadata and traversal budget. + /// - maximumFrontier: Inclusive live pending-node cap; defaults to 10,000. + /// - maximumVisited: Inclusive total enqueued-node cap; defaults to 10,000. + /// - maximumResults: Inclusive returned node-ordinal cap; defaults to 10,000. + /// - Returns: The complete reachable component in BFS order, or throws without partial success. + public func breadthFirst(fromNode node: Int, limits: JSONLimits = .balanced, maximumFrontier: Int = 10_000, maximumVisited: Int = 10_000, maximumResults: Int = 10_000) throws -> JSONOperationReport<[Int]> { guard maximumFrontier >= 0, maximumVisited >= 0, maximumResults >= 0 else { @@ -57,16 +89,28 @@ package struct JSONGraphProjection: Sendable { .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) } - package func nodeValue(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + /// Returns an original node row under a new whole-source admission and owned-output charge. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: Independent finite source/selection/output budget. + public func nodeValue(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { try checkNode(node) return try value(at: profile.nodesAt, row: node, limits: limits) } - package func edgeValue(at edge: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + /// Returns an original edge row under a new whole-source admission and owned-output charge. + /// - Parameters: + /// - edge: Original edge-row ordinal in this graph. + /// - limits: Independent finite source/selection/output budget. + public func edgeValue(at edge: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { guard edge >= 0, edge < edgeCount else { throw JSONGraphFailure.edgeOutOfBounds(edge) } return try value(at: profile.edgesAt, row: edge, limits: limits) } /// Source-map lookup under a new explicit full-source admission. - package func nodeSpan(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: Independent finite source and source-map lookup budget. + /// - Returns: The original value span, or nil when this source has no matching map entry. + public func nodeSpan(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { try checkNode(node) let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } let state = try scope.check(); let path = try rowPath(profile.nodesAt, row: node, scope: scope, state: state) diff --git a/Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift b/Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift new file mode 100644 index 0000000..61ba51b --- /dev/null +++ b/Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONNavigation +import JSONInspection + +// Shared scenario inputs are also executed by the distinct-package public consumer. +enum GraphPublicCases { + enum CheckFailure: Error { case failed(Int) } + static let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + static let text = #"{"nodes":[{"id":"A","n":9007199254740993},{"id":"B"},{"id":"C"},{"id":"isolate"}],"edges":[{"from":"A","to":"C"},{"from":"A","to":"B"},{"from":"C","to":"A"}]}"# + static func check(_ value: Bool, _ id: Int) throws { if !value { throw CheckFailure.failed(id) } } + static func doc(_ text: String = text) throws -> JSONDocument { try JSONParser().parseDocument(Data(text.utf8)) } + static func build(_ text: String = text) throws -> JSONOperationReport { try JSONGraphProjection.build(document: doc(text), profile: profile) } + static func graphError(_ expected: JSONGraphFailure, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(100) } catch let failure as JSONGraphFailure { try check(failure == expected, 101) } + } + static func resource(_ metric: JSONResourceMetric, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(102) } catch let failure as JSONFailure { try check(failure.category == .resource && failure.resource?.metric == metric, 103) } + } + static func run(_ id: Int) async throws { + switch id { + case 1: + let result = try build(); let graph: JSONGraphProjection = result.result + try check(graph.nodeCount == 4 && graph.edgeCount == 3 && result.statistics[.workUnits] > 0, id) + case 2: + let document = try doc(#"{"":{"0":[{"id":"A"}]},"edges":[]}"#) + let exact = JSONGraphProfile(nodesAt: [.key(""), .key("0")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let graph = try JSONGraphProjection.build(document: document, profile: exact).result + try check(try graph.nodeValue(at: 0).result == JSONPointer(parsing: "//0/0").resolve(in: document.root), id) + let indexed = JSONGraphProfile(nodesAt: [.index(0)], edgesAt: exact.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + do { _ = try JSONGraphProjection.build(document: document, profile: indexed); throw CheckFailure.failed(id) } + catch let failure as JSONAccessFailure { try check(failure.cause.code == .wrongKind, id) } + case 3: + let graph = try build().result + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + try check(try graph.outgoingEdges(of: 0).result == [0,1], id) + try check(try graph.breadthFirst(fromNode: 3).result == [3], id) + case 4: + let graph = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + let explicitReuse = JSONGraphProfile(nodesAt: profile.nodesAt, edgesAt: profile.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "from") + let reused = try JSONGraphProjection.build(document: doc(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A"}]}"#), profile: explicitReuse).result + try check(try reused.breadthFirst(fromNode: 0).result == [0], id) + case 5: + try graphError(.duplicateNodeID(first: [.key("nodes"),.index(0)], second: [.key("nodes"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#) + } + case 6: + let graph = try build(#"{"nodes":[{"id":"é"},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#).result + let a = try graph.nodeValue(at: 0).result.requiredMember("id"), b = try graph.nodeValue(at: 1).result.requiredMember("id") + try check(a != b && graph.nodeCount == 2, id) + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + case 7: + try graphError(.duplicateDirectedEdge(first: [.key("edges"),.index(0)], second: [.key("edges"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"A"}]}"#) + } + let reverse = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(reverse.edgeCount == 2, id) + case 8: + try graphError(.danglingEndpoint(at: [.key("edges"),.index(0),.key("to")])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"missing"}]}"#) + } + case 9: + let path: [JSONLocationComponent] = [.key("nodes"),.index(0),.key("id")] + for (row,error) in [("{}", JSONGraphFailure.idMissing(at: path)), (#"{"id":null}"#, .idNull(at: path)), (#"{"id":1}"#, .idWrongKind(at: path, kind: .number)), ("false", .rowNotObject(at: [.key("nodes"),.index(0)], kind: .boolean))] { + try graphError(error) { _ = try build("{\"nodes\":[" + row + "],\"edges\":[]}") } + } + try graphError(.idNull(at: [.key("edges"),.index(0),.key("to")])) { _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":null}]}"#) } + case 10: + let graph = try build().result + try graphError(.nodeOutOfBounds(-1)) { _ = try graph.nodeValue(at: -1) } + try graphError(.edgeOutOfBounds(3)) { _ = try graph.edgeValue(at: 3) } + try graphError(.frontierLimitExceeded(limit: 0)) { _ = try graph.breadthFirst(fromNode: 0, maximumFrontier: 0) } + try graphError(.resultLimitExceeded(limit: 0)) { _ = try graph.outgoingEdges(of: 0, maximumResults: 0) } + let empty = try JSONGraphProjection.build(document: doc(#"{"nodes":[],"edges":[]}"#), profile: profile, maximumNodes: 0, maximumEdges: 0).result + try check(empty.nodeCount == 0 && empty.edgeCount == 0, id) + do { _ = try graph.breadthFirst(fromNode: 0, maximumResults: -1); throw CheckFailure.failed(id) } + catch let failure as JSONFailure { try check(failure.code == .invalidLimits, id) } + case 11: + let graph = try build().result, baseline = try graph.breadthFirst(fromNode: 0) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + try check(try graph.breadthFirst(fromNode: 0, limits: limits).statistics[.workUnits] == baseline.statistics[.workUnits], id) + limits[.workUnits] -= 1 + try resource(.workUnits) { _ = try graph.breadthFirst(fromNode: 0, limits: limits) } + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 12: + let result = try build(), graph = result.result, row = try graph.nodeValue(at: 0) + try check(graph.document.sourceBytes == doc().sourceBytes && result.statistics[.retainedBytes] > 0 && row.statistics[.retainedBytes] > 0, id) + let number = try row.result.requiredMember("n").numberValue + try check(try number?.integer(as: UInt64.self) == 9_007_199_254_740_993, id) + try check(try graph.edgeValue(at: 0).result == JSONPointer(parsing: "/edges/0").resolve(in: graph.document.root), id) + case 13: + let graph = try build().result + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + do { _ = try graph.breadthFirst(fromNode: 0); throw CheckFailure.failed(id) } catch is CancellationError {} + do { _ = try build(); throw CheckFailure.failed(id) } catch is CancellationError {} + } + try await task.value + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 14: + let graph = try build().result + try await withThrowingTaskGroup(of: [Int].self) { group in + for _ in 0..<8 { group.addTask { + try graphError(.resultLimitExceeded(limit: 1)) { _ = try graph.breadthFirst(fromNode: 0, maximumResults: 1) } + return try graph.breadthFirst(fromNode: 0).result + } } + for try await result in group { try check(result == [0,2,1], id) } + } + case 15: + let original = try doc(), graph = try JSONGraphProjection.build(document: original, profile: profile).result + let replacement = original.replacingRoot(try JSON.parse(#"{"nodes":[{"id":"new"}],"edges":[]}"#)) + let next = try JSONGraphProjection.build(document: replacement, profile: profile).result + try check(try graph.nodeSpan(at: 0).result != nil && next.nodeSpan(at: 0).result == nil, id) + try check(graph.nodeCount == 4 && next.nodeCount == 1 && graph.document.root == original.root, id) + case 16: + // The selected graph is tiny, but an unselected subtree still undergoes core admission. + let invalid = JSONDocument(root: .object(try JSONObject(members: [JSONMember("nodes", value: .array([])),JSONMember("edges", value: .array([])),JSONMember("unselected", value: .string(String(repeating: "x", count: 32)))]))) + var limits = JSONLimits.balanced; limits[.stringBytes] = 16 + try resource(.stringBytes) { _ = try JSONGraphProjection.build(document: invalid, profile: profile, limits: limits) } + default: throw CheckFailure.failed(id) + } + } +} + +@Test func graphGP01OptInSurface() async throws { try await GraphPublicCases.run(1) } + +@Test func graphGP02TypedRootAndEmptyKeys() async throws { try await GraphPublicCases.run(2) } + +@Test func graphGP03SourceOrderBFS() async throws { try await GraphPublicCases.run(3) } + +@Test func graphGP04SelfLoopAndCycle() async throws { try await GraphPublicCases.run(4) } + +@Test func graphGP05DuplicateIDPaths() async throws { try await GraphPublicCases.run(5) } + +@Test func graphGP06ScalarDistinctIDs() async throws { try await GraphPublicCases.run(6) } + +@Test func graphGP07OrderedPairs() async throws { try await GraphPublicCases.run(7) } + +@Test func graphGP08DanglingEndpoint() async throws { try await GraphPublicCases.run(8) } + +@Test func graphGP09MissingNullWrongKind() async throws { try await GraphPublicCases.run(9) } + +@Test func graphGP10BoundsAndZeroCaps() async throws { try await GraphPublicCases.run(10) } + +@Test func graphGP11IndependentWholeSourceBudget() async throws { try await GraphPublicCases.run(11) } + +@Test func graphGP12OwnedValueAndSourceHandoff() async throws { try await GraphPublicCases.run(12) } + +@Test func graphGP13CancellationAndHealth() async throws { try await GraphPublicCases.run(13) } + +@Test func graphGP14ConcurrentQueries() async throws { try await GraphPublicCases.run(14) } + +@Test func graphGP15SourceMapCoherence() async throws { try await GraphPublicCases.run(15) } + +@Test func graphGP16UnselectedAdmission() async throws { try await GraphPublicCases.run(16) } diff --git a/Tools/GraphPublicQualification/Package.swift b/Tools/GraphPublicQualification/Package.swift new file mode 100644 index 0000000..e099d57 --- /dev/null +++ b/Tools/GraphPublicQualification/Package.swift @@ -0,0 +1,4 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "GraphPublicQualification", platforms: [.macOS("27.0"), .iOS("27.0")], dependencies: [.package(name: "Swift-JSON", path: "../..")], targets: [.executableTarget(name: "GraphPublicConsumer", dependencies: [.product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONNavigation", package: "Swift-JSON"), .product(name: "JSONInspection", package: "Swift-JSON")])], swiftLanguageModes: [.v6]) diff --git a/Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift b/Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift new file mode 100644 index 0000000..d8e126a --- /dev/null +++ b/Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONNavigation +import JSONInspection + +// Shared scenario inputs are also executed by the distinct-package public consumer. +enum GraphPublicCases { + enum CheckFailure: Error { case failed(Int) } + static let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + static let text = #"{"nodes":[{"id":"A","n":9007199254740993},{"id":"B"},{"id":"C"},{"id":"isolate"}],"edges":[{"from":"A","to":"C"},{"from":"A","to":"B"},{"from":"C","to":"A"}]}"# + static func check(_ value: Bool, _ id: Int) throws { if !value { throw CheckFailure.failed(id) } } + static func doc(_ text: String = text) throws -> JSONDocument { try JSONParser().parseDocument(Data(text.utf8)) } + static func build(_ text: String = text) throws -> JSONOperationReport { try JSONGraphProjection.build(document: doc(text), profile: profile) } + static func graphError(_ expected: JSONGraphFailure, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(100) } catch let failure as JSONGraphFailure { try check(failure == expected, 101) } + } + static func resource(_ metric: JSONResourceMetric, _ body: () throws -> Void) throws { + do { try body(); throw CheckFailure.failed(102) } catch let failure as JSONFailure { try check(failure.category == .resource && failure.resource?.metric == metric, 103) } + } + static func run(_ id: Int) async throws { + switch id { + case 1: + let result = try build(); let graph: JSONGraphProjection = result.result + try check(graph.nodeCount == 4 && graph.edgeCount == 3 && result.statistics[.workUnits] > 0, id) + case 2: + let document = try doc(#"{"":{"0":[{"id":"A"}]},"edges":[]}"#) + let exact = JSONGraphProfile(nodesAt: [.key(""), .key("0")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let graph = try JSONGraphProjection.build(document: document, profile: exact).result + try check(try graph.nodeValue(at: 0).result == JSONPointer(parsing: "//0/0").resolve(in: document.root), id) + let indexed = JSONGraphProfile(nodesAt: [.index(0)], edgesAt: exact.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + do { _ = try JSONGraphProjection.build(document: document, profile: indexed); throw CheckFailure.failed(id) } + catch let failure as JSONAccessFailure { try check(failure.cause.code == .wrongKind, id) } + case 3: + let graph = try build().result + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + try check(try graph.outgoingEdges(of: 0).result == [0,1], id) + try check(try graph.breadthFirst(fromNode: 3).result == [3], id) + case 4: + let graph = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + let explicitReuse = JSONGraphProfile(nodesAt: profile.nodesAt, edgesAt: profile.edgesAt, nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "from") + let reused = try JSONGraphProjection.build(document: doc(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A"}]}"#), profile: explicitReuse).result + try check(try reused.breadthFirst(fromNode: 0).result == [0], id) + case 5: + try graphError(.duplicateNodeID(first: [.key("nodes"),.index(0)], second: [.key("nodes"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#) + } + case 6: + let graph = try build(#"{"nodes":[{"id":"é"},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#).result + let a = try graph.nodeValue(at: 0).result.requiredMember("id"), b = try graph.nodeValue(at: 1).result.requiredMember("id") + try check(a != b && graph.nodeCount == 2, id) + try check(try graph.breadthFirst(fromNode: 0).result == [0,1], id) + case 7: + try graphError(.duplicateDirectedEdge(first: [.key("edges"),.index(0)], second: [.key("edges"),.index(1)])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"A"},{"from":"A","to":"A"}]}"#) + } + let reverse = try build(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#).result + try check(reverse.edgeCount == 2, id) + case 8: + try graphError(.danglingEndpoint(at: [.key("edges"),.index(0),.key("to")])) { + _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":"missing"}]}"#) + } + case 9: + let path: [JSONLocationComponent] = [.key("nodes"),.index(0),.key("id")] + for (row,error) in [("{}", JSONGraphFailure.idMissing(at: path)), (#"{"id":null}"#, .idNull(at: path)), (#"{"id":1}"#, .idWrongKind(at: path, kind: .number)), ("false", .rowNotObject(at: [.key("nodes"),.index(0)], kind: .boolean))] { + try graphError(error) { _ = try build("{\"nodes\":[" + row + "],\"edges\":[]}") } + } + try graphError(.idNull(at: [.key("edges"),.index(0),.key("to")])) { _ = try build(#"{"nodes":[{"id":"A"}],"edges":[{"from":"A","to":null}]}"#) } + case 10: + let graph = try build().result + try graphError(.nodeOutOfBounds(-1)) { _ = try graph.nodeValue(at: -1) } + try graphError(.edgeOutOfBounds(3)) { _ = try graph.edgeValue(at: 3) } + try graphError(.frontierLimitExceeded(limit: 0)) { _ = try graph.breadthFirst(fromNode: 0, maximumFrontier: 0) } + try graphError(.resultLimitExceeded(limit: 0)) { _ = try graph.outgoingEdges(of: 0, maximumResults: 0) } + let empty = try JSONGraphProjection.build(document: doc(#"{"nodes":[],"edges":[]}"#), profile: profile, maximumNodes: 0, maximumEdges: 0).result + try check(empty.nodeCount == 0 && empty.edgeCount == 0, id) + do { _ = try graph.breadthFirst(fromNode: 0, maximumResults: -1); throw CheckFailure.failed(id) } + catch let failure as JSONFailure { try check(failure.code == .invalidLimits, id) } + case 11: + let graph = try build().result, baseline = try graph.breadthFirst(fromNode: 0) + var limits = JSONLimits.balanced; limits[.workUnits] = baseline.statistics[.workUnits] + try check(try graph.breadthFirst(fromNode: 0, limits: limits).statistics[.workUnits] == baseline.statistics[.workUnits], id) + limits[.workUnits] -= 1 + try resource(.workUnits) { _ = try graph.breadthFirst(fromNode: 0, limits: limits) } + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 12: + let result = try build(), graph = result.result, row = try graph.nodeValue(at: 0) + try check(graph.document.sourceBytes == doc().sourceBytes && result.statistics[.retainedBytes] > 0 && row.statistics[.retainedBytes] > 0, id) + let number = try row.result.requiredMember("n").numberValue + try check(try number?.integer(as: UInt64.self) == 9_007_199_254_740_993, id) + try check(try graph.edgeValue(at: 0).result == JSONPointer(parsing: "/edges/0").resolve(in: graph.document.root), id) + case 13: + let graph = try build().result + let task = Task { + withUnsafeCurrentTask { $0?.cancel() } + do { _ = try graph.breadthFirst(fromNode: 0); throw CheckFailure.failed(id) } catch is CancellationError {} + do { _ = try build(); throw CheckFailure.failed(id) } catch is CancellationError {} + } + try await task.value + try check(try graph.breadthFirst(fromNode: 0).result == [0,2,1], id) + case 14: + let graph = try build().result + try await withThrowingTaskGroup(of: [Int].self) { group in + for _ in 0..<8 { group.addTask { + try graphError(.resultLimitExceeded(limit: 1)) { _ = try graph.breadthFirst(fromNode: 0, maximumResults: 1) } + return try graph.breadthFirst(fromNode: 0).result + } } + for try await result in group { try check(result == [0,2,1], id) } + } + case 15: + let original = try doc(), graph = try JSONGraphProjection.build(document: original, profile: profile).result + let replacement = original.replacingRoot(try JSON.parse(#"{"nodes":[{"id":"new"}],"edges":[]}"#)) + let next = try JSONGraphProjection.build(document: replacement, profile: profile).result + try check(try graph.nodeSpan(at: 0).result != nil && next.nodeSpan(at: 0).result == nil, id) + try check(graph.nodeCount == 4 && next.nodeCount == 1 && graph.document.root == original.root, id) + case 16: + // The selected graph is tiny, but an unselected subtree still undergoes core admission. + let invalid = JSONDocument(root: .object(try JSONObject(members: [JSONMember("nodes", value: .array([])),JSONMember("edges", value: .array([])),JSONMember("unselected", value: .string(String(repeating: "x", count: 32)))]))) + var limits = JSONLimits.balanced; limits[.stringBytes] = 16 + try resource(.stringBytes) { _ = try JSONGraphProjection.build(document: invalid, profile: profile, limits: limits) } + default: throw CheckFailure.failed(id) + } + } +} + +@main struct GraphPublicConsumer { + static func main() async throws { + for id in 1...16 { + try await GraphPublicCases.run(id) + print("GRAPH_PUBLIC_CHECK GP\(String(format: "%02d", id)) passed") + } + let report = try GraphPublicCases.build() + print("GRAPH_PUBLIC_RESOURCE work=\(report.statistics[.workUnits]) retained=\(report.statistics[.retainedBytes]) peak=\(report.statistics.peakLogicalRetainedBytes) nodes=\(report.statistics[.nodes])") + print("GRAPH_PUBLIC_CONSUMER passed cases=16") + } +} From 68cbdf6183f05a01f283869b180d254aca190e30 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:49:47 +0200 Subject: [PATCH 3/3] Keep edge record members explicitly package-scoped --- Sources/JSONCLIEngine/CLIBuildIdentity.swift | 2 +- Sources/JSONInspection/Graph/JSONGraphProfile.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/JSONCLIEngine/CLIBuildIdentity.swift b/Sources/JSONCLIEngine/CLIBuildIdentity.swift index c5e3271..13960f4 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:480e22d7a4e7c8011e9d1197af921600fc5bc858a700b297aa86f87b2f90730a" + package static let revision = "sha256:3fc7a735250ad2de052c8a791835e10fd783bb930604dce54a754c5e10e5f0ff" package static let kind = "source-sha256" } diff --git a/Sources/JSONInspection/Graph/JSONGraphProfile.swift b/Sources/JSONInspection/Graph/JSONGraphProfile.swift index 4e197db..efcd0cb 100644 --- a/Sources/JSONInspection/Graph/JSONGraphProfile.swift +++ b/Sources/JSONInspection/Graph/JSONGraphProfile.swift @@ -64,7 +64,7 @@ public enum JSONGraphFailure: Error, Sendable, Equatable { /// Trusted row ordinals in this captured source, not durable graph identifiers. package struct JSONGraphEdgeRecord: Sendable, Equatable { - public let row: Int - public let source: Int - public let target: Int + package let row: Int + package let source: Int + package let target: Int }