diff --git a/AGENTS.md b/AGENTS.md index 2aebea7..4bfd841 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,4 +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. -P2 graph/store/set/matrix/retrieval/persistence and JSON text-sequence framing remain deferred. +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 4a191c4..2336449 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": [ @@ -312,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 new file mode 100644 index 0000000..8055869 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/APICandidate.md @@ -0,0 +1,48 @@ +# Graph API proposal for later owner review + +All NAV11 types and operations are **package-only**, not application API. Product +count remains ten, dependencies remain empty, Navigation56/Inspection95 unchanged. +The proposed future public subset, contingent on separate review, is: + +- Immutable `JSONGraphProfile(nodesAt:edgesAt:nodeIDKey:edgeSourceKey:edgeTargetKey:)`. +- `JSONGraphProjection.build(document:profile:limits:maximumNodes:maximumEdges:)` + returning `JSONOperationReport`, defaults 10000/50000. +- Read-only captured document/profile/nodeCount/edgeCount and complete traversal + summary. IDs/CSR buffers are research metadata; promoting their raw arrays is + not proposed. Source-local node/edge ordinals have no cross-view identity. +- `nodeValue(at:limits:)`, `edgeValue(at:limits:)`, `nodeSpan(at:limits:)` and + `outgoingEdges(of:limits:maximumResults:)` with independent finite whole-source + admissions. Returned JSON aliases receive additional owned-value accounting. +- `breadthFirst(fromNode:limits:maximumFrontier:maximumVisited:maximumResults:)` + returning original node ordinals; defaults 10000 each, start included, complete + reachable component. Zero caps refuse a real start; an empty graph has no start. +- Typed semantic errors with original row/field paths; core missing/kind/resource + and cancellation errors stay intact. No partial-success variant exists. + +BFS stores queue history as returned output; frontier means pending entries, while +visited/result caps independently bound total enqueues/output. Graph metadata +aliases, visited flags and queue entries share the query's original traversal +ledger. Scalar-exact linear resolution and O(N²+NE+E²) construction are explicit +costs, not optimized lookup claims. No new JSON identity/path or serialization. + +Actual compiled spelling is in Sources/JSONInspection/Graph. Constructors remain +fileprivate. Compiler probes reject external type access, private initializers, +mutation, internal reducers, and context escape. No public DocC examples imply +these private declarations are available. No merge or promotion is authorized. + +## Public successor review + +The owner approved the compiler-derived 34-declaration public subset for the draft +candidate, subject to qualification. The exact source spelling is authoritative: +public JSONGraphProfile (immutable explicit paths/keys and initializer), +JSONGraphProjection (checked factory, immutable document/profile/traversal/counts +and five independent query operations), and JSONGraphFailure with fourteen typed cases. +No existing public declaration changes. CSR buffers, ID staging, edge records, +reducers and the noncopyable context remain inaccessible externally. The old +public-type-invisibility negatives are replaced by internal builder/query/stage +negatives; initializer, storage, mutation and ownership rejections remain. + +Sixteen public-only scenarios and a distinct-package consumer supplement all +G01–G32 and 27 independently pinned fixtures. Three executable DocC examples and +26 external full-codegen rejections are mandatory. This is signature approval +for a draft, not merge/release approval or a claim of unexecuted platform passes. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md b/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md new file mode 100644 index 0000000..16377f1 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/GraphADR.md @@ -0,0 +1,57 @@ +# NAV11 directed simple graph experiment + +Package-only JSONInspection research, based on merged Navigation/Inspection; +no NAV10, new product, public symbol or graph JSON model. One original +`JSONTraversal.inspectOwned` invocation selects `[nodesAt, edgesAt]` with +`.rejectAmbiguous` and depth one. All strict root selections and overlap checks +precede callbacks; nodes are validated before edges, in their physical order. + +Negative capacities are configuration failures before admission. Existing core +precedence is retained: complete source admission precedes strict selection, +overlap and root-kind checks. No separate unmetered profile scan or second ledger +is added to move those checks ahead of admission. Missing roots preserve core +`missingMember`/`missingTarget`; wrong root kinds preserve typed core access errors. +Paths and explicit keys are immutable. Reusing a field spelling across roles is +intentional and valid (an edge's single field may explicitly name both ends of a +self-edge); no field inference or silent repair occurs. + +The exclusive context adds mutating delegates only: direct string member selection, +scalar-exact string comparison and matching alias release. JSONObject.lookup and +JSONComparison.textEquals remain authorities. Missing, null and wrong kind are +reported separately; strings own 32+UTF8 logical bytes until released or handed off. +Profile keys are explicitly key-admitted on that meter even for empty arrays; +the preexisting retainKey delegate gains an opt-in caller-input admission flag, +with its original default behavior/counters preserved. +No cursor, scope, ledger or noncopyable capability leaves the reducer. Original +2-positive/20-negative codegen tests are unchanged; helper escape probes and runtime +parity run separately in Debug and Release. A compiler crash blocks implementation. + +Graph metadata is source-local: first row ordinals, exact ID aliases, endpoint +ordinals and CSR offsets/edge ordinals in original edge order. Original source is +transferred once after complete validation/finalization. Defaults are 10,000 nodes, +50,000 edges; each independently configurable finite cap is inclusive and still +subject to all 49 core metrics. Linear exact scans may cost O(N²+NE+E²). + +Later value/span/catalogue/BFS calls start explicit independent full-source +operations. BFS has a head-index queue, visited-at-enqueue and source-edge order. +Defaults: 10,000 frontier entries, visited nodes and returned nodes. Queue history +remains charged until handoff; frontier capacity means live pending entries. +Results certify the complete reachable component including the start, not global +reachability. Failure/cancellation returns no partial result. + +Logical policy V1: 256 fixed builder bytes, 32+UTF8 per node ID alias, +16 per node record, 24 per edge record, 8 per offset and adjacency reference, +plus exact profile keys/paths and the original source. Queries admit graph metadata +aliases, queue/output slots (8 each), visited flags (1 each) and their source; +returned JSON aliases additionally receive core owned-retention accounting. +These are logical quotas, not allocator/RSS or caller-instruction guarantees. + +No public promotion, merge, release, timing or speed claim. Controlled performance +remains HOLD. Store, persistence, graph editing, weights, algorithms other than BFS, +query/CLI/HTML/network adapters and NAV10 batching remain separate work. + +BFS entry precedence is negative capacities, node bounds, whole source/metadata +admission, then enqueue's frontier/visited/result ceilings. Work/cancellation that +is observed first takes precedence. Edge rows validate source field then target +field before resolving source then target IDs, then compare ordered pairs. All +node rows precede all edge rows; a late duplicate node outranks a dangling edge. diff --git a/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md b/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md new file mode 100644 index 0000000..62504b6 --- /dev/null +++ b/Documentation/Architecture/StructuralIntelligence/NAV11/SourceAuthority.md @@ -0,0 +1,22 @@ +# NAV11 source authority + +Base main: 1a0306d076a08c8c43281f91412a11cd744ff3f6. NAV10 PR66/d566f28 is +independent and not a source dependency. Stable 1.6.1/e431f15 remains immutable. + +| Graph work | Existing delegate | Invocation boundary | +| --- | --- | --- | +| Complete root/source/map admission and failures | JSONReadScope, JSONAdmission, JSONWorkMeter | Original inspectOwned scope | +| Physical/virtual roots and exact overlap | JSONTraversal.execute, rejectAmbiguous | Same original scope, before callbacks | +| Row locations and bounded paths | JSONTraversalHit, JSONLocationComponent, context.appending | Same scope | +| Direct string member/presence/kind | JSONObject.lookup through mutating context delegate | No reader/new ledger | +| ID equality | JSONComparison.textEquals through context.equalStrings | Scalar-exact charged UTF8 comparison | +| Owned alias accounting, cancellation, terminal health | context.perform + scope.run/check | Original state, no escape | +| Adjacency and BFS | Package-only graph reducers | Checked work/retention in that invocation | +| Completed source handoff | context.transferDocumentOwnership | Once after complete valid construction | +| Later source value/span | JSONReadScope.select and sourceMap.span | Explicit new whole-source operation | +| Returned value alias | JSONAdmission.value(owned:true) | Same later-read ledger, additional output cost | + +No JSON, Pointer, value equality, schema, stream, Patch, HTML or CLI semantic change. +The new context methods are package-only and mutating; original ownership codegen +fixtures are byte-identical. ExtractionManifest records original Apache-2.0 code +and independent fixture provenance. Public symbol graphs must remain unchanged. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 2c1de90..66f544e 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, @@ -295,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 new file mode 100644 index 0000000..2caae38 --- /dev/null +++ b/Documentation/Qualification/GraphCases.json @@ -0,0 +1,80 @@ +{ + "schema_version": 1, + "status": "public API candidate; owner review and full current-source qualification required", + "functions": [ + "JSONInspectionTests.graphG01EmptySourceAndNoValidStart()", + "JSONInspectionTests.graphG02IndependentOracleAndSourceOrder()", + "JSONInspectionTests.graphG03ScalarDistinctIDs()", + "JSONInspectionTests.graphG04SpecialIDsAreNotPaths()", + "JSONInspectionTests.graphG05DuplicateNodeLocations()", + "JSONInspectionTests.graphG06EqualPayloadsPreserveRows()", + "JSONInspectionTests.graphG07MissingNodeID()", + "JSONInspectionTests.graphG08NullNodeID()", + "JSONInspectionTests.graphG09WrongNodeIDKinds()", + "JSONInspectionTests.graphG10NonobjectNode()", + "JSONInspectionTests.graphG11MissingEdgeSource()", + "JSONInspectionTests.graphG12NullEdgeTarget()", + "JSONInspectionTests.graphG13WrongEndpointKind()", + "JSONInspectionTests.graphG14NonobjectEdge()", + "JSONInspectionTests.graphG15DanglingAndEmpty()", + "JSONInspectionTests.graphG16DuplicateOrderedEdge()", + "JSONInspectionTests.graphG17ReverseEdgesAreDistinct()", + "JSONInspectionTests.graphG18SelfEdgeIsVisitedOnce()", + "JSONInspectionTests.graphG19CycleTerminates()", + "JSONInspectionTests.graphG20IsolatesRemainSourceNodes()", + "JSONInspectionTests.graphG21SourceEdgeNeighborOrder()", + "JSONInspectionTests.graphG22DiamondEnqueuesOnce()", + "JSONInspectionTests.graphG23TypedPathsAndPointerDifferential()", + "JSONInspectionTests.graphG24OverlapAndDuplicateRootsFailBeforeRows()", + "JSONInspectionTests.graphG25WholeSourceAdmissionAndFreshQueries()", + "JSONInspectionTests.graphG26InclusiveWorkAndRetention()", + "JSONInspectionTests.graphG27EveryCapacityAndCheckedOverflow()", + "JSONInspectionTests.graphG28CancellationStopsBuildAndBFS()", + "JSONInspectionTests.graphG29FailurePrecedenceAndTerminalLatch()", + "JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()", + "JSONInspectionTests.graphG31CapturedRootAndSourceMapCoherence()", + "JSONInspectionTests.graphG32CheckedMetadataAndExistingPublicSemantics()" + ], + "fixture_cases": 27, + "context_positive_codegen": 2, + "context_negative_codegen": 8, + "graph_positive_codegen": 2, + "graph_negative_codegen": 18, + "public_functions": [ + "JSONInspectionTests.graphGP01OptInSurface()", + "JSONInspectionTests.graphGP02TypedRootAndEmptyKeys()", + "JSONInspectionTests.graphGP03SourceOrderBFS()", + "JSONInspectionTests.graphGP04SelfLoopAndCycle()", + "JSONInspectionTests.graphGP05DuplicateIDPaths()", + "JSONInspectionTests.graphGP06ScalarDistinctIDs()", + "JSONInspectionTests.graphGP07OrderedPairs()", + "JSONInspectionTests.graphGP08DanglingEndpoint()", + "JSONInspectionTests.graphGP09MissingNullWrongKind()", + "JSONInspectionTests.graphGP10BoundsAndZeroCaps()", + "JSONInspectionTests.graphGP11IndependentWholeSourceBudget()", + "JSONInspectionTests.graphGP12OwnedValueAndSourceHandoff()", + "JSONInspectionTests.graphGP13CancellationAndHealth()", + "JSONInspectionTests.graphGP14ConcurrentQueries()", + "JSONInspectionTests.graphGP15SourceMapCoherence()", + "JSONInspectionTests.graphGP16UnselectedAdmission()" + ], + "public_consumer_checks": [ + "GP01", + "GP02", + "GP03", + "GP04", + "GP05", + "GP06", + "GP07", + "GP08", + "GP09", + "GP10", + "GP11", + "GP12", + "GP13", + "GP14", + "GP15", + "GP16" + ], + "public_negative_codegen": 26 +} diff --git a/Documentation/Qualification/TestDiscovery.json b/Documentation/Qualification/TestDiscovery.json index 6f5fc8a..1575ed4 100644 --- a/Documentation/Qualification/TestDiscovery.json +++ b/Documentation/Qualification/TestDiscovery.json @@ -12,7 +12,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 124 }, "parameterized_invocations": 325, "composition_external_functions": 5, @@ -35,7 +35,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 6 }, "coverage_unmapped": { "Sources/JSON/Pointer/JSONLocationComponent.swift": "declaration-only enum", @@ -76,7 +76,7 @@ "JSONCLITests": 88, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 124 }, "wasi_root_functions": { "JSONTests": 190, @@ -89,7 +89,7 @@ "JSONBridgesTests": 10, "JSONNavigationTests": 47, "JSONSchemaExamplesTests": 98, - "JSONInspectionTests": 76 + "JSONInspectionTests": 124 }, "wasi_parameterized_invocations": 288, "portable_shared_state_functions": { @@ -103,7 +103,7 @@ "JSONCLITests": 1, "JSONNavigationTests": 1, "JSONSchemaExamplesTests": 4, - "JSONInspectionTests": 4 + "JSONInspectionTests": 6 }, "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..7a8aff6 100644 --- a/README.md +++ b/README.md @@ -716,3 +716,28 @@ 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. + +## 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 096ea07..43cb0d5 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, 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 @@ -16,7 +17,7 @@ SHARED_TESTS += '|navigationSharedImmutableRootIndependentInvocations' SHARED_TESTS += '|inspectionI17CoverageSpansReplacementAndSharedInvocations' SHARED_TESTS += '|snapshotS19NavigationParityAndIndependentInvocations|snapshotV22ConcurrentLookupsUseIndependentAdmissionAndHealth' -SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics' +SHARED_TESTS += '|projectionP16SharedViewsHaveIndependentHealthAndLegacyStatistics|graphG30ConcurrentViewsHaveIndependentHealth|graphGP14ConcurrentQueries' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -27,12 +28,15 @@ def commands(lane, output): output = output.relative_to(ROOT) if lane == 'inspection': result = [['python3', '-B', 'Scripts/public_check.py'], - ['swift', 'test', '--filter', 'inspection|snapshot|projectionP'], - ['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_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] @@ -174,6 +178,9 @@ 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')], + ['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'], @@ -232,20 +239,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 +311,12 @@ def check_result(command, code, log, events=None): passed = passed and 'TESTING_EVENT_RECEIPTS passed cases=19' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_ergonomics.py']: passed = passed and 'ERGONOMICS_COMPILER passed commands=8 guide_fences=3' in log.splitlines() + if command[:3] == ['python3', '-B', 'Scripts/qualify_public_graph.py']: + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_public_graph_receipt(data) + if command[:3] in (['python3', '-B', 'Scripts/qualify_graph_context.py'], ['python3', '-B', 'Scripts/qualify_graph.py']): + data = json.loads((ROOT / command[command.index('--output') + 1] / 'receipt.json').read_text()) + passed = passed and check_graph_receipt(data, context='qualify_graph_context.py' in command[2]) if command[:3] == ['python3', '-B', 'Scripts/qualify_navigation_reducer.py']: passed = passed and 'REDUCER_CODEGEN passed positive=2 negative=20' in log.splitlines() if command[:3] == ['python3', '-B', 'Scripts/qualify_snapshot.py']: @@ -328,14 +341,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=19' 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 +368,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..42f721f --- /dev/null +++ b/Scripts/graph_acceptance.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Exact graph identities and genuine compiler outcomes; no broad exit-zero gate.""" +import json,pathlib +ROOT=pathlib.Path(__file__).resolve().parents[1] +POLICY=json.loads((ROOT/'Documentation/Qualification/GraphCases.json').read_text()) +EXPECTED=frozenset(POLICY['functions']) | frozenset(POLICY['public_functions']) +SHARED=frozenset({'JSONInspectionTests.graphG30ConcurrentViewsHaveIndependentHealth()', 'JSONInspectionTests.graphGP14ConcurrentQueries()'}) +def check_graph_cases(events,shared=False): + names=[n for n in events['function_names'] if n.startswith('JSONInspectionTests.graphG')] + return len(names)==len(set(names)) and set(names)==(SHARED if shared else EXPECTED) +def check_graph_receipt(data,context=False): + commands=data.get('commands',[]);expected_count=18 if context else 28;negative_count=8 if context else 18 + if data.get('status')!='passed' or len(commands)!=expected_count or len({c.get('name') for c in commands})!=expected_count:return False + negatives=[c for c in commands if c.get('expected_diagnostic')] + if len(negatives)!=negative_count:return False + for c in commands: + if c.get('status')!='passed' or c.get('compiler_crash') is not False:return False + if c.get('expected_diagnostic'): + if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'].lower() in d.lower() for d in c.get('primary_diagnostics',[])):return False + elif c.get('exit_code')!=0:return False + return True + +PUBLIC_CHECKS = frozenset(POLICY['public_consumer_checks']) +def check_public_graph_receipt(data): + commands=data.get('commands',[]);results=data.get('consumer_results',{}) + if data.get('status')!='passed' or len(commands)!=45 or len({c.get('name') for c in commands})!=45 or set(results)!={'debug','release','address'}:return False + negatives=[c for c in commands if c.get('expected_diagnostic')] + if len(negatives)!=26:return False + for c in commands: + if c.get('status')!='passed' or c.get('compiler_crash') is not False:return False + if c.get('expected_diagnostic'): + if c.get('exit_code',0)<=0 or not any(c['expected_diagnostic'] in d for d in c.get('primary_diagnostics',[])):return False + elif c.get('exit_code')!=0:return False + for row in results.values(): + names=row.get('cases',[]) + if len(names)!=16 or len(set(names))!=16 or set(names)!=PUBLIC_CHECKS:return False + if not all(isinstance(row.get(k),int) and row[k]>0 for k in ['work_units','logical_retained_bytes','peak_logical_retained_bytes','admitted_nodes']):return False + return True diff --git a/Scripts/qualify_graph.py b/Scripts/qualify_graph.py new file mode 100644 index 0000000..f111fa5 --- /dev/null +++ b/Scripts/qualify_graph.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Full-codegen proof of graph invariants and non-public storage/reducer boundaries.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT = pathlib.Path(__file__).resolve().parents[1] +POSITIVE = r'''import JSON +import JSONNavigation +import JSONInspection +@main struct GraphConsumer { + static func check(_ value: Bool) { precondition(value) } + static func main() throws { + let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") + let document = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"é","n":9007199254740993},{"id":"e\u0301"}],"edges":[{"from":"é","to":"e\u0301"}]}"#)) + let built = try JSONGraphProjection.build(document: document, profile: profile) + check(built.result.nodeCount == 2 && built.result.edgeCount == 1) + print("PRIVATE_GRAPH exact passed") + check(try built.result.breadthFirst(fromNode: 0).result == [0,1]) + print("PRIVATE_GRAPH bfs passed") + check(try built.result.outgoingEdges(of: 0).result == [0]) + print("PRIVATE_GRAPH adjacency passed") + check(try built.result.nodeValue(at: 0).result.objectValue?["n"]?.numberValue?.integer(as: UInt64.self) == 9007199254740993) + print("PRIVATE_GRAPH values passed") + let baseline = try JSONTraversal(document: document).visit { _,_ in .continue } + check(built.statistics[.nodes] == baseline.statistics[.nodes] && built.statistics[.retainedBytes] > 0) + print("PRIVATE_GRAPH admission passed") + } +} +''' +PROBES = [ + ('positive', POSITIVE, None, False), + ('external-builder', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find', True), + ('external-query', 'import JSONInspection\nlet x = GraphQuery.self\n', 'cannot find', True), + ('external-stage', 'import JSONInspection\nlet x = GraphWorkStage.self\n', 'cannot find', True), + ('external-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find', True), + ('external-context', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find', True), + ('core-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find', True), + ('immutable-ids', 'import JSONInspection\nfunc f(_ x: inout JSONGraphProjection) { x.nodeIDs.removeAll() }\n', "'nodeIDs' is a 'let' constant", False), + ('forged-graph', 'import JSON\nimport JSONNavigation\nimport JSONInspection\nfunc f(_ d: JSONDocument, _ p: JSONGraphProfile, _ s: JSONTraversalSummary) { _ = JSONGraphProjection(document: d, profile: p, nodeIDs: [], edges: [], offsets: [], adjacency: [], traversal: s) }\n', 'initializer is inaccessible', False), + ('private-reducer', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find', False), +] +CHECKS = {'exact', 'bfs', 'adjacency', 'values', 'admission'} + +def main(): + parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--output', required=True) + out = ROOT / parser.parse_args().output; out.mkdir(parents=True, exist_ok=True) + receipt = dict(status='running', scope='same-package graph proof; graph public surface plus private storage/reducers; same-package invariant proof', + source_inputs=[dict(path=p.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(p.read_bytes()).hexdigest()) for p in files()], commands=[]) + def save(): (out / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr; diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + crash = result.returncode < 0 or any(s in text for s in ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation', 'Found ownership error']) + passed = not crash and (result.returncode == 0 if expected is None else result.returncode > 0 and any(expected.lower() in d.lower() for d in diagnostics)) + log = out / (name + '.log'); log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], exit_code=result.returncode, + status='passed' if passed else 'failed', compiler_crash=crash, expected_diagnostic=expected, + primary_diagnostics=diagnostics, log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: raise ValueError('Unexpected graph compiler outcome: ' + name) + return result.stdout + try: + identity = re.sub(r'[^a-zA-Z0-9_]', '_', ROOT.name.lower()) + if not identity or identity[0].isdigit(): identity = '_' + identity + for config in ['debug', 'release']: + run(config + '-build', ['swift','build','-c',config,'--target','JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift','build','-c',config,'--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + common = ['swiftc','-swift-version','6','-warnings-as-errors','-I',str(modules),'-Onone' if config == 'debug' else '-O','-parse-as-library'] + for name, text, diagnostic, external in PROBES: + source = out / (name + '.swift'); source.write_text(text) + run(config + '-' + name, common + ['-package-name','graph_external' if external else identity, + '-module-name','GraphCompilerProbe','-c',str(source),'-o',str(out / (config + '-' + name + '.o'))], diagnostic) + objects = [] + for module in ['JSON','JSONNavigation','JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: raise ValueError('Missing objects: ' + module) + objects.extend(str(p) for p in selected) + binary = out / ('private-' + config) + run(config + '-link', common + ['-package-name',identity,str(out/'positive.swift'),*objects,'-o',str(binary)]) + output = run(config + '-run', [str(binary)]) + checks = re.findall(r'^PRIVATE_GRAPH (\S+) passed$', output, re.M) + if len(checks) != 5 or set(checks) != CHECKS: raise ValueError('Private projection consumer identity/count drift') + receipt['status'] = 'passed' + except (ValueError, OSError, subprocess.TimeoutExpired) as error: receipt.update(status='failed', error=str(error)) + save(); print('GRAPH_COMPILER ' + receipt['status'] + ' commands=28 positive_codegen=2 negative_codegen=18 private_cases=5/config') + return 0 if receipt['status'] == 'passed' else 1 +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/qualify_graph_context.py b/Scripts/qualify_graph_context.py new file mode 100644 index 0000000..13619df --- /dev/null +++ b/Scripts/qualify_graph_context.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Code-generation and original-meter proof for the graph's exclusive delegates.""" +import argparse, hashlib, json, pathlib, re, subprocess +from archive_candidate import files +ROOT=pathlib.Path(__file__).resolve().parents[1] +POSITIVE='''import JSON +import JSONNavigation +struct Probe: JSONTraversalOwnedReducer { + var strings: [String] = [] + mutating func observe(_ hit: JSONTraversalHit, value: JSON, context: inout JSONTraversalContext) throws -> JSONTraversalAction { + if hit.relativeDepth == 0 { try context.retainKey("id", admittingCallerInput: true); try context.release(2); _ = try context.arrayCount(value, at: hit.absoluteLocation); return .continue } + let field = try context.directStringMember(value, key: "id", at: hit.absoluteLocation) + precondition(field.kind == .string) + if let text = field.text { let equal = try context.equalStrings(text, text); precondition(equal); strings.append(text) } + return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> Int { + for text in strings { try context.releaseStringAlias(text) } + try context.work(7); return strings.count + } +} +@main struct Main { + static func main() throws { + let document = JSONDocument(root: try JSON.parse(#"{"n":[{"id":"é"}],"e":[{"id":"e\\u0301"}]}"#)) + let walk = JSONTraversal(document: document) + let roots: JSONTraversalScope = .virtual(roots: [[.key("n")],[.key("e")]], overlap: .rejectAmbiguous) + let base = try walk.visit(scope: roots, options: .init(depthLimit: 1)) { _,_ in .continue } + var reducer = Probe() + let owned = try walk.inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &reducer) + precondition(owned.result == 2) + precondition(owned.statistics[.nodes] == base.statistics[.nodes]) + precondition(owned.statistics[.workUnits] > base.statistics[.workUnits]) + precondition(owned.statistics[.retainedBytes] == base.statistics[.retainedBytes]) + precondition(reducer.strings.count == 2) + var exact = Probe(); var limits = JSONLimits.balanced + limits[.workUnits] = owned.statistics[.workUnits] + _ = try JSONTraversal(document: document, limits: limits).inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &exact) + limits[.workUnits] -= 1 + do { var short = Probe(); _ = try JSONTraversal(document: document, limits: limits).inspectOwned(scope: roots, options: .init(depthLimit: 1), reducer: &short); preconditionFailure() } + catch let error as JSONFailure { precondition(error.category == .resource) } + print("GRAPH_CONTEXT parity passed nodes=\\(owned.statistics[.nodes]) work=\\(owned.statistics[.workUnits]) retained=\\(owned.statistics[.retainedBytes])") + } +} +''' +PROBES={ + 'positive':(POSITIVE,None), + 'bound':('func f(_ c: inout JSONTraversalContext) { _ = c.directStringMember }', "cannot reference 'mutating' method as function value"), + 'escape':('func f(_ c: inout JSONTraversalContext) -> () throws -> Bool { { try c.equalStrings("a", "a") } }', "escaping closure captures 'inout' parameter"), + 'task':('func f(_ c: inout JSONTraversalContext) { _ = Task { try c.releaseStringAlias("a") } }', "escaping closure captures 'inout' parameter"), + 'store':('final class Box { var c: JSONTraversalContext? }; func f(_ c: inout JSONTraversalContext) -> Box { let b = Box(); b.c = consume c; return b }', 'missing reinitialization of inout parameter'), +} +def main(): + ap=argparse.ArgumentParser();ap.add_argument('--output',required=True);out=ROOT/ap.parse_args().output;out.mkdir(parents=True,exist_ok=True) + receipt={'status':'running','source_inputs':[{'path':p.relative_to(ROOT).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()],'commands':[]} + def save(): (out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') + def run(name,args,expected=None): + p=subprocess.run(args,cwd=ROOT,capture_output=True,text=True,timeout=900);data=p.stdout+p.stderr + diagnostics=re.findall(r': error: ([^\n]+)',data);crash=p.returncode<0 or any(x in data for x in ['Stack dump:','Assertion failed','Found ownership error','fatal error encountered during compilation']) + passed=not crash and (p.returncode==0 if expected is None else p.returncode>0 and any(expected.lower() in d.lower() for d in diagnostics)) + log=out/(name+'.log');log.write_text(data) + receipt['commands'].append({'name':name,'command':args,'exit_code':p.returncode,'status':'passed' if passed else 'failed','expected_diagnostic':expected,'primary_diagnostics':diagnostics,'compiler_crash':crash,'log':log.relative_to(ROOT).as_posix(),'log_sha256':hashlib.sha256(log.read_bytes()).hexdigest()});save() + if not passed: raise ValueError(name) + return p.stdout + try: + identity=re.sub('[^a-zA-Z0-9_]','_',ROOT.name.lower()) + if identity[0].isdigit():identity='_'+identity + for config in ['debug','release']: + run(config+'-build',['swift','build','-c',config,'--target','JSONNavigation']) + bp=pathlib.Path(run(config+'-binpath',['swift','build','-c',config,'--show-bin-path']).strip());mods=bp/'Modules' if (bp/'Modules').is_dir() else bp + common=['swiftc','-swift-version','6','-warnings-as-errors','-package-name',identity,'-I',str(mods),'-O' if config=='release' else '-Onone','-parse-as-library'] + for name,(body,expected) in PROBES.items(): + f=out/(name+'.swift');f.write_text(body if name=='positive' else 'import JSON\nimport JSONNavigation\n'+body+'\n') + run(config+'-'+name,common+['-c',str(f),'-o',str(out/(config+'-'+name+'.o'))],expected) + objects=[] + for module in ['JSON','JSONNavigation']: + aggregate=bp/(module+'.o');objects+= [str(aggregate)] if aggregate.exists() else [str(p) for p in sorted((bp/(module+'.build')).glob('*.o'))] + exe=out/('parity-'+config);run(config+'-link',common+[str(out/'positive.swift'),*objects,'-o',str(exe)]) + output=run(config+'-run',[str(exe)]) + if len(re.findall(r'^GRAPH_CONTEXT parity passed nodes=\d+ work=\d+ retained=\d+$',output,re.M))!=1:raise ValueError('Missing parity witness') + receipt['status']='passed' + except (ValueError,OSError,subprocess.TimeoutExpired) as e:receipt.update(status='failed',error=str(e)) + save();print('GRAPH_CONTEXT '+receipt['status']+' positive=2 negative=8 runtime=2');return 0 if receipt['status']=='passed' else 1 +if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/qualify_public_graph.py b/Scripts/qualify_public_graph.py new file mode 100644 index 0000000..f2abd0f --- /dev/null +++ b/Scripts/qualify_public_graph.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Qualify public graph imports, semantics and three executable DocC examples.""" +import argparse +import hashlib +import json +import pathlib +import re +import subprocess +import sys +from archive_candidate import files + +ROOT = pathlib.Path(__file__).resolve().parents[1] +CHECKS = {'GP%02d' % i for i in range(1,17)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', required=True) + output = ROOT / parser.parse_args().output + output.mkdir(parents=True, exist_ok=True) + shared = (ROOT / 'Tests/JSONInspectionTests/GraphPublicAcceptanceTests.swift').read_text().split('\n@Test')[0].replace('import Testing\n', '') + actual = (ROOT / 'Tools/GraphPublicQualification/Sources/GraphPublicConsumer/Main.swift').read_text() + if actual.split('\n@main struct')[0] != shared: + raise ValueError('Public consumer scenario bytes differ from the sixteen pinned root cases') + inputs = [{'path': p.relative_to(ROOT).as_posix(), + 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} for p in files()] + receipt = dict(status='running', source_inputs=inputs, commands=[], consumer_results={}) + + def save(): + (output / 'receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') + + def run(name, command, expected=None): + result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, timeout=900) + text = result.stdout + result.stderr + crash = result.returncode < 0 or any(x in text for x in + ['Stack dump:', 'Assertion failed', 'fatal error encountered during compilation']) + diagnostics = re.findall(r': error: ([^\n]+)', result.stderr) + passed = not crash and (result.returncode == 0 if expected is None else + result.returncode > 0 and any(expected in d for d in diagnostics)) + log = output / (name + '.log') + log.write_text(text.replace(str(ROOT), '.')) + receipt['commands'].append(dict(name=name, command=[x.replace(str(ROOT), '.') for x in command], + exit_code=result.returncode, status='passed' if passed else 'failed', compiler_crash=crash, + expected_diagnostic=expected, primary_diagnostics=diagnostics, + log=log.relative_to(ROOT).as_posix(), log_sha256=hashlib.sha256(log.read_bytes()).hexdigest())) + save() + if not passed: + raise ValueError('Unexpected compiler/consumer outcome: ' + name) + return result.stdout + + def consumer(name, command): + text = run(name, command) + checks = re.findall(r'^GRAPH_PUBLIC_CHECK (\S+) passed$', text, re.M) + if len(checks) != 16 or set(checks) != CHECKS or 'GRAPH_PUBLIC_CONSUMER passed cases=16' not in text: + raise ValueError('Missing/duplicate graph consumer checks: ' + name) + resource = re.search(r'^GRAPH_PUBLIC_RESOURCE work=(\d+) retained=(\d+) peak=(\d+) nodes=(\d+)$', text, re.M) + if resource is None or not all(int(x) > 0 for x in resource.groups()): + raise ValueError('Missing finite cumulative report: ' + name) + receipt['consumer_results'][name] = dict(cases=checks, work_units=int(resource[1]), + logical_retained_bytes=int(resource[2]), peak_logical_retained_bytes=int(resource[3]), + admitted_nodes=int(resource[4])) + save() + + try: + for config in ['debug', 'release']: + run(config + '-build', ['swift', 'build', '-c', config, '--target', 'JSONInspection']) + binpath = pathlib.Path(run(config + '-binpath', ['swift', 'build', '-c', config, '--show-bin-path']).strip()) + modules = binpath / 'Modules' if (binpath / 'Modules').is_dir() else binpath + objects = [] + for module in ['JSON', 'JSONNavigation', 'JSONInspection']: + aggregate = binpath / (module + '.o') + selected = [aggregate] if aggregate.is_file() else sorted((binpath / (module + '.build')).glob('*.o')) + if not selected: + raise ValueError('Missing built objects: ' + module) + objects.extend(str(p) for p in selected) + common = ['swiftc', '-swift-version', '6', '-warnings-as-errors', '-package-name', + 'graph_external_consumer', '-I', str(modules), + '-O' if config == 'release' else '-Onone'] + consumer(config, ['swift', 'run', '-c', config, '--package-path', + 'Tools/GraphPublicQualification', 'GraphPublicConsumer']) + guide = ROOT / 'Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md' + fences = re.findall(r'```swift\n(.*?)\n```', guide.read_text(), re.S) + if len(fences) != 3: + raise ValueError('Changed three-fence guide inventory') + for index, fence in enumerate(fences): + file = output / ('guide-' + str(index) + '.swift') + file.write_text('import Foundation\nimport JSON\nimport JSONNavigation\nimport JSONInspection\n' + '@main struct Guide { static func main() throws {\n' + fence + '\n} }\n') + binary = output / ('guide-' + config + '-' + str(index)) + run(config + '-guide-' + str(index), common + ['-parse-as-library', str(file), *objects, '-o', str(binary)]) + run(config + '-guide-' + str(index) + '-run', [str(binary)]) + probes = [('core-import-isolation', 'import JSON\nlet x = JSONGraphProjection.self\n', 'cannot find'), ('scope-isolation', 'import JSON\nlet x = JSONReadScope.self\n', 'cannot find'), ('capability-isolation', 'import JSONNavigation\nlet x = JSONTraversalContext.self\n', 'cannot find'), ('forged-graph', 'import JSON\nimport JSONNavigation\nimport JSONInspection\nfunc f(_ d: JSONDocument, _ p: JSONGraphProfile, _ s: JSONTraversalSummary) { _ = JSONGraphProjection(document: d, profile: p, nodeIDs: [], edges: [], offsets: [], adjacency: [], traversal: s) }\n', 'initializer is inaccessible'), ('immutable-profile', 'import JSONInspection\nfunc f(_ p: inout JSONGraphProfile) { p.nodesAt.removeAll() }\n', "'let' constant"), ('immutable-count', 'import JSONInspection\nfunc f(_ p: inout JSONGraphProjection) { p.nodeCount = 0 }\n', 'get-only property'), ('private-ids', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.nodeIDs }\n', 'is inaccessible'), ('private-edges', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.edges }\n', 'is inaccessible'), ('private-offsets', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.offsets }\n', 'is inaccessible'), ('private-adjacency', 'import JSONInspection\nfunc f(_ p: JSONGraphProjection) { _ = p.adjacency }\n', 'is inaccessible'), ('private-builder', 'import JSONInspection\nlet x = GraphBuilder.self\n', 'cannot find'), ('private-query', 'import JSONInspection\nlet x = GraphQuery.self\n', 'cannot find'), ('private-record', 'import JSONInspection\nlet x = JSONGraphEdgeRecord.self\n', 'cannot find')] + for name, source, expected in probes: + file = output / (name + '.swift'); file.write_text(source) + run(config + '-' + name, common + ['-parse-as-library', '-c', str(file), + '-o', str(output / (config + '-' + name + '.o'))], expected) + consumer('address', ['swift', 'run', '-c', 'release', '--sanitize', 'address', + '--scratch-path', '.build/graph-public-consumer-address', '--package-path', + 'Tools/GraphPublicQualification', 'GraphPublicConsumer']) + receipt['status'] = 'passed' + except (OSError, ValueError, subprocess.TimeoutExpired) as error: + receipt.update(status='failed', error=str(error)) + save() + print('PUBLIC_GRAPH_COMPILER ' + receipt['status'] + + ' configurations=2 consumer_cases=16 address_cases=16 guide_fences=3 negative_probes=26') + return 0 if receipt['status'] == 'passed' else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/Scripts/test_qualification_harness.py b/Scripts/test_qualification_harness.py index 843ec53..0a12338 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, 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): @@ -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', 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}, - 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'], 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'], 4) + self.assertEqual(policy[key]['JSONInspectionTests'], 6) self.assertEqual(policy[key]['JSONIntegrationTests'], 11) def test_private_projection_identities_cannot_be_missing_renamed_or_duplicated(self): @@ -95,6 +96,40 @@ 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),48) + self.assertTrue(check_graph_cases({'function_names':names})) + for broken in [names[:-1],names+names[:1],['WrongModule.'+names[0].split('.',1)[1]]+names[1:]]: + self.assertFalse(check_graph_cases({'function_names':broken})) + for context,count,negatives in [(True,18,8),(False,28,18)]: + commands=[dict(name=str(n),status='passed',compiler_crash=False,exit_code=1 if n; storage and reducers remain non-public. No release is implied. diff --git a/Sources/JSONInspection/Documentation.docc/JSONInspection.md b/Sources/JSONInspection/Documentation.docc/JSONInspection.md index 31eb714..6f0a53c 100644 --- a/Sources/JSONInspection/Documentation.docc/JSONInspection.md +++ b/Sources/JSONInspection/Documentation.docc/JSONInspection.md @@ -39,3 +39,14 @@ or numeric matrix. Sampling does not exempt any source from admission. - ``JSONTreeProjection`` - ``JSONSparseTableProjection`` - ``JSONProjectionFailure`` + +### Experimental scope + +- + +### Source-bound directed graphs + +- +- ``JSONGraphProfile`` +- ``JSONGraphProjection`` +- ``JSONGraphFailure`` diff --git a/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md b/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md new file mode 100644 index 0000000..47c7de7 --- /dev/null +++ b/Sources/JSONInspection/Documentation.docc/SourceBoundGraphs.md @@ -0,0 +1,62 @@ +# Build a source-bound directed graph + +An explicit profile names two disjoint physical arrays and their direct string fields. +Build validates every row and publishes only a complete directed simple graph. +ID and field-key identity uses exact decoded Unicode scalars. Arrays retain source +order. Duplicate IDs, dangling endpoints and repeated directed pairs throw; reverse +edges, self-edges, cycles and isolated nodes are valid data. + +## Capture the complete source + +```swift +let graphSource = try JSONParser().parseDocument(Data(#"{"nodes":[{"id":"A","n":9007199254740993},{"id":"B"}],"edges":[{"from":"A","to":"B"}]}"#.utf8)) +let graphProfile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +let graphBuild = try JSONGraphProjection.build(document: graphSource, profile: graphProfile) +precondition(graphBuild.result.nodeCount == 2 && graphBuild.result.edgeCount == 1) +precondition(graphBuild.statistics[.workUnits] > 0) +``` + +## Traverse a complete reachable component + +```swift +let graph = try JSONGraphProjection.build(document: .init(root: JSON.parse(#"{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"from":"A","to":"B"},{"from":"B","to":"A"}]}"#)), profile: .init(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to")).result +let outgoing = try graph.outgoingEdges(of: 0).result +let reachable = try graph.breadthFirst(fromNode: 0).result +precondition(outgoing == [0]) +precondition(reachable == [0, 1]) +``` + +## Preserve semantic failures + +```swift +let duplicate = JSONDocument(root: try JSON.parse(#"{"nodes":[{"id":"A"},{"id":"A"}],"edges":[]}"#)) +let profile = JSONGraphProfile(nodesAt: [.key("nodes")], edgesAt: [.key("edges")], nodeIDKey: "id", edgeSourceKey: "from", edgeTargetKey: "to") +do { + _ = try JSONGraphProjection.build(document: duplicate, profile: profile) + preconditionFailure("Duplicate ID was accepted") +} catch JSONGraphFailure.duplicateNodeID(let first, let second) { + precondition(first == [.key("nodes"), .index(0)]) + precondition(second == [.key("nodes"), .index(1)]) +} +``` + +Every later value, span, outgoing-edge or BFS query is a new independent whole-source +admission under the supplied finite limits. Returned JSON aliases remain charged. +No constructor budget is silently reused. Build defaults to 10,000 nodes and 50,000 +edges; outgoing queries default to 50,000 results; BFS defaults to 10,000 live pending +nodes, 10,000 visited nodes and 10,000 results. All caps are inclusive. Zero caps +refuse nonempty work; cancellation, quotas and typed semantic failures throw without +successful partial artifacts. Missing, null and wrong kind remain distinguishable. +Core selection errors retain their typed cause; operational errors are not graph facts. + +Ordinals name positions in this immutable captured source, not durable graph IDs. +There is no replacement-source argument. Replacing a document root drops obsolete +source spans; an existing graph keeps its original source and map. Node spans can be +nil for a source without a map. Storage, reducers and mutable meters are private. + +BFS uses source-edge order and visited-at-enqueue, including the start. Its output +certifies the reachable component, not global connectivity, acyclicity or schema validity. +Exact construction uses bounded linear scans and can cost quadratic work; repeated +queries re-admit the whole source. Logical retention is not RSS. Controlled G4 +performance remains HOLD, with no speed claim. No new product, graph serializer, +mutation, topology, weighted edges or persistent identity is supplied. diff --git a/Sources/JSONInspection/Graph/JSONGraphProfile.swift b/Sources/JSONInspection/Graph/JSONGraphProfile.swift new file mode 100644 index 0000000..efcd0cb --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphProfile.swift @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// Explicit physical arrays and direct string fields for a source-bound directed simple graph. +/// +/// Paths use exact typed components; roles are never guessed. Explicit field reuse is valid. +public struct JSONGraphProfile: Sendable { + /// Absolute typed location of the complete node array. + public let nodesAt: [JSONLocationComponent] + /// Absolute typed location of the complete edge array, disjoint from the node array. + public let edgesAt: [JSONLocationComponent] + /// Exact direct key of each node’s required string identifier. + public let nodeIDKey: JSONKey + /// Exact direct key of each edge’s required source string identifier. + public let edgeSourceKey: JSONKey + /// Exact direct key of each edge’s required target string identifier. + public let edgeTargetKey: JSONKey + /// Defines roles without interpreting path strings, normalizing keys or validating a source. + /// - Parameters: + /// - nodesAt: Absolute typed node-array location. + /// - edgesAt: Absolute typed edge-array location; build rejects overlap. + /// - nodeIDKey: Direct required string ID key in node rows. + /// - edgeSourceKey: Direct required string source ID key in edge rows. + /// - edgeTargetKey: Direct required string target ID key in edge rows. + public init(nodesAt: [JSONLocationComponent], edgesAt: [JSONLocationComponent], + nodeIDKey: JSONKey, edgeSourceKey: JSONKey, edgeTargetKey: JSONKey) { + self.nodesAt = nodesAt; self.edgesAt = edgesAt; self.nodeIDKey = nodeIDKey + self.edgeSourceKey = edgeSourceKey; self.edgeTargetKey = edgeTargetKey + } +} + +/// Graph semantic refusals retain exact original locations, never normalized IDs. +public enum JSONGraphFailure: Error, Sendable, Equatable { + /// The node array exceeds the explicit inclusive node cap. + case nodeLimitExceeded(limit: Int) + /// The edge array exceeds the explicit inclusive edge cap. + case edgeLimitExceeded(limit: Int) + /// An original node or edge row is not an object. + case rowNotObject(at: [JSONLocationComponent], kind: JSONTraversalKind) + /// A required direct ID or endpoint field is absent. + case idMissing(at: [JSONLocationComponent]) + /// A required direct ID or endpoint field is explicitly null. + case idNull(at: [JSONLocationComponent]) + /// A required direct ID or endpoint field is not a string. + case idWrongKind(at: [JSONLocationComponent], kind: JSONTraversalKind) + /// Two node IDs have identical decoded scalar sequences; both row paths are retained. + case duplicateNodeID(first: [JSONLocationComponent], second: [JSONLocationComponent]) + /// An edge endpoint has no exact matching node ID; its field path is retained. + case danglingEndpoint(at: [JSONLocationComponent]) + /// Two original edge rows have the same ordered endpoint pair. + case duplicateDirectedEdge(first: [JSONLocationComponent], second: [JSONLocationComponent]) + /// The requested node ordinal is outside this captured graph. + case nodeOutOfBounds(Int) + /// The requested edge ordinal is outside this captured graph. + case edgeOutOfBounds(Int) + /// BFS would exceed its live pending-node frontier cap. + case frontierLimitExceeded(limit: Int) + /// BFS would exceed its total visited-node cap. + case visitedLimitExceeded(limit: Int) + /// A query would exceed its complete-result cap. + case resultLimitExceeded(limit: Int) +} + +/// Trusted row ordinals in this captured source, not durable graph identifiers. +package struct JSONGraphEdgeRecord: Sendable, Equatable { + package let row: Int + package let source: Int + package let target: Int +} diff --git a/Sources/JSONInspection/Graph/JSONGraphProjection.swift b/Sources/JSONInspection/Graph/JSONGraphProjection.swift new file mode 100644 index 0000000..316044c --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphProjection.swift @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +/// An immutable directed simple graph attached to one completely admitted source document. +/// +/// Build validates every node, edge and adjacency slot before publication. Source-local +/// ordinals preserve physical row order; they are not durable or cross-graph identities. +/// Later queries independently admit the whole captured source under their explicit limits. +/// Construction uses bounded exact scans and may require quadratic work. +public struct JSONGraphProjection: Sendable { + /// The original captured source, including its coherent source map when supplied. + public let document: JSONDocument + /// The explicit immutable selection and field roles validated at build. + public let profile: JSONGraphProfile + package let nodeIDs: [String] + package let edges: [JSONGraphEdgeRecord] + package let offsets: [Int] + package let adjacency: [Int] + /// Complete construction coverage for both selected arrays. + public let traversal: JSONTraversalSummary + /// Number of completely validated original node rows. + public var nodeCount: Int { nodeIDs.count } + /// Number of completely validated original edge rows. + public var edgeCount: Int { edges.count } + fileprivate init(document: JSONDocument, profile: JSONGraphProfile, nodeIDs: [String], + edges: [JSONGraphEdgeRecord], offsets: [Int], adjacency: [Int], + traversal: JSONTraversalSummary) { + self.document = document; self.profile = profile; self.nodeIDs = nodeIDs + self.edges = edges; self.offsets = offsets; self.adjacency = adjacency; self.traversal = traversal + } + + /// Complete source admission, nodes, edges and adjacency share one original meter. + /// - Parameters: + /// - document: Immutable complete source; unselected subtrees are admitted too. + /// - profile: Strict disjoint typed array roots and exact direct string field keys. + /// - limits: One cumulative finite budget for source, validation, metadata and transfer. + /// - maximumNodes: Inclusive node-row cap; defaults to 10,000. + /// - maximumEdges: Inclusive edge-row cap; defaults to 50,000. + /// - Returns: A complete source-owning graph and its construction statistics. + /// - Throws: Typed graph/core selection errors, resource failure or cancellation; never a partial graph. + public static func build(document: JSONDocument, profile: JSONGraphProfile, + limits: JSONLimits = .balanced, maximumNodes: Int = 10_000, + maximumEdges: Int = 50_000) throws -> JSONOperationReport { + guard maximumNodes >= 0, maximumEdges >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + var reducer = GraphBuilder(profile: profile, limits: limits, maximumNodes: maximumNodes, maximumEdges: maximumEdges) + return try JSONTraversal(document: document, limits: limits).inspectOwned( + scope: .virtual(roots: [profile.nodesAt, profile.edgesAt], overlap: .rejectAmbiguous), + options: .init(depthLimit: 1), reducer: &reducer) + } + + /// A new whole-source operation; returns original physical outgoing edge ordinals. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: New independent whole-source and query budget. + /// - maximumResults: Inclusive returned edge-ordinal cap; defaults to 50,000. + /// - Returns: Every outgoing original edge-row ordinal in source order, or throws. + public func outgoingEdges(of node: Int, limits: JSONLimits = .balanced, maximumResults: Int = 50_000) + throws -> JSONOperationReport<[Int]> { + guard maximumResults >= 0 else { throw JSONFailure(.configuration, code: .invalidLimits) } + try checkNode(node) + var reducer = GraphQuery(graph: self, query: .outgoing(node), limits: limits, + maximumFrontier: 0, maximumVisited: 0, maximumResults: maximumResults) + return try JSONTraversal(document: document, limits: limits) + .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) + } + + /// Complete reachable component, including start; never a partial success. + /// Visited-at-enqueue and source-edge order make cycles/self-edges deterministic. + /// Queue history remains owned output; the frontier cap counts only pending nodes. + /// - Parameters: + /// - node: Original start node ordinal, included in the result. + /// - limits: New independent whole-source, metadata and traversal budget. + /// - maximumFrontier: Inclusive live pending-node cap; defaults to 10,000. + /// - maximumVisited: Inclusive total enqueued-node cap; defaults to 10,000. + /// - maximumResults: Inclusive returned node-ordinal cap; defaults to 10,000. + /// - Returns: The complete reachable component in BFS order, or throws without partial success. + public func breadthFirst(fromNode node: Int, limits: JSONLimits = .balanced, + maximumFrontier: Int = 10_000, maximumVisited: Int = 10_000, + maximumResults: Int = 10_000) throws -> JSONOperationReport<[Int]> { + guard maximumFrontier >= 0, maximumVisited >= 0, maximumResults >= 0 else { + throw JSONFailure(.configuration, code: .invalidLimits) + } + try checkNode(node) + var reducer = GraphQuery(graph: self, query: .breadthFirst(node), limits: limits, + maximumFrontier: maximumFrontier, maximumVisited: maximumVisited, + maximumResults: maximumResults) + return try JSONTraversal(document: document, limits: limits) + .inspectOwned(options: .init(depthLimit: 0), reducer: &reducer) + } + + /// Returns an original node row under a new whole-source admission and owned-output charge. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: Independent finite source/selection/output budget. + public func nodeValue(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + try checkNode(node) + return try value(at: profile.nodesAt, row: node, limits: limits) + } + /// Returns an original edge row under a new whole-source admission and owned-output charge. + /// - Parameters: + /// - edge: Original edge-row ordinal in this graph. + /// - limits: Independent finite source/selection/output budget. + public func edgeValue(at edge: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + guard edge >= 0, edge < edgeCount else { throw JSONGraphFailure.edgeOutOfBounds(edge) } + return try value(at: profile.edgesAt, row: edge, limits: limits) + } + /// Source-map lookup under a new explicit full-source admission. + /// - Parameters: + /// - node: Original node-row ordinal in this graph. + /// - limits: Independent finite source and source-map lookup budget. + /// - Returns: The original value span, or nil when this source has no matching map entry. + public func nodeSpan(at node: Int, limits: JSONLimits = .balanced) throws -> JSONOperationReport { + try checkNode(node) + let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } + let state = try scope.check(); let path = try rowPath(profile.nodesAt, row: node, scope: scope, state: state) + let span = try document.sourceMap?.span(at: path.components, kind: .value, meter: &state.meter) + return try scope.complete(span) + } + private func checkNode(_ node: Int) throws { + guard node >= 0, node < nodeCount else { throw JSONGraphFailure.nodeOutOfBounds(node) } + } + private func value(at root: [JSONLocationComponent], row: Int, limits: JSONLimits) throws -> JSONOperationReport { + let scope = try JSONReadScope(document, limits: limits); defer { scope.invalidate() } + let state = try scope.check(); let path = try rowPath(root, row: row, scope: scope, state: state) + let value = try scope.run(path.components) { try scope.select(path.components, state: $0) } + // A returned JSON alias remains owned through handoff, independently of input. + try JSONAdmission.value(value, meter: &state.meter, owned: true) + return try scope.complete(value) + } + private func rowPath(_ root: [JSONLocationComponent], row: Int, scope: JSONReadScope, + state: JSONCodingState) throws -> JSONReadPath { + var path = JSONReadPath(scope: scope, components: [], bytes: 0) + for part in root { try state.meter.work(); path = try scope.append(part, to: path.components, state: state) } + return try scope.append(.index(row), to: path.components, state: state) + } +} + +internal enum GraphWorkStage: Sendable, Equatable { case nodeIdentity, endpointIdentity, edgePair, adjacency, breadthFirst } + +internal struct GraphBuilder: JSONTraversalOwnedReducer { + /// Qualification-only phase marker; no scope/capability is exposed. + var workObserver: (@Sendable (GraphWorkStage) -> Void)? = nil + let profile: JSONGraphProfile + let limits: JSONLimits + let maximumNodes: Int + let maximumEdges: Int + var ids: [String] = [] + var edges: [JSONGraphEdgeRecord] = [] + var nodeTotal = 0, edgeTotal = 0 + + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work() + if hit.relativeDepth == 0 { + let count = try context.arrayCount(value, at: hit.absoluteLocation) + if hit.scopeRootIndex == 0 { + guard count <= maximumNodes else { throw JSONGraphFailure.nodeLimitExceeded(limit: maximumNodes) } + nodeTotal = count + _ = try ProjectionAccounting.next(count, limits: limits) + try context.retain(256) + _ = try context.retainLocation(profile.nodesAt); _ = try context.retainLocation(profile.edgesAt) + for key in [profile.nodeIDKey, profile.edgeSourceKey, profile.edgeTargetKey] { + try context.work(); try context.retain(32); try context.retainKey(key, admittingCallerInput: true) + } + } else { + guard count <= maximumEdges else { throw JSONGraphFailure.edgeLimitExceeded(limit: maximumEdges) } + edgeTotal = count + } + return .continue + } + guard case .object = value else { throw JSONGraphFailure.rowNotObject(at: try context.retainLocation(hit.absoluteLocation), kind: hit.kind) } + if hit.scopeRootIndex == 0 { + let id = try string(value, key: profile.nodeIDKey, at: hit.absoluteLocation, context: &context) + for prior in ids.indices { + try context.work() + workObserver?(.nodeIdentity) + if try context.equalStrings(ids[prior], id) { + throw JSONGraphFailure.duplicateNodeID( + first: try context.appending(.index(prior), to: profile.nodesAt), + second: try context.retainLocation(hit.absoluteLocation)) + } + } + _ = try ProjectionAccounting.next(ids.count, limits: limits) + try context.retain(16); ids.append(id) + } else { + // Required fields are checked source then target before resolving either. + let source = try string(value, key: profile.edgeSourceKey, at: hit.absoluteLocation, context: &context) + let target = try string(value, key: profile.edgeTargetKey, at: hit.absoluteLocation, context: &context) + let from = try resolve(source, key: profile.edgeSourceKey, at: hit.absoluteLocation, context: &context) + let to = try resolve(target, key: profile.edgeTargetKey, at: hit.absoluteLocation, context: &context) + for prior in edges.indices { + try context.work() + workObserver?(.edgePair) + if edges[prior].source == from, edges[prior].target == to { + throw JSONGraphFailure.duplicateDirectedEdge( + first: try context.appending(.index(prior), to: profile.edgesAt), + second: try context.retainLocation(hit.absoluteLocation)) + } + } + _ = try ProjectionAccounting.next(edges.count, limits: limits) + try context.retain(24); edges.append(.init(row: edges.count, source: from, target: to)) + try context.releaseStringAlias(source); try context.releaseStringAlias(target) + } + return .skipDescendants + } + private func string(_ value: JSON, key: JSONKey, at row: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> String { + let member = try context.directStringMember(value, key: key, at: row) + if let text = member.text { return text } + let path = try context.appending(.key(key), to: row) + guard let kind = member.kind else { throw JSONGraphFailure.idMissing(at: path) } + if kind == .null { throw JSONGraphFailure.idNull(at: path) } + throw JSONGraphFailure.idWrongKind(at: path, kind: kind) + } + private func resolve(_ text: String, key: JSONKey, at row: [JSONLocationComponent], + context: inout JSONTraversalContext) throws -> Int { + for index in ids.indices { + try context.work() + workObserver?(.endpointIdentity) + if try context.equalStrings(ids[index], text) { return index } + } + throw JSONGraphFailure.danglingEndpoint(at: try context.appending(.key(key), to: row)) + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> JSONGraphProjection { + guard ids.count == nodeTotal, edges.count == edgeTotal else { throw JSONFailure(.configuration, code: .invalidOperation) } + var offsets: [Int] = [], adjacency: [Int] = [] + try context.work(); try context.retain(8); offsets.append(0) + // Deterministic source-ordered CSR, deliberately bounded linear scans. + for node in ids.indices { + try context.work() + for edge in edges.indices { + workObserver?(.adjacency) + try context.work() + if edges[edge].source == node { + _ = try ProjectionAccounting.next(adjacency.count, limits: limits) + try context.retain(8); adjacency.append(edge) + } + } + _ = try ProjectionAccounting.next(offsets.count, limits: limits) + try context.retain(8); offsets.append(adjacency.count) + } + try context.work() + let document = try context.transferDocumentOwnership() + return .init(document: document, profile: profile, nodeIDs: ids, edges: edges, + offsets: offsets, adjacency: adjacency, traversal: summary) + } +} diff --git a/Sources/JSONInspection/Graph/JSONGraphQuery.swift b/Sources/JSONInspection/Graph/JSONGraphQuery.swift new file mode 100644 index 0000000..e23bc48 --- /dev/null +++ b/Sources/JSONInspection/Graph/JSONGraphQuery.swift @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON +import JSONNavigation + +internal struct GraphQuery: JSONTraversalOwnedReducer { + var workObserver: (@Sendable (GraphWorkStage) -> Void)? = nil + enum Query { case outgoing(Int), breadthFirst(Int) } + let graph: JSONGraphProjection + let query: Query + let limits: JSONLimits + let maximumFrontier: Int + let maximumVisited: Int + let maximumResults: Int + mutating func observe(_ hit: JSONTraversalHit, value: JSON, + context: inout JSONTraversalContext) throws -> JSONTraversalAction { + try context.work(); return .skipDescendants + } + mutating func finish(_ summary: JSONTraversalSummary, context: inout JSONTraversalContext) throws -> [Int] { + let aliases = try retainAliases(context: &context) + var result: [Int] = [] + switch query { + case .outgoing(let node): + for position in graph.offsets[node].. UInt64 { + var total: UInt64 = 0 + func retain(_ bytes: UInt64, context: inout JSONTraversalContext) throws { + try context.retain(bytes) + // Each successful retain is bounded below UInt64.max by the core. + total += bytes + } + try context.work(); try retain(256, context: &context) + for path in [graph.profile.nodesAt, graph.profile.edgesAt] { + for part in path { + try context.work(); try retain(32, context: &context) + if case .key(let key) = part { + for _ in key.string.utf8 { try context.work(); try retain(1, context: &context) } + } + } + } + for key in [graph.profile.nodeIDKey, graph.profile.edgeSourceKey, graph.profile.edgeTargetKey] { + try context.work(); try retain(32, context: &context) + for _ in key.string.utf8 { try context.work(); try retain(1, context: &context) } + } + for id in graph.nodeIDs { + try context.work(); try retain(48, context: &context) + for _ in id.utf8 { try context.work(); try retain(1, context: &context) } + } + for _ in graph.edges { try context.work(); try retain(24, context: &context) } + for _ in graph.offsets { try context.work(); try retain(8, context: &context) } + for _ in graph.adjacency { try context.work(); try retain(8, context: &context) } + return total + } +} diff --git a/Sources/JSONNavigation/JSONTraversalContext.swift b/Sources/JSONNavigation/JSONTraversalContext.swift index 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 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") + } +}