From 1f1cc8af95840ec218f55641c5a0f34d2984a49c Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:49:34 +0200 Subject: [PATCH 01/59] Plan JSONHTML rendering and exact export for 1.3.0 --- .../Architecture/Release1.3/Acceptance.json | 614 ++++++++++++++++++ .../Architecture/Release1.3/Decisions.md | 22 + Documentation/Architecture/Release1.3/Plan.md | 74 +++ .../Architecture/Release1.3/Progress.json | 52 ++ 4 files changed, 762 insertions(+) create mode 100644 Documentation/Architecture/Release1.3/Acceptance.json create mode 100644 Documentation/Architecture/Release1.3/Decisions.md create mode 100644 Documentation/Architecture/Release1.3/Plan.md create mode 100644 Documentation/Architecture/Release1.3/Progress.json diff --git a/Documentation/Architecture/Release1.3/Acceptance.json b/Documentation/Architecture/Release1.3/Acceptance.json new file mode 100644 index 0000000..db1be33 --- /dev/null +++ b/Documentation/Architecture/Release1.3/Acceptance.json @@ -0,0 +1,614 @@ +{ + "schema_version": 1, + "release": "1.3.0", + "requirements": [ + { + "id": "JH01", + "gate": "H0", + "requirement": "The product/module is JSONHTML in the same package; its target depends on JSON and the exact public set contains five products.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH02", + "gate": "H0", + "requirement": "Forward product policy, manifest/CI assertions, consumers and test-count expectations are revised coherently; historical released records remain accurate.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH03", + "gate": "H0", + "requirement": "Data/String/report APIs, checked Sendable configuration, finite presets and asset strategy compile through a public consumer.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH04", + "gate": "H1", + "requirement": "All six root kinds, empty containers and mixed deep/wide structures produce a complete strict UTF-8 HTML document.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH05", + "gate": "H1", + "requirement": "Large integers, exact decimals, negative zero and admitted exponent/number spellings survive display and exported JSON without JS numeric conversion.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH06", + "gate": "H1", + "requirement": "Scalar-distinct composed/decomposed keys/strings and insertion/array order remain distinct and ordered.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH07", + "gate": "H1", + "requirement": "One noncopyable invocation meter covers admission, traversal, token helpers, paths, assets, Base64, annotations and result copies.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH08", + "gate": "H1", + "requirement": "Inclusive boundaries and checked overflow cover core work/output/retention and renderer node/DOM/document/outline/annotation capacities.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH09", + "gate": "H1", + "requirement": "Cancellation inside owned loops and valid restrictive/zero ceilings produce honest failures without a successful partial result/report.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH10", + "gate": "H1", + "requirement": "Repeated and concurrent calls have exclusive state; deterministic output uses stable IDs/assets and no clock/random mutation.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH11", + "gate": "H1", + "requirement": "Data and String materialization charges are complete; final-document byte count is distinguished from cumulative core helper output.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH12", + "gate": "H2", + "requirement": "Formatted mode has no script; interactive native disclosure remains usable with scripting disabled and allows multiple open branches.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH13", + "gate": "H2", + "requirement": "Default expansion, bounded outline, ancestor opening, focus, root/empty/numeric/special-key paths and generated IDs behave coherently.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH14", + "gate": "H2", + "requirement": "Copy/download bytes decode to the owned formatted JSON; RFC Pointer metadata preserves controls, CR/LF, NUL and exact scalar sequences.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH15", + "gate": "H2", + "requirement": "Raw and escaped Pointer copy modes have explicit labels; unavailable clipboard behavior is reported and manual/UTF-8 alternatives work.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH16", + "gate": "H2", + "requirement": "Literal search is bounded/chunked, preserves scalar distinction, opens matched ancestors and reports its rendered-text scope honestly.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH17", + "gate": "H2", + "requirement": "Script/style closing strings, ampersands, quotes, event-handler payloads and keys such as __proto__/constructor remain inert data.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH18", + "gate": "H2", + "requirement": "No payload-driven innerHTML, executable interpolation, URLs/selectors or script-element text assignment appears in the output/control path.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH19", + "gate": "H2", + "requirement": "Emitted CSS/JS asset hashes match CSP block bytes; controls work under policy and external resource requests remain absent.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH20", + "gate": "H2", + "requirement": "Annotations attach by exact typed path or identify missing targets at their parent; no empty list asserts schema validity.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH21", + "gate": "H2", + "requirement": "JSONDocument locations remain coherent, unmapped/replaced roots invent none, and annotation messages/paths are bounded and escaped.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH22", + "gate": "H3", + "requirement": "Safari, Firefox and Chromium execute file/served, JS on/off, outline/search/copy/download and policy cases with recorded versions.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH23", + "gate": "H3", + "requirement": "Keyboard/focus, text/contrast/themes and relevant VoiceOver behavior are reviewed; accessibility claims match the executed evidence.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH24", + "gate": "H3", + "requirement": "Print output includes every value regardless of collapse/script state, excludes UI clutter, and preserves applicable UI state.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH25", + "gate": "H3", + "requirement": "Five-product/platform/resource qualification; pure rendering does not execute UI frameworks, while JSONHTML may link narrowly isolated Apple PDF/printing frameworks.", + "status": "not_run", + "evidence": [], + "amendment": "Maintainer-approved transfer/export framework amendment; original JH25 remains historical." + }, + { + "id": "JH26", + "gate": "H3", + "requirement": "Representative generation/output-expansion/retention and browser DOM/interactivity measurements justify finite preset values.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH27", + "gate": "H4", + "requirement": "Existing JSONWriter behavior, native/safety/docs lanes, public examples and baseline semantic/corpus cases remain qualified.", + "status": "not_run", + "evidence": [] + }, + { + "id": "JH28", + "gate": "H4", + "requirement": "Final feature branch/PR head/base, review disposition, source/asset hashes, actual receipts and release placement are recorded.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX01", + "gate": "T0", + "requirement": "Reconcile live source/worktree/renderer branches and preserve unrelated changes before tracked edits.", + "status": "passed", + "evidence": [ + "Live main/origin, tag, worktree and ruleset reads; isolated codex/1.3-jsonhtml checkout at 532f725; primary checkout unchanged" + ] + }, + { + "id": "TX02", + "gate": "T0", + "requirement": "Exact five-product manifest, forward policy and CI expectations; no sixth product or external root package dependency.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX03", + "gate": "T0", + "requirement": "Final public APIs, reports, errors, finite presets and representation builders compile with actual Swift 6.4 / SDK 27.", + "status": "not_run", + "evidence": [], + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + }, + { + "id": "TX04", + "gate": "T0", + "requirement": "Explicit framework amendment; pure generation executes without WebKit; core JSON imports no WebKit/AppKit/UIKit.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX05", + "gate": "T0", + "requirement": "One JSON conformance in JSONHTML, qualified same-package compiler behavior and independent public consumers.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX06", + "gate": "T0", + "requirement": "Actual UTType and exported/imported-content-type probes, conditional availability and correct extensions.", + "status": "not_run", + "evidence": [], + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + }, + { + "id": "TX07", + "gate": "T1", + "requirement": "Legacy writer default/output/mutation/initializer behavior retained under spaces, compact and zero indentation.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX08", + "gate": "T1", + "requirement": "Tabs emitted directly by the cursor; checked 0...8 unit count and separate tab display width.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX09", + "gate": "T1", + "requirement": "Shared helpers admit the input once and keep one noncopyable native preparation meter across selected formats.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX10", + "gate": "T2", + "requirement": "Exact large numbers, negative zero, exponent spellings and constructed-number fallback; insertion order preserved.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX11", + "gate": "T2", + "requirement": "Composed/decomposed keys and strings, empty/numeric/control/special keys preserve decoded scalar identity.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX12", + "gate": "T2", + "requirement": "All six root kinds, empty containers and mixed/deep/wide values are handled without recursive untrusted traversal.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX13", + "gate": "T2", + "requirement": "Native .json import uses strict parser and finite defaults; invalid UTF-8, duplicate keys and trailing syntax fail.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX14", + "gate": "T2", + "requirement": "JSON/native JSON equality does not replace separate scalar/order/lexeme checks; no Foundation JSON authority.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX15", + "gate": "T2", + "requirement": "Plain text contains only the pretty JSON body with quoted scalar-string roots and no UI/fence/labels.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX16", + "gate": "T2", + "requirement": "Markdown uses json info string, safe fence, actual tabs, LF policy and unaltered JSON body bytes.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX17", + "gate": "T2", + "requirement": "No mutable global export defaults, shared render cache or heavy ProxyRepresentation getter.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX18", + "gate": "T2", + "requirement": "Immutable prepared artifacts match advertised/available sets; conditional exports omit missing or unselected data.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX19", + "gate": "T2", + "requirement": "Selecting unavailable/empty format sets fails; explicit JSON/Markdown/PDF selection advertises exactly that format.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX20", + "gate": "T2", + "requirement": "Exact data(for:) lookup does no rendering/subtype negotiation; unavailable data produces bounded error.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX21", + "gate": "T2", + "requirement": "Bounded filename/title validation prevents control/path/separator traversal and selects correct extensions.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX22", + "gate": "T2", + "requirement": "Wrapper presentation formats are export-only; direct import advertises native JSON only.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX23", + "gate": "T2", + "requirement": "Direct exports use documented bounded defaults and independently owned state per transport request.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX24", + "gate": "T2", + "requirement": "One native preparation call is all-or-nothing; failures install no partial artifact or successful report.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX25", + "gate": "T3", + "requirement": "Static transfer HTML is complete, expanded, highlighted and script-free with deterministic default theme.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX26", + "gate": "T3", + "requirement": "Static export omits controls/hidden disclosure/line numbers/outline/annotations by default; no JSON truncation.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX27", + "gate": "T3", + "requirement": "Escaping/CSP/assets hold for script/style delimiters, quotes, ampersands, Unicode/control and prototype-like keys.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX28", + "gate": "T3", + "requirement": "No external requests/fonts or browser numeric reconstruction; emitted CSS/CSP hashes match actual block bytes.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX29", + "gate": "T3", + "requirement": "Standalone HTML opening and native rich-paste compatibility are qualified separately.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX30", + "gate": "T3", + "requirement": "Per-format and aggregate prepared byte limits include hidden artifacts and overlapping owned buffers.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX31", + "gate": "T3", + "requirement": "Inclusive minus-one/boundary/plus-one and overflow probes for work/retention/output/metadata/style/HTML caps.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX32", + "gate": "T3", + "requirement": "Cancellation inside owned loops, result-copy failure and terminal latch behavior preserve accounting.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX33", + "gate": "T3", + "requirement": "Repeated and concurrent native calls are deterministic and invocation-owned; all state is checked Sendable.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX34", + "gate": "T4", + "requirement": "RTF is valid styled RTF with token colors/monospace/tab stops; no renamed HTML/Markdown/attachments.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX35", + "gate": "T4", + "requirement": "Independent RTF decode matches pretty body with Unicode/surrogate/combining/tab/backslash/brace cases.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX36", + "gate": "T4", + "requirement": "RTF strategy has bounded growth/run/UTF-16 range accounting and qualified platform/isolation behavior.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX37", + "gate": "T4", + "requirement": "RTF styles remain present in Mac/mobile decoder and real rich-editor observations; recipient limits recorded.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX38", + "gate": "T5", + "requirement": "PDF preparation is explicit async MainActor work; synchronous native prepare and transfer closures never render PDF.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX39", + "gate": "T5", + "requirement": "WebPage/WKWebView backend is actually compiled and qualified; WebPage preference/fallback rationale recorded.", + "status": "not_run", + "evidence": [], + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + }, + { + "id": "TX40", + "gate": "T5", + "requirement": "Document-owned static HTML prerequisite required; no silent value rerender or unbounded nested quota reset.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX41", + "gate": "T5", + "requirement": "Nonpersistent isolated WebKit state, JS disabled, denied external navigation/subresources and real layout readiness.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX42", + "gate": "T5", + "requirement": "Continuous capture complete; paginated A4/Letter output is genuine, measured and never silently downgraded.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX43", + "gate": "T5", + "requirement": "PDF pages include last sentinel, long/wrapped Unicode content and legible/selectable text without UI clutter.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX44", + "gate": "T5", + "requirement": "PDF input/output/page/bundle caps and aggregate retention checked; platform work distinguished from native meter.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX45", + "gate": "T5", + "requirement": "Cancellation/timeout during load/layout/export returns honestly, disposes state and rejects late completions.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX46", + "gate": "T5", + "requirement": "Concurrent PDF jobs have separate state/cancellation; no singleton web view or unsafe actor annotation.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX47", + "gate": "T5", + "requirement": "Successful PDF adds one immutable artifact; failure leaves original document and advertisement unchanged.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX48", + "gate": "T6", + "requirement": "Five-product public consumers/resources and Apple framework boundary qualify Mac/iOS/iPadOS SDK/simulator/device cases.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX49", + "gate": "T6", + "requirement": "Actual Mac/iOS/iPad receiving-app matrix records share/drag/paste/open behavior and selected content types.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX50", + "gate": "T6", + "requirement": "Plain-text fallback versus Markdown subtype tested; explicit Copy JSON/Copy Markdown/Share PDF paths qualified.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX51", + "gate": "T6", + "requirement": "Existing renderer JH requirements qualified with explicit JH25 dependency amendment; no hidden weakened baseline.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX52", + "gate": "T6", + "requirement": "Native/safety/docs lanes pass on final exact candidate with deliberate nonzero suite/case counts and current base.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX53", + "gate": "T6", + "requirement": "Generation, combined preparation, RTF/PDF expansion and responsiveness measurements justify frozen finite profiles.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX54", + "gate": "T6", + "requirement": "Public docs/examples describe exactness, preparation, format selection, recipient and system-resource boundaries.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX55", + "gate": "T6", + "requirement": "Separate review pass findings resolved; final API/framework/limits choices and source/log/artifact identity returned.", + "status": "not_run", + "evidence": [] + }, + { + "id": "TX56", + "gate": "T6", + "requirement": "Release slot reconciled; no main push/tag/release/messages under this brief alone; all incomplete rows explicit.", + "status": "not_run", + "evidence": [] + } + ], + "gate_status": { + "H0": "not_run", + "H1": "not_run", + "H2": "not_run", + "H3": "not_run", + "H4": "not_run", + "T0": "not_run", + "T1": "not_run", + "T2": "not_run", + "T3": "not_run", + "T4": "not_run", + "T5": "not_run", + "T6": "not_run" + } +} diff --git a/Documentation/Architecture/Release1.3/Decisions.md b/Documentation/Architecture/Release1.3/Decisions.md new file mode 100644 index 0000000..ac14f7b --- /dev/null +++ b/Documentation/Architecture/Release1.3/Decisions.md @@ -0,0 +1,22 @@ +# 1.3 decisions and open qualification choices + +| Decision | Disposition | +| --- | --- | +| Product and version | Maintainer assigns JSONHTML as the fifth product in the existing Swift-JSON package, targeting 1.3.0. | +| 1.2 release ownership | Linux/WASI release work proceeds separately. This branch must integrate the final 1.2 source before its final gates. | +| Specification precedence | Transfer/export explicit amendments override conflicting renderer statements; remaining H/JH requirements remain. | +| Package boundary | JSONHTML depends on JSON only; private regex and empty root external dependencies remain. Apple framework declarations/execution stay conditional and out of core JSON. | +| Framework amendment | JSONHTML may link Apple PDF/printing frameworks. Pure HTML/native export preparation remains actor-independent and does not execute WebKit. | +| Writer compatibility | Proposed JSONIndentationUnit with default spaces; append the defaulted option to retain existing calls. Export tabs/preserved lexemes are explicit. Final declaration requires compiler/behavior evidence. | +| Asset strategy | Prefer versioned original Swift constants for fixed CSS/JS; measure and qualify hashes, CSP and full emitted bytes before selection is frozen. | +| RTF | Prefer owned bounded token-based emission. Strategy remains provisional until independent text/style and platform evidence. | +| PDF | Prefer modern WebPage where executed behavior qualifies it. Region capture is distinct from pagination; A4/Letter/default geometry and backend remain open. | +| Finite presets | No invented values. Freeze additional finite ceilings from native/browser/format measurements; leave core 49 metric values/meanings unchanged. | +| Transfer selection | One Apple JSON conformance, JSON-only strict import, immutable per-item export selection. PDF must already be prepared. | +| Device limitation | Preserve physical-iPad deferral explicitly; record actual iPhone and simulator checks for new capabilities. | + +The initial SDK declaration probe typechecked DataRepresentation, exportingCondition, suggestedFileName, exported/importedContentTypes, UTType.markdown, WebPage.load(html:), mediaType and exported(as: .pdf(region:)) on macOS, iPhoneOS and iPhoneSimulator SDK 27.2 with Swift 6.4/language 6 and warnings as errors. This confirms these declaration shapes, not JSON same-package conformance, offscreen rendering, format negotiation, finite policies or pagination. Actual source/log/SDK receipts remain private maintainer evidence. + +The first probe was stopped by the default module-cache sandbox path before loading the standard library. The successful run used an invocation-owned explicit module-cache directory. No compatibility or unsafe compiler flag was added. + +Review findings resolved in this plan: captured base/release assignment is stale; fifth-product and Apple PDF amendments are explicit; PDF region export cannot establish pagination; default numeric policy requires measurements; all six formats remain in the intended objective; source/receiver/system work and historical qualification remain separate. diff --git a/Documentation/Architecture/Release1.3/Plan.md b/Documentation/Architecture/Release1.3/Plan.md new file mode 100644 index 0000000..20db5f0 --- /dev/null +++ b/Documentation/Architecture/Release1.3/Plan.md @@ -0,0 +1,74 @@ +# Swift-JSON 1.3.0 — JSONHTML and exact export + +Status: maintainer-approved release direction; implementation and qualification pending. + +1.3.0 introduces **JSONHTML**, a fifth library product in the existing Swift-JSON package. It depends on JSON; JSON does not acquire a reverse dependency. The five-product policy becomes effective with the working target, consumers and matching CI checks. Historical four-product releases remain unchanged. JSONRegexRuntime remains private and root external dependencies remain empty. + +The portable product renders complete self-contained HTML from exact values/documents. Its export preparation supplies JSON, plain text, Markdown, static HTML and qualified styled RTF. On supported Apple platforms, JSONHTML also provides CoreTransferable integration and explicit asynchronous PDF preparation. This is one product, not a new repository or a sixth adapter product. + +## Release coordination + +The 1.2.0 release train owns Linux and WASI work. This feature begins from merged Linux source `532f725740a4d61c32bc9870986d050b71f9db78`, rather than the briefs' captured `e9c6693c7cb29df5125e1699fef70f98229ccfdf`. Before integration, bring the final qualified 1.2.0 source into the feature and rerun affected checks against that exact combined tree. Do not modify the parallel worktree, change release assets or route this feature into 1.2.0. + +No 1.3.0 tag, merge or publication is authorized by this plan. A complete release candidate must pass the implementation/qualification gates first. + +## Reconciled contracts + +The renderer specification supplies H0–H4 and JH01–JH28. The transfer/export specification adds T0–T6 and TX01–TX56. The transfer pack's explicit amendments take precedence where these documents differ; the maintainer assigns the combined scope to 1.3.0. + +- Pure renderer/export preparation is synchronous, invocation-owned and callable away from MainActor. Apple transfer/framework declarations are conditional; only PDF/printing platform lifetime is MainActor isolated. Core JSON imports no WebKit, AppKit, UIKit or CoreTransferable. +- JSONHTML may link the required Apple frameworks for its PDF backend. Pure HTML generation never starts WebKit. This explicitly amends the earlier module-wide framework exclusion and JH25. +- Define one JSON: Transferable conformance in JSONHTML on Apple. Strict bounded native JSON is the only import representation. HTML, text, Markdown, RTF and PDF are export-only. +- Preserve existing JSONWriter defaults and calls: compact nil indentation, spaces, insertion order and normalized number style. Add a defaulted indentation-unit parameter at a source-compatible position. Exports deliberately select preserved number spelling and one tab per level; direct renderer defaults remain two spaces. +- Use exact typed paths, generated ordinal IDs and inert ASCII Base64 metadata. Preserve scalar-distinct keys, numeric lexemes, insertion order and the distinction between absence/null. Never reconstruct JSON through browser Number/JSON.parse/JSON.stringify or Foundation serialization. +- Full output is all-or-nothing. No hidden truncation, partial preview, new online resolver, arbitrary script/theme injection or asynchronous sink API enters this scope. + +## Delivery sequence + +| Slice | Gates and acceptance | Reviewable result | +| --- | --- | --- | +| A — Contracts and native probes | H0/T0; JH01–03; TX01–06 | Compiler-driven public shapes, actual content types/conformance behavior, conditional Apple boundary, asset/version strategy and measured finite-preset proposal. Add the fifth target only alongside working code. | +| B — Shared exact formatting | T1; JH07–11; TX07–09 | Additive indentation units and admitted token helpers. Preserve all legacy writer/stream output; prove one noncopyable writing meter and unchanged original default semantics. | +| C — Formatted renderer and baseline exports | H1/T2/T3; JH04–11, JH17–21; TX10–33 | Complete static highlighted HTML, native JSON/text/Markdown, immutable prepared artifacts, strict JSON import, exact format lookup/selection, bounded metadata and one cumulative native preparation operation. | +| D — Interactive inspection | H2; JH12–24 | Multiple native disclosure nodes, bounded outline/literal search, themes/focus, exact copy/download fallbacks, annotations/coherent source locations, CSP and full-content printing. | +| E — Styled RTF | T4; TX34–37 | Owned bounded RTF emitter preferred, independently decoded text/styles, Unicode/surrogate/tab-stop fidelity and real receiver evidence. | +| F — Apple PDF | T5; TX38–47 | Explicit async preparation from the document's own static HTML; qualified WebPage or justified fallback, actual continuous/paginated policies, isolated state, cancellation/timeout/late-result handling, page completeness and immutable success. | +| G — Integrated qualification and docs | H3/H4/T6; JH22–28; TX48–56 | All final platform/consumer/browser/receiver checks, comparative measurements, reviewed finite profiles, DocC/examples, separate source review and reproducible local candidate. | + +Keep coherent commits on the 1.3 feature branch initially. Split B, E and F into dependent branches/PRs when useful for review; base them on actual feature commits. Each PR describes its dependency and exact base/head. Intermediate four-format output is an intermediate result, not completion of the six-format release. + +## Accounting and fidelity design + +Admit the JSON root once and pass the active noncopyable writing meter through token generation, escaping, Base64, paths, assets, annotation binding and final Data/String materialization. Extract package-scoped admitted helpers narrowly; invoking independent public writers/renderers inside one preparation operation must not restart quotas. + +The 49 core metrics retain their meanings/default values/weights. JSONHTMLLimits and JSONExportLimits add distinct finite product capacities for emitted/DOM nodes, HTML bytes, outline/annotations, aggregate/per-format prepared bytes and bounded metadata/style runs. Finite numeric presets require representative measurements before freezing; the briefing supplies no qualified numbers. + +Reports distinguish final HTML bytes, cumulative helper output, per-format/aggregate artifacts and logical peak retention. Hidden prepared artifacts and overlapping token/copy/print/markup buffers count. Zero/restrictive quotas remain meaningful; checked growth/overflow and in-loop cancellation remain required. A failed operation returns no successful document/report. + +PDF preparation is a second explicit operation with a separate finite PDF allowance. It admits the existing bundle and static HTML, bounds owned work/input/output/page count and ensures final aggregate artifact limits. Web engine allocations, transport copying and receiver work are platform/caller work; none imply a hard RSS or instruction cap. No composite convenience may hide another reset. + +## Asset, transfer and PDF decisions + +Prefer original fixed CSS/vanilla JS as generated Swift constants with explicit versions and byte hashes, avoiding another resource-bundle dependency. Qualify actual emitted block hashes/CSP, UTF-8 copy/Pointer reconstruction, payload inertness and absence of external requests. Original assets retain Apache-2.0; record any adaptation and its notices before inclusion. + +Direct transfer order is JSON, static HTML, UTF-8 plain text, Markdown. Prepared order is JSON, static HTML, RTF, UTF-8 plain text, Markdown, PDF, omitting unselected/unavailable representations through immutable conditions. Plain-text requests can match conforming subtypes; test real negotiation and explicit format-only selections. OS requests are independent bounded operations. Transfer closures return prepared PDF bytes and never start WebKit. + +Prefer a bounded owned RTF emitter to preserve actor-independent preparation. Verify its decoded body/styles through independent platform decoders and recipients. A necessary actor-bound native RTF adapter would require a separate explicit asynchronous operation and a recorded API decision. + +The SDK exposes WebPage loading, print media and region-based PDF export. Declaration availability does not prove offscreen behavior or pagination. Probe full-content capture separately from genuine A4/US Letter pagination and page limits; do not label a tall capture as paginated output. Freeze PDF geometry/media/wrapping/defaults only after an executed backend result. Preserve nonpersistent per-job state, deny external navigation/subresources and scripting, await actual readiness, dispose state on every terminal path and reject late callbacks. + +## Acceptance and release evidence + +Acceptance.json retains all **84** requirement IDs. Status changes require evidence; package integrity and SDK typechecking do not qualify renderer, transfer receiver or PDF behavior. Treat missing environments as blocked/not_run and preserve actual failed probes. + +- Native Swift Testing: all six roots, exact large/exponent/negative-zero numbers, scalar/order/lexeme checks, controls/prototype-like keys, deep/wide inputs, all new quota boundaries/overflow, mid-loop cancellation, terminal failures and concurrent independent calls. +- Existing baseline: retain exact test discovery, all 49 cap probes, both pinned independent suites and policy exclusions, malformed replay, ordinary-invalid versus operational-error outcomes and required Native / native, Native / safety and Native / docs names/settings. +- Five-product consumers: macOS, mobile SDK/simulator/device matrix and the qualified 1.2 Linux/WASI matrix for portable renderer/export capabilities. Apple-only transfer/PDF rows must remain distinct. Preserve the approved physical-iPad deferral explicitly; no simulator result is a physical-device pass. +- Browser: Safari/Firefox/Chromium, local file/served, scripting on/off, CSP/network, search/outline, keyboard/focus/themes, relevant VoiceOver, clipboard fallback/actual UTF-8 downloads and complete print output. +- Apple export: explicit type negotiation, share/drag/paste/open receivers, independently decoded RTF, PDF pages/bounds/last-content sentinel and long/Unicode/bidi content. Record app/font/recipient limitations and actual platform versions. +- Measurements: generation/preparation costs, output expansion, logical retention, browser DOM/interactivity and PDF responsiveness. Preserve the 10% review trigger and prior dispositions; do not infer hard RSS or browser CPU guarantees. +- Final integration: fresh exact-source affected lanes, current-base reviews, five DocC catalogues/compiled examples, updated product/support/dependency contracts, deterministic source/evidence archives and matching qualified assets. + +## First implementation increment + +Finish same-package JSON conformance and renderer/report compiler probes, then implement the additive indentation unit and a tested exact formatted-output path. Add meaningful fixtures and public consumers with that code. Introduce JSONHTML, forward five-product policy, exact manifest/test assertions and its DocC catalogue coherently, without an empty placeholder target. Record finite-profile measurements and backend gaps as open until their evidence exists. diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json new file mode 100644 index 0000000..278765c --- /dev/null +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "target_release": "1.3.0", + "status": "Plan and isolated feature branch prepared; implementation pending", + "actual_base": "532f725740a4d61c32bc9870986d050b71f9db78", + "captured_briefing_base": "e9c6693c7cb29df5125e1699fef70f98229ccfdf", + "feature_branch": "codex/1.3-jsonhtml", + "pr": null, + "release_train_dependency": "Integrate final qualified 1.2.0 Linux/WASI source before final feature gates", + "expected_final_products": [ + "JSON", + "JSONSchema", + "JSONStreaming", + "JSONPatch", + "JSONHTML" + ], + "current_manifest_products": 4, + "qualified_scope": [ + "Briefing integrity only", + "Source/worktree/ruleset orientation", + "Three supported Apple SDK declaration probes only" + ], + "implementation_qualified": false, + "next_increment": "Same-package conformance/API probes, additive indentation units and tested formatted renderer/export path; introduce fifth target and exact checks with working source", + "open_decisions": [ + "Measured finite renderer/export/PDF presets", + "Executed PDF backend/pagination/defaults", + "Qualified RTF strategy", + "Exact final declaration shapes" + ], + "notes": [ + "No raw private briefing packs or source-clone records enter Git", + "No empty product or passing placeholder tests", + "Existing core defaults and 49 metric meanings retained", + "No messages, merge, release or protection changes" + ], + "input_packets": [ + { + "file": "JSONHTML-Transfer-Export-Codex-Briefing-v0.1.zip", + "sha256": "5394fccbcd48244004e146189f4c764857cb83c05fd8a2a91380ca4f9d752105", + "bytes": 75580, + "entries": 40 + }, + { + "file": "JSONHTML-Fifth-Product-Briefing-v0.1.zip", + "sha256": "5985664bbc71b188fb0aeecf7b3eb4d872ce41e2d3e16851dfb36eecee91b61f", + "bytes": 15997, + "entries": 8 + } + ], + "updated_utc": "2026-10-13T10:48:57.258071+00:00" +} From b3c63c95af9b62f345996ae59398a4e64fbf10b7 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:29:36 +0200 Subject: [PATCH 02/59] feat: add exact formatted JSONHTML product --- AGENTS.md | 3 +- CHANGELOG.md | 5 + Documentation/Architecture/API.md | 7 +- .../Architecture/ExtractionManifest.json | 3 +- .../Architecture/Release1.3/Acceptance.json | 314 ++++++++++++------ .../Architecture/Release1.3/Decisions.md | 25 ++ .../Architecture/Release1.3/Increment.md | 75 +++++ Documentation/Architecture/Release1.3/Plan.md | 2 +- .../Architecture/Release1.3/Progress.json | 21 +- Documentation/CI/README.md | 4 +- Documentation/DocumentationTooling.md | 9 +- Documentation/Qualification/README.md | 9 + Documentation/README.md | 5 +- Examples/Consumer/Package.swift | 3 +- Examples/Consumer/README.md | 3 + Examples/Consumer/Sources/Consumer/main.swift | 16 + Package.swift | 5 +- README.md | 22 +- Scripts/README.md | 4 +- Scripts/build_documentation.py | 2 +- Scripts/build_documentation_site.py | 10 +- Scripts/ci.py | 9 +- Scripts/documentation_catalog.py | 2 +- Sources/JSON/Codec/JSONWriter.swift | 67 ++-- Sources/JSON/Codec/JSONWritingOptions.swift | 19 +- Sources/JSON/Documentation.docc/JSON.md | 2 + Sources/JSON/Limits/JSONWorkMeter.swift | 4 + .../Documentation.docc/FormattedRendering.md | 53 +++ .../JSONHTML/Documentation.docc/JSONHTML.md | 37 +++ Sources/JSONHTML/JSONHTMLAssets.swift | 9 + Sources/JSONHTML/JSONHTMLRenderReport.swift | 32 ++ Sources/JSONHTML/JSONHTMLRenderer.swift | 179 ++++++++++ .../JSONHTML/JSONHTMLRenderingOptions.swift | 104 ++++++ .../FormattedRendererTests.swift | 215 ++++++++++++ Tests/JSONTests/FormattingTests.swift | 65 ++++ 35 files changed, 1184 insertions(+), 160 deletions(-) create mode 100644 Documentation/Architecture/Release1.3/Increment.md create mode 100644 Sources/JSONHTML/Documentation.docc/FormattedRendering.md create mode 100644 Sources/JSONHTML/Documentation.docc/JSONHTML.md create mode 100644 Sources/JSONHTML/JSONHTMLAssets.swift create mode 100644 Sources/JSONHTML/JSONHTMLRenderReport.swift create mode 100644 Sources/JSONHTML/JSONHTMLRenderer.swift create mode 100644 Sources/JSONHTML/JSONHTMLRenderingOptions.swift create mode 100644 Tests/JSONHTMLTests/FormattedRendererTests.swift create mode 100644 Tests/JSONTests/FormattingTests.swift diff --git a/AGENTS.md b/AGENTS.md index b8050da..443ee58 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,8 @@ Swift-JSON requires Swift tools/compiler 6.4+, Swift language mode 6 and macOS/SDK 27+. Read README.md, CONTRIBUTING.md and the relevant contracts in Documentation/Architecture before changing behavior. -- Keep four public products: JSON, JSONSchema, JSONStreaming and JSONPatch. +- Keep five public products: JSON, JSONSchema, JSONStreaming, JSONPatch and JSONHTML. + JSONHTML depends on JSON only; the maintainer-approved 1.3 addition preserves historical four-product releases. JSONRegexRuntime is package-private. Root package dependencies remain empty. - Preserve exact bounded decimals, decoded Unicode scalar identity, ordered objects with order-independent equality, one strict UTF-8 grammar, native diff --git a/CHANGELOG.md b/CHANGELOG.md index c0cb962..bc51116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Develop JSONHTML as the fifth product for 1.3: exact formatted standalone HTML, + shared strict-writer token formatting, literal tab indentation, cumulative + finite reports, bounded script-free output and a fifth native DocC catalogue. + Interactive rendering and transfer/RTF/PDF export remain later increments. + - Keep object-schema construction and document admission within one cumulative budget, including live retention and terminal owned failures/cancellation. Tight limits that previously fit each phase separately can now fail the composed diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index 9df8e3c..3a0bfeb 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -1,7 +1,7 @@ # Public API contract This document describes the implemented public interface. Native compiler symbol -graphs supply the complete declarations in the four DocC catalogs. Examples +graphs supply the complete declarations in the five DocC catalogs. Examples in the [README](../../README.md) are compiled and executed by the public consumer. ## Products and ownership @@ -12,9 +12,10 @@ in the [README](../../README.md) are compiled and executed by the public consume | JSONSchema | JSONSchemaDocument, JSONSchemaRegistry, JSONSchemaCompiler, CompiledJSONSchema, JSONSchemaValidator | Offline immutable plan, invocation-owned evaluation and private native regex | | JSONStreaming | JSONEventSequence, JSONValueSequence, NDJSONValueSequence, JSONTopLevelArraySequence, JSONAsyncWriter | Exclusive pull state, shared codec, strict framing and awaited handoffs | | JSONPatch | JSONPatch, JSONPatchOperation, JSONPatchResult, JSONMergePatch | Transaction staging, exact comparison and iterative editing | +| JSONHTML | JSONHTMLRenderer, JSONHTMLRenderingOptions, JSONHTMLLimits, JSONHTMLRenderReport | Shared exact formatting, script-free standalone HTML and cumulative bounded emission | JSON imports no siblings. JSONSchema depends on JSON and package-private -JSONRegexRuntime. Streaming and Patch depend on JSON. There are four public +JSONRegexRuntime. Streaming, Patch and HTML depend on JSON. There are five public products and no external root-package dependencies. Runtime code uses no HTTP, UI, MCP or documentation-tool dependencies. @@ -66,7 +67,7 @@ BOM, malformed UTF-8, unpaired surrogate escapes, raw controls, malformed number duplicate decoded keys and trailing data. BOM rejection is the package's strict policy. Parsing and streaming use one grammar. -JSONWritingOptions selects compact output or indentation of 0...8 spaces, insertion +JSONWritingOptions selects compact output or indentation of 0...8 units (spaces by default or literal tabs), insertion or scalar key order, and normalized or preserved number text. Preserved lexemes were admitted with their grammar and identity. Sorting makes no cryptographic canonicalization claim. diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 0ac51bf..cbf2680 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -194,7 +194,8 @@ "JSONNumber.swift reference reviewed; new bounded decimal storage and conversions written without copying upstream numeric implementation.", "Native codec, Codable, Pointer, Patch and streaming are original implementations from the integrated contract, not automatically attributed to candidates.", "Schema graph, explicit evaluator, exact sets/assertions and scalar format recognizers are original implementations from Draft 2020-12, RFC3986/3987/3339/3492/5321/5891/5892/5893/6531/6570/6901 and fixed Unicode13 data.", - "Private FIPS 180-4 SHA-256 is an original Swift implementation with checked cumulative production accounting, bounded scratch and inner cancellation. The separate trusted fixture tool mirrors its primitive core byte-for-byte; public_check verifies parity. No CryptoKit fallback or runtime dependency is introduced." + "Private FIPS 180-4 SHA-256 is an original Swift implementation with checked cumulative production accounting, bounded scratch and inner cancellation. The separate trusted fixture tool mirrors its primitive core byte-for-byte; public_check verifies parity. No CryptoKit fallback or runtime dependency is introduced.", + "JSONHTML formatted rendering, original fixed CSS and shared strict-writer token classification are original Apache-2.0 code. No third-party renderer, browser numeric authority or runtime dependency is adapted." ], "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies." } diff --git a/Documentation/Architecture/Release1.3/Acceptance.json b/Documentation/Architecture/Release1.3/Acceptance.json index db1be33..7a8853b 100644 --- a/Documentation/Architecture/Release1.3/Acceptance.json +++ b/Documentation/Architecture/Release1.3/Acceptance.json @@ -6,85 +6,115 @@ "id": "JH01", "gate": "H0", "requirement": "The product/module is JSONHTML in the same package; its target depends on JSON and the exact public set contains five products.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "JH02", "gate": "H0", "requirement": "Forward product policy, manifest/CI assertions, consumers and test-count expectations are revised coherently; historical released records remain accurate.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "JH03", "gate": "H0", "requirement": "Data/String/report APIs, checked Sendable configuration, finite presets and asset strategy compile through a public consumer.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH04", "gate": "H1", "requirement": "All six root kinds, empty containers and mixed deep/wide structures produce a complete strict UTF-8 HTML document.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "JH05", "gate": "H1", "requirement": "Large integers, exact decimals, negative zero and admitted exponent/number spellings survive display and exported JSON without JS numeric conversion.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH06", "gate": "H1", "requirement": "Scalar-distinct composed/decomposed keys/strings and insertion/array order remain distinct and ordered.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "JH07", "gate": "H1", "requirement": "One noncopyable invocation meter covers admission, traversal, token helpers, paths, assets, Base64, annotations and result copies.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH08", "gate": "H1", "requirement": "Inclusive boundaries and checked overflow cover core work/output/retention and renderer node/DOM/document/outline/annotation capacities.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH09", "gate": "H1", "requirement": "Cancellation inside owned loops and valid restrictive/zero ceilings produce honest failures without a successful partial result/report.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "JH10", "gate": "H1", "requirement": "Repeated and concurrent calls have exclusive state; deterministic output uses stable IDs/assets and no clock/random mutation.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH11", "gate": "H1", "requirement": "Data and String materialization charges are complete; final-document byte count is distinguished from cumulative core helper output.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "JH12", "gate": "H2", "requirement": "Formatted mode has no script; interactive native disclosure remains usable with scripting disabled and allows multiple open branches.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH13", @@ -118,22 +148,31 @@ "id": "JH17", "gate": "H2", "requirement": "Script/style closing strings, ampersands, quotes, event-handler payloads and keys such as __proto__/constructor remain inert data.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH18", "gate": "H2", "requirement": "No payload-driven innerHTML, executable interpolation, URLs/selectors or script-element text assignment appears in the output/control path.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH19", "gate": "H2", "requirement": "Emitted CSS/JS asset hashes match CSP block bytes; controls work under policy and external resource requests remain absent.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH20", @@ -174,30 +213,42 @@ "id": "JH25", "gate": "H3", "requirement": "Five-product/platform/resource qualification; pure rendering does not execute UI frameworks, while JSONHTML may link narrowly isolated Apple PDF/printing frameworks.", - "status": "not_run", - "evidence": [], - "amendment": "Maintainer-approved transfer/export framework amendment; original JH25 remains historical." + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "amendment": "Maintainer-approved transfer/export framework amendment; original JH25 remains historical.", + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH26", "gate": "H3", "requirement": "Representative generation/output-expansion/retention and browser DOM/interactivity measurements justify finite preset values.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH27", "gate": "H4", "requirement": "Existing JSONWriter behavior, native/safety/docs lanes, public examples and baseline semantic/corpus cases remain qualified.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "JH28", "gate": "H4", "requirement": "Final feature branch/PR head/base, review disposition, source/asset hashes, actual receipts and release placement are recorded.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX01", @@ -212,23 +263,31 @@ "id": "TX02", "gate": "T0", "requirement": "Exact five-product manifest, forward policy and CI expectations; no sixth product or external root package dependency.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "TX03", "gate": "T0", "requirement": "Final public APIs, reports, errors, finite presets and representation builders compile with actual Swift 6.4 / SDK 27.", - "status": "not_run", - "evidence": [], - "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open.", + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX04", "gate": "T0", "requirement": "Explicit framework amendment; pure generation executes without WebKit; core JSON imports no WebKit/AppKit/UIKit.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX05", @@ -249,43 +308,59 @@ "id": "TX07", "gate": "T1", "requirement": "Legacy writer default/output/mutation/initializer behavior retained under spaces, compact and zero indentation.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "TX08", "gate": "T1", "requirement": "Tabs emitted directly by the cursor; checked 0...8 unit count and separate tab display width.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] }, { "id": "TX09", "gate": "T1", "requirement": "Shared helpers admit the input once and keep one noncopyable native preparation meter across selected formats.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX10", "gate": "T2", "requirement": "Exact large numbers, negative zero, exponent spellings and constructed-number fallback; insertion order preserved.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX11", "gate": "T2", "requirement": "Composed/decomposed keys and strings, empty/numeric/control/special keys preserve decoded scalar identity.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX12", "gate": "T2", "requirement": "All six root kinds, empty containers and mixed/deep/wide values are handled without recursive untrusted traversal.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX13", @@ -298,8 +373,11 @@ "id": "TX14", "gate": "T2", "requirement": "JSON/native JSON equality does not replace separate scalar/order/lexeme checks; no Foundation JSON authority.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX15", @@ -368,36 +446,51 @@ "id": "TX24", "gate": "T2", "requirement": "One native preparation call is all-or-nothing; failures install no partial artifact or successful report.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX25", "gate": "T3", "requirement": "Static transfer HTML is complete, expanded, highlighted and script-free with deterministic default theme.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX26", "gate": "T3", "requirement": "Static export omits controls/hidden disclosure/line numbers/outline/annotations by default; no JSON truncation.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX27", "gate": "T3", "requirement": "Escaping/CSP/assets hold for script/style delimiters, quotes, ampersands, Unicode/control and prototype-like keys.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX28", "gate": "T3", "requirement": "No external requests/fonts or browser numeric reconstruction; emitted CSS/CSP hashes match actual block bytes.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX29", @@ -417,22 +510,31 @@ "id": "TX31", "gate": "T3", "requirement": "Inclusive minus-one/boundary/plus-one and overflow probes for work/retention/output/metadata/style/HTML caps.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX32", "gate": "T3", "requirement": "Cancellation inside owned loops, result-copy failure and terminal latch behavior preserve accounting.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX33", "gate": "T3", "requirement": "Repeated and concurrent native calls are deterministic and invocation-owned; all state is checked Sendable.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX34", @@ -537,8 +639,11 @@ "id": "TX48", "gate": "T6", "requirement": "Five-product public consumers/resources and Apple framework boundary qualify Mac/iOS/iPadOS SDK/simulator/device cases.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX49", @@ -565,50 +670,65 @@ "id": "TX52", "gate": "T6", "requirement": "Native/safety/docs lanes pass on final exact candidate with deliberate nonzero suite/case counts and current base.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX53", "gate": "T6", "requirement": "Generation, combined preparation, RTF/PDF expansion and responsiveness measurements justify frozen finite profiles.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX54", "gate": "T6", "requirement": "Public docs/examples describe exactness, preparation, format selection, recipient and system-resource boundaries.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX55", "gate": "T6", "requirement": "Separate review pass findings resolved; final API/framework/limits choices and source/log/artifact identity returned.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." }, { "id": "TX56", "gate": "T6", "requirement": "Release slot reconciled; no main push/tag/release/messages under this brief alone; all incomplete rows explicit.", - "status": "not_run", - "evidence": [] + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." } ], "gate_status": { - "H0": "not_run", - "H1": "not_run", + "H0": "partial", + "H1": "partial", "H2": "not_run", - "H3": "not_run", - "H4": "not_run", - "T0": "not_run", - "T1": "not_run", - "T2": "not_run", - "T3": "not_run", + "H3": "partial", + "H4": "partial", + "T0": "partial", + "T1": "partial", + "T2": "partial", + "T3": "partial", "T4": "not_run", "T5": "not_run", - "T6": "not_run" + "T6": "partial" } } diff --git a/Documentation/Architecture/Release1.3/Decisions.md b/Documentation/Architecture/Release1.3/Decisions.md index ac14f7b..a0d1105 100644 --- a/Documentation/Architecture/Release1.3/Decisions.md +++ b/Documentation/Architecture/Release1.3/Decisions.md @@ -20,3 +20,28 @@ The initial SDK declaration probe typechecked DataRepresentation, exportingCondi The first probe was stopped by the default module-cache sandbox path before loading the standard library. The successful run used an invocation-owned explicit module-cache directory. No compatibility or unsafe compiler flag was added. Review findings resolved in this plan: captured base/release assignment is stale; fifth-product and Apple PDF amendments are explicit; PDF region export cannot establish pagination; default numeric policy requires measurements; all six formats remain in the intended objective; source/receiver/system work and historical qualification remain separate. + +## First formatted increment decisions + +The native compiler accepts the appended defaulted `JSONIndentationUnit` option. +The original cursor now emits package-only byte classifications and value-start +markers; its byte facade consumes exactly the same steps and accounting. JSONHTML +uses that cursor directly after one admission, with one noncopyable writing meter. +No second parser/formatter or temporary formatted JSON buffer is introduced. + +The initial public API is formatted-only: `JSONHTMLRenderer`, immutable facade +options, Data/String and successful report variants. It does not advertise +interactive or transfer behavior before implementation. HTML-specific failures +are latched into the same package meter before scratch cleanup. Final copies +charge overlapping logical ownership; successful core output counts HTML bytes. + +The fixed original CSS is a Swift constant. Its emitted bytes and CSP SHA-256 are +checked against an independent Python known answer. Print selectors explicitly +override all three theme classes, a specificity issue found during source review. +The HTML DOM count is an upper bound: 24 fixed nodes plus four per syntax or +whitespace run. Defaults provisionally reuse existing balanced node/output caps; +browser calibration is still required before final preset qualification. + +Actual JSON/JSONHTML modules and a public report consumer typecheck on iPhoneOS and +iPhoneSimulator SDK 27.2, Swift 6.4/language 6, with warnings as errors. This does +not qualify simulator/device execution, Linux/WASI runtime or transfer/PDF APIs. diff --git a/Documentation/Architecture/Release1.3/Increment.md b/Documentation/Architecture/Release1.3/Increment.md new file mode 100644 index 0000000..8774e50 --- /dev/null +++ b/Documentation/Architecture/Release1.3/Increment.md @@ -0,0 +1,75 @@ +# First JSONHTML increment + +Branch: `codex/1.3-jsonhtml`, isolated from the 1.2 Linux/WASI release work. +Base: `532f725740a4d61c32bc9870986d050b71f9db78`. + +This increment adds the working fifth product, JSONHTML, depending on JSON only. +Its formatted renderer produces complete expanded HTML for all six JSON roots. +It shares the strict writer cursor, preserves source numeric lexemes, insertion +order and scalar identity, escapes keys/values/titles, uses fixed original hashed +CSS and emits no scripts or external assets. Data/String/report APIs are checked +Sendable. The additive writer indentation option defaults to legacy spaces; +tabs are literal bytes, with separate HTML visual tab width. + +One invocation meter covers admission, title checks, cursor traversal, escaping, +assets and the final result copy. Additional HTML capacities bound complete bytes, +rendered values and a conservative DOM node upper bound. Failures return no partial +document/report and latch the meter before releasing owned scratch. Core failure +types and cancellation propagate. Logical retention remains distinct from RSS. + +## Verification + +Native environment: macOS 27.0 arm64, Xcode 27.2 (27B5019j), SDK 27.2, +Apple Swift 6.4 (swiftlang-6.4.0.34.1), language mode 6. Explicitly select this +Xcode with `DEVELOPER_DIR` and its macOS SDK with `SDKROOT`. + +The full initial native, safety and docs lanes passed with unchanged inputs. +After source review corrections, the committed source is refreshed through: + +``` +python3 -B Scripts/ci.py --lane native --output .build/ci/jsonhtml-native-final +python3 -B Scripts/ci.py --lane safety --output .build/ci/jsonhtml-safety-final +python3 -B Scripts/ci.py --lane docs --output .build/ci/jsonhtml-docs-final +``` + +Expected discovery is exact: 205 root functions, 173 parameterized invocations, +49 original cap probes, seven targeted shared-state functions, three tests each +in mobile and downstream consumers, and one qualification function. Independent +syntax: 318 observed, 287 evaluated successes, 31 strict/finite-policy dispositions. +Independent schema: 2337 observed, 2300 evaluated successes, 37 policy dispositions. +The rooted downstream consumer retains 20 cases (8 positive, 12 negative) and two +operational errors. Five DocC catalogues cover all 627 authored public declarations; +README examples compile and run. Actual command exits, per-case records, hashes +and source identities belong to the materialized lane receipts, not these counts. + +Six additional compiler commands pass for JSON, JSONHTML and a public report consumer +on iPhoneOS and iPhoneSimulator SDK 27.2 with warnings as errors. These are module +and interface typechecks, not device or simulator runtime passes. A supplemental +Python HTML parser checks exact body bytes, conservative DOM accounting and the CSS +CSP hash. It is not a browser runtime or accessibility/printing qualification. + +Release-native public-consumer measurements cover scalar, exact hostile input, +400-member and 80-level values, five timed samples after warmup. Reported logical +retention includes buffer/result overlap. Timings are observational under concurrent +host work, not idle-host comparisons or throughput guarantees. HTML defaults are +provisional finite policy; browser calibration remains pending. Existing performance +dispositions and the 10% review trigger remain unchanged. + +## Review and remaining gates + +Source review fixed print-rule specificity so light, dark and automatic themes all +use the intended print palette. It strengthened exact public-product target mapping +in CI, added independent CSS hash evidence and explicit hostile control-bearing key +bytes, and checked indentation's supported upper boundary. The failed exploratory +symbol-extraction command used an assumed module search path; actual SwiftPM graph +extraction succeeded through the docs lane. No unsupported compiler flag was added. The post-review CSS test caught a stale CSP +hash after the print rule changed; the exact emitted bytes were rehashed with +Python and the digest/length/CSP known answers corrected before committing. + +This is an intermediate implementation, not a 1.3 release candidate. Interactive +disclosure/search/outline/copy/download, source annotations, Transferable/selection, +Markdown/RTF/PDF preparation, browser matrix/accessibility/printing, final renderer +preset calibration, mobile runtime and integrated Linux/WASI qualification remain +open. The final qualified 1.2 source must be integrated before final 1.3 gates. +Physical-iPad deferral remains as approved. No release, tag, merge, protection change, +publication or sibling-source modification belongs to this increment. diff --git a/Documentation/Architecture/Release1.3/Plan.md b/Documentation/Architecture/Release1.3/Plan.md index 20db5f0..1fa05b5 100644 --- a/Documentation/Architecture/Release1.3/Plan.md +++ b/Documentation/Architecture/Release1.3/Plan.md @@ -1,6 +1,6 @@ # Swift-JSON 1.3.0 — JSONHTML and exact export -Status: maintainer-approved release direction; implementation and qualification pending. +Status: first formatted-rendering increment implemented and locally checked; complete 1.3 release qualification remains pending. 1.3.0 introduces **JSONHTML**, a fifth library product in the existing Swift-JSON package. It depends on JSON; JSON does not acquire a reverse dependency. The five-product policy becomes effective with the working target, consumers and matching CI checks. Historical four-product releases remain unchanged. JSONRegexRuntime remains private and root external dependencies remain empty. diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index 278765c..b29c015 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -1,7 +1,7 @@ { "schema_version": 1, "target_release": "1.3.0", - "status": "Plan and isolated feature branch prepared; implementation pending", + "status": "First formatted renderer increment implemented; full 1.3 gates remain partial", "actual_base": "532f725740a4d61c32bc9870986d050b71f9db78", "captured_briefing_base": "e9c6693c7cb29df5125e1699fef70f98229ccfdf", "feature_branch": "codex/1.3-jsonhtml", @@ -14,14 +14,18 @@ "JSONPatch", "JSONHTML" ], - "current_manifest_products": 4, + "current_manifest_products": 5, "qualified_scope": [ "Briefing integrity only", "Source/worktree/ruleset orientation", - "Three supported Apple SDK declaration probes only" + "Three supported Apple SDK declaration probes only", + "Formatted exact Data/String rendering and cumulative reports", + "16 new behavior functions; complete local native/safety/docs lanes", + "Five-product public consumer and 627 documented declarations", + "JSON/JSONHTML iPhoneOS and iPhoneSimulator module/public-interface typechecks only" ], "implementation_qualified": false, - "next_increment": "Same-package conformance/API probes, additive indentation units and tested formatted renderer/export path; introduce fifth target and exact checks with working source", + "next_increment": "Integrate the qualified 1.2 base when ready; continue scoped interactive rendering and export/compiler probes under separate gates", "open_decisions": [ "Measured finite renderer/export/PDF presets", "Executed PDF backend/pagination/defaults", @@ -48,5 +52,12 @@ "entries": 8 } ], - "updated_utc": "2026-10-13T10:48:57.258071+00:00" + "updated_utc": "2026-10-13T11:26:47.617801+00:00", + "evidence_locations": [ + "Scripts/ci.py --lane native --output .build/ci/jsonhtml-native-final", + "Scripts/ci.py --lane safety --output .build/ci/jsonhtml-safety-final", + "Scripts/ci.py --lane docs --output .build/ci/jsonhtml-docs-final", + "Documentation/Architecture/Release1.3/Increment.md" + ], + "final_source_refresh": "The final-source lane receipts are materialized privately after committing this source; initial full lanes already passed. Any final failure keeps that lane open." } diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index 7a201eb..b100ea3 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -8,12 +8,12 @@ are recorded; macOS/SDK 27 and Swift 6.4 remain minimums. Pushes to main, pull requests and manual dispatch run three lanes: - Native: public inventory, manifest/dependency graph, release build, debug and - release Swift Testing, four-product example, rooted JSON/JSONSchema consumer, + release Swift Testing, five-product example, rooted JSON/JSONSchema consumer, public cap tests and pinned independent suites under an external timeout. - Safety: full Address Sanitizer tests, targeted shared-state Thread Sanitizer tests, and strict memory safety compilation with warnings as errors. - Documentation: public symbol graphs, complete authored API/parameter comments, - executed README examples, four native DocC catalogs with warnings as errors, + executed README examples, five native DocC catalogs with warnings as errors, native static-hosting transforms and site route/asset checks. Actions are pinned by commit. Jobs have read-only repository permissions and diff --git a/Documentation/DocumentationTooling.md b/Documentation/DocumentationTooling.md index 46b6095..5278746 100644 --- a/Documentation/DocumentationTooling.md +++ b/Documentation/DocumentationTooling.md @@ -1,7 +1,7 @@ # Building and maintaining the DocC catalogs Swift source comments are the API documentation authority. Native compiler symbol -graphs supply the declarations and links for four Swift DocC catalogs. Each catalog +graphs supply the declarations and links for five Swift DocC catalogs. Each catalog combines the product reference with curated Topics and a focused usage guide. See the [Swift DocC documentation](https://www.swift.org/documentation/docc/). @@ -19,7 +19,7 @@ python3 Scripts/ci.py --lane docs The lane records native preflight, extracts fresh public symbol graphs, requires summaries and named-parameter documentation on every authored public declaration, checks README Swift fences against their executable consumer source, runs that -consumer, and compiles all four DocC catalogs with warnings as errors. Zero public +consumer, and compiles all five DocC catalogs with warnings as errors. Zero public symbols, missing comments, stale examples or a failed catalog fail the lane. | Output | Purpose | @@ -27,13 +27,14 @@ symbols, missing comments, stale examples or a failed catalog fail the lane. | `.build/handoff/docc/JSON.doccarchive` | JSON API and exact values/native coding guide | | `.build/handoff/docc/JSONSchema.doccarchive` | Schema API and offline validation guide | | `.build/handoff/docc/JSONStreaming.doccarchive` | Streaming API and completion/backpressure guide | +| `.build/handoff/docc/JSONHTML.doccarchive` | Exact formatted HTML API and rendering guide | | `.build/handoff/docc/JSONPatch.doccarchive` | Patch API and choosing/applying patches guide | | `.build/handoff/docc/symbols/` | Public compiler symbol graphs grouped by product | | `.build/handoff/docc/coverage.json` | Counts and documentation-coverage failures | | `.build/handoff/docc/receipt.json` | Commands, toolchain, counts, exits and diagnostics | | `.build/handoff/docc/compiled-catalogs.tar.gz` | All four compiled catalogs with their exact native filenames | | `.build/handoff/docc/compiled-catalogs.MANIFEST.sha256` | Independently verifiable hashes for every archived catalog file | -| `.build/handoff/pages/site/` | Static site, landing page and all four references under the project base path | +| `.build/handoff/pages/site/` | Static site, landing page and all five references under the project base path | | `.build/handoff/pages/receipt.json` | Native transformations, input/output hashes and route/asset verification | Open a `.doccarchive` in Xcode for the compiled reference. To preview a source @@ -62,7 +63,7 @@ The manifest and receipt preserve file-level and archive-level digests. The docs lane uses native `docc process-archive transform-for-static-hosting` to prepare each qualified archive under `/Swift-JSON/`. A shared landing -page links all four references and guides. The build verifies product/guide routes, +page links all five references and guides. The build verifies product/guide routes, local HTML links and assets, rejects private-path/credential markers and symlinks, and records a file manifest. Only the generated site is packaged for Pages; source graphs, command logs and private maintainer records are not website content. diff --git a/Documentation/Qualification/README.md b/Documentation/Qualification/README.md index 13618a0..28b5da6 100644 --- a/Documentation/Qualification/README.md +++ b/Documentation/Qualification/README.md @@ -52,6 +52,15 @@ and six shared-state safety functions. Both Linux architectures and the three Apple lanes qualify this combined source; historical counts above remain tied to their original snapshots. +The first 1.3 formatted HTML increment adds three shared-formatting functions and +13 JSONHTML functions: exactly 205 across six bundles (JSON 104, JSONSchema 51, +JSONRegexRuntime 12, JSONStreaming 14, JSONPatch 11, JSONHTML 13). All 173 +parameterized invocations, 49 core cap probes and prior corpus policies remain. +Thread Sanitizer checks seven shared-state functions. New HTML capacities have +separate inclusive boundary/overflow tests; they do not change the original 49. +See the [increment record](../Architecture/Release1.3/Increment.md) for scope and +remaining browser/platform/export qualification. + | Independent input | Observed policy | | --- | --- | | JSONTestSuite, pinned in FixtureRegistry.json | 318 discovered: 101 accepted, 186 rejected, 31 strict/finite-policy rejections | diff --git a/Documentation/README.md b/Documentation/README.md index b1c1010..f1c1f26 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -6,13 +6,14 @@ behavioral contracts and native qualification procedure. ## API references and guided reading -The four native DocC catalogs combine inline API documentation with focused guides: +The five native DocC catalogs combine inline API documentation with focused guides: | Product | Catalog source | Guide | | --- | --- | --- | | JSON | [JSON](../Sources/JSON/Documentation.docc/JSON.md) | [Exact values and native coding](../Sources/JSON/Documentation.docc/ExactValuesAndCoding.md) | | JSONSchema | [JSONSchema](../Sources/JSONSchema/Documentation.docc/JSONSchema.md) | [Offline schema validation](../Sources/JSONSchema/Documentation.docc/OfflineValidation.md) | | JSONStreaming | [JSONStreaming](../Sources/JSONStreaming/Documentation.docc/JSONStreaming.md) | [Completion and backpressure](../Sources/JSONStreaming/Documentation.docc/CompletionAndBackpressure.md) | +| JSONHTML | [JSONHTML](../Sources/JSONHTML/Documentation.docc/JSONHTML.md) | [Formatted rendering](../Sources/JSONHTML/Documentation.docc/FormattedRendering.md) | | JSONPatch | [JSONPatch](../Sources/JSONPatch/Documentation.docc/JSONPatch.md) | [Choosing and applying patches](../Sources/JSONPatch/Documentation.docc/ApplyingPatches.md) | [Browse the public documentation](https://thorfuchs.github.io/Swift-JSON/) or @@ -33,7 +34,7 @@ Compiled archives and public symbol graphs remain available in CI artifacts. ## Consumers, qualification and maintenance -- [Four-product compiled example](../Examples/Consumer/README.md) +- [Five-product compiled example](../Examples/Consumer/README.md) - [Downstream JSON/JSONSchema interface](ConsumerInterface.md) and [runnable public consumer](../Tools/MCPReadiness/README.md) - [Independent-suite policies and qualification](Qualification/README.md) - [Native CI lanes and receipts](CI/README.md) diff --git a/Examples/Consumer/Package.swift b/Examples/Consumer/Package.swift index 707f023..f0d9465 100644 --- a/Examples/Consumer/Package.swift +++ b/Examples/Consumer/Package.swift @@ -11,7 +11,8 @@ let package = Package( .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON"), .product(name: "JSONStreaming", package: "Swift-JSON"), - .product(name: "JSONPatch", package: "Swift-JSON") + .product(name: "JSONPatch", package: "Swift-JSON"), + .product(name: "JSONHTML", package: "Swift-JSON") ])], swiftLanguageModes: [.v6] ) diff --git a/Examples/Consumer/README.md b/Examples/Consumer/README.md index f0865e4..aa28ca5 100644 --- a/Examples/Consumer/README.md +++ b/Examples/Consumer/README.md @@ -15,3 +15,6 @@ Run `swift run --package-path Examples/Consumer Consumer` from the repo root. Each scenario must assert observable results and fail on an error. Compile-time visibility and runtime assertions together qualify public ergonomics. Examples use the public APIs documented in README and the product DocC catalogs. + +The fifth product, JSONHTML, additionally checks exact highlighted output, checked +Sendable reports, one admission and a cumulative work refusal using public APIs. diff --git a/Examples/Consumer/Sources/Consumer/main.swift b/Examples/Consumer/Sources/Consumer/main.swift index 0c14257..318c2dd 100644 --- a/Examples/Consumer/Sources/Consumer/main.swift +++ b/Examples/Consumer/Sources/Consumer/main.swift @@ -4,6 +4,7 @@ import JSON import JSONSchema import JSONStreaming import JSONPatch +import JSONHTML func requireSendable(_ value: T) {} let exact = try JSONNumber(parsing: "9007199254740993") @@ -29,6 +30,21 @@ do { precondition(builderFailure?.resource?.metric == .workUnits) precondition(builderFailure?.resource?.domain == .schemaCompilation) print("SCHEMA_BUILDER_CUMULATIVE_BUDGET passed limit=150 minimum_payload_scans=200") +let htmlRenderer = JSONHTMLRenderer(options: .init(title: "Public exact values", indentation: 1, indentationUnit: .tabs)) +requireSendable(htmlRenderer) +let htmlReport = try htmlRenderer.renderWithReport(value) +requireSendable(htmlReport) +precondition(htmlReport.statistics.renderedJSONNodes == 3) +precondition(htmlReport.statistics.operation[.nodes] == 3) +precondition(htmlReport.statistics.documentBytes == UInt64(htmlReport.result.count)) +precondition(String(decoding: htmlReport.result, as: UTF8.self).contains("9007199254740993")) +var htmlLimits = JSONHTMLLimits() +htmlLimits.core[.workUnits] = htmlReport.statistics.operation[.workUnits] - 1 +do { + _ = try JSONHTMLRenderer(options: .init(title: "Public exact values", indentation: 1, indentationUnit: .tabs, limits: htmlLimits)).render(value) + fatalError("HTML cumulative budget reset") +} catch let error as JSONFailure { precondition(error.resource?.metric == .workUnits) } +print("JSONHTML_PUBLIC_CONSUMER passed roots=1 exact_number=1 cumulative_refusal=1") try runREADMEExamples() try await runProductExamples() print("Consumer passed: exact values/Codable/Pointer, Patch/Merge Patch, schema documents/registry, streams/sink, reports and Sendable; immutable schema compilation/evaluation/output/inspection") diff --git a/Package.swift b/Package.swift index c372695..8bdfcff 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,8 @@ let package = Package( .library(name: "JSON", targets: ["JSON"]), .library(name: "JSONSchema", targets: ["JSONSchema"]), .library(name: "JSONStreaming", targets: ["JSONStreaming"]), - .library(name: "JSONPatch", targets: ["JSONPatch"]) + .library(name: "JSONPatch", targets: ["JSONPatch"]), + .library(name: "JSONHTML", targets: ["JSONHTML"]) ], targets: [ .target(name: "JSON"), @@ -17,6 +18,8 @@ let package = Package( .target(name: "JSONSchema", dependencies: ["JSON", "JSONRegexRuntime"], resources: [.copy("Resources/Draft2020-12")]), .target(name: "JSONStreaming", dependencies: ["JSON"]), .target(name: "JSONPatch", dependencies: ["JSON"]), + .target(name: "JSONHTML", dependencies: ["JSON"]), + .testTarget(name: "JSONHTMLTests", dependencies: ["JSONHTML", "JSON"]), .testTarget(name: "JSONTests", dependencies: ["JSON"], exclude: ["ImplementationPlan.md"]), .testTarget(name: "JSONSchemaTests", dependencies: ["JSONSchema"], exclude: ["ImplementationPlan.md"]), .testTarget(name: "JSONStreamingTests", dependencies: ["JSONStreaming"], exclude: ["ImplementationPlan.md"]), diff --git a/README.md b/README.md index 12baf94..3b68967 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,10 @@ Choose the products your application needs: | **JSON** | Exact values, strict parsing/writing, native Codable, Pointer, presence, checked construction, source documents and diagnostics | | **JSONSchema** | Draft 2020-12 compilation/evaluation, offline registries, bounded regex, annotations and raw declaration inspection | | **JSONStreaming** | Pull syntax events and values, strict NDJSON, root-array elements and awaited output chunks | +| **JSONHTML** | Exact, highlighted, script-free standalone HTML with cumulative reports (1.3 development) | | **JSONPatch** | Staged RFC 6902 JSON Patch and RFC 7396 Merge Patch | -`JSONSchema`, `JSONStreaming` and `JSONPatch` build on `JSON`. The package-private +`JSONSchema`, `JSONStreaming`, `JSONPatch` and `JSONHTML` build on `JSON`. The package-private `JSONRegexRuntime` is an implementation detail and has no public product. The root SwiftPM dependency graph is empty. Qualification and benchmark tools are separate packages. @@ -90,9 +91,9 @@ Then add products to your target dependencies: .product(name: "JSONSchema", package: "swift-json") ``` -Add `JSONStreaming` and `JSONPatch` in the same way when needed. A local checkout can +Add `JSONStreaming`, `JSONPatch` and the 1.3 development product `JSONHTML` in the same way when needed. A local checkout can use `.package(path: "../Swift-JSON")`. The requirement above accepts compatible 1.x -releases. See the [four-product consumer](Examples/Consumer) for a complete buildable +releases. See the [five-product consumer](Examples/Consumer) for a complete buildable manifest. The website tracks `main`; release assets include compiled DocC catalogues for the exact released source. @@ -339,6 +340,17 @@ hard RSS or throughput guarantees. Read the [resource contract](Documentation/Ar [machine-readable profiles](Documentation/Architecture/ResourceProfiles.json) and [49-cap ownership matrix](Documentation/Qualification/LimitsMatrix.json). +## Formatted HTML (1.3 development) + +The additive `JSONHTML` product renders complete highlighted HTML from the same +exact writer cursor. It preserves number lexemes and insertion order, escapes +payload text and includes fixed CSS under a hash-based Content Security Policy. +Every call shares one finite budget through admission, formatting and final copying. +The [formatted rendering guide](Sources/JSONHTML/Documentation.docc/FormattedRendering.md) +describes its API and provisional HTML capacities. Interactive controls, transfer +formats, RTF and PDF remain later increments. Existing published versions do not +include this product. + ## Documentation and examples Browse the [public documentation site](https://thorfuchs.github.io/Swift-JSON/): @@ -348,11 +360,11 @@ Browse the [public documentation site](https://thorfuchs.github.io/Swift-JSON/): [JSONPatch](https://thorfuchs.github.io/Swift-JSON/JSONPatch/documentation/jsonpatch/). The site tracks `main` and publishes only after all three native CI lanes pass. -The [documentation index](Documentation/README.md) links the behavior contracts. The four DocC catalogs +The [documentation index](Documentation/README.md) links the behavior contracts. The five DocC catalogs provide public API references, focused guides, parameter descriptions and failure contracts, compiled from the native Swift public symbol graphs. -Build all four catalogs on a supported native host: +Build all five catalogs on a supported native host: ```sh python3 Scripts/ci.py --lane docs diff --git a/Scripts/README.md b/Scripts/README.md index e54a11e..6e58671 100644 --- a/Scripts/README.md +++ b/Scripts/README.md @@ -9,10 +9,10 @@ separate from the dependency-free library. Native lanes fail on unsupported host | `python3 Scripts/preflight.py` | Actual native OS/compiler/SDK receipt | | `python3 Scripts/ci.py --lane native` | Manifests, builds, Swift Testing, consumers and suites | | `python3 Scripts/ci.py --lane safety` | Address/Thread Sanitizer and strict memory safety | -| `python3 Scripts/ci.py --lane linux` | Actual Linux preflight, all four products, exact root/cap/corpus/consumer counts | +| `python3 Scripts/ci.py --lane linux` | Actual Linux preflight, all five products, exact root/cap/corpus/consumer counts | | `python3 Scripts/ci.py --lane linux-safety` | Linux Address/Thread Sanitizer and strict memory safety | | `python3 Scripts/qualify_linux_reproduction.py --output PATH` | Actual source archive, byte-equal rebuild, clean extraction, build/tests and public consumer | -| `python3 Scripts/ci.py --lane docs` | Four DocC catalogs, authored API/parameter coverage, executed README examples and verified static Pages site | +| `python3 Scripts/ci.py --lane docs` | Five DocC catalogs, authored API/parameter coverage, executed README examples and verified static Pages site | | `python3 Scripts/build_documentation_site.py` | Transform already-qualified DocC archives and verify static routes/assets | | `python3 Scripts/qualification_watchdog.py --timeout 900 --output PATH -- COMMAND` | Process-group timeout and native receipt | | `python3 Scripts/archive_candidate.py --output PATH` | Public source archive with exact hashes and byte-equal rebuild | diff --git a/Scripts/build_documentation.py b/Scripts/build_documentation.py index 777c625..e76d51b 100644 --- a/Scripts/build_documentation.py +++ b/Scripts/build_documentation.py @@ -60,7 +60,7 @@ def package_catalogs(output): 'bytes': archive.stat().st_size, 'verified_files': len(records), 'manifest': str(manifest.relative_to(ROOT)), 'manifest_sha256': hashlib.sha256(manifest.read_bytes()).hexdigest(), - 'policy': 'Four native DocC archives; exact internal filenames, sorted entries and normalized tar/gzip metadata.'} + 'policy': 'Five native DocC archives; exact internal filenames, sorted entries and normalized tar/gzip metadata.'} def main(): diff --git a/Scripts/build_documentation_site.py b/Scripts/build_documentation_site.py index 75e9831..8e81e41 100644 --- a/Scripts/build_documentation_site.py +++ b/Scripts/build_documentation_site.py @@ -23,12 +23,14 @@ GUIDES = {'JSON': ('Exact values and native coding', 'exactvaluesandcoding'), 'JSONSchema': ('Offline schema validation', 'offlinevalidation'), 'JSONStreaming': ('Completion and backpressure', 'completionandbackpressure'), - 'JSONPatch': ('Choosing and applying patches', 'applyingpatches')} + 'JSONPatch': ('Choosing and applying patches', 'applyingpatches'), + 'JSONHTML': ('Exact formatted HTML', 'formattedrendering')} DESCRIPTIONS = { 'JSON': 'Exact values, strict UTF-8, native Codable, Pointer, presence and finite resource reports.', 'JSONSchema': 'Draft 2020-12, explicit offline registries, immutable plans, annotations and bounded regex.', 'JSONStreaming': 'Pull events and values, strict NDJSON, root-array completion and awaited output chunks.', - 'JSONPatch': 'Transactional RFC 6902 JSON Patch and RFC 7396 Merge Patch with exact value semantics.'} + 'JSONPatch': 'Transactional RFC 6902 JSON Patch and RFC 7396 Merge Patch with exact value semantics.', + 'JSONHTML': 'Exact formatted, highlighted standalone HTML with finite cumulative accounting.'} class Links(HTMLParser): @@ -69,7 +71,7 @@ def landing(commit): return f''' Swift-JSON Documentation - +

", meter: &meter) + try output.escaped(options.title, meter: &meter) + try output.append("

", meter: &meter)
+        let writing = JSONWritingOptions(limits: options.limits.core, indentation: options.indentation,
+                                         numberStyle: .preserved, indentationUnit: options.indentationUnit)
+        var cursor = JSONWritingCursor(value: value, options: writing)
+        var active: JSONWritingTokenKind?
+        while let token = try cursor.nextToken(meter: &meter) {
+            if token.beginsValue { try output.addNode(meter: &meter) }
+            if token.kind != active {
+                if let active, active != .whitespace { try output.append("", meter: &meter) }
+                // Four is a conservative upper bound for one span/text run or whitespace run.
+                try output.addDOM(4, meter: &meter)
+                if token.kind != .whitespace {
+                    try output.append("", meter: &meter)
+                }
+                active = token.kind
+            }
+            try output.escaped(token.byte, meter: &meter)
+        }
+        if let active, active != .whitespace { try output.append("", meter: &meter) }
+        try output.append("
\n", meter: &meter) + try meter.checkpoint() + return output + } +} + +package struct JSONHTMLDocumentBuffer { + package var bytes: [UInt8] = [] + private var nodes: UInt64 = 0 + private var dom: UInt64 = 0 + private let limits: JSONHTMLLimits + package init(limits: JSONHTMLLimits) { self.limits = limits } + package static func require(_ metric: JSONHTMLResourceMetric, _ count: UInt64, _ limit: UInt64) throws { + guard count <= limit else { throw JSONHTMLFailure.limitExceeded(metric, limit: limit, attempted: count) } + } + package static func adding(_ metric: JSONHTMLResourceMetric, _ count: UInt64, _ amount: UInt64, _ limit: UInt64) throws -> UInt64 { + let (next, overflow) = count.addingReportingOverflow(amount) + guard !overflow else { throw JSONHTMLFailure.counterOverflow(metric) } + try require(metric, next, limit) + return next + } + package mutating func addDOM(_ amount: UInt64, meter: inout JSONWorkMeter) throws { + try meter.work() + dom = try Self.adding(.domNodes, dom, amount, limits.maxDOMNodes) + } + package mutating func addNode(meter: inout JSONWorkMeter) throws { + try meter.work() + nodes = try Self.adding(.renderedJSONNodes, nodes, 1, limits.maxRenderedJSONNodes) + } + package mutating func append(_ text: String, meter: inout JSONWorkMeter) throws { + for byte in text.utf8 { try append(byte, meter: &meter) } + } + private mutating func append(_ byte: UInt8, meter: inout JSONWorkMeter) throws { + try meter.work() + _ = try Self.adding(.documentBytes, UInt64(bytes.count), 1, limits.maxDocumentBytes) + try meter.charge(.outputBytes); try meter.retain(1) + bytes.append(byte) + } + package mutating func escaped(_ text: String, meter: inout JSONWorkMeter) throws { + for byte in text.utf8 { try meter.work(); try escaped(byte, meter: &meter) } + } + package mutating func escaped(_ byte: UInt8, meter: inout JSONWorkMeter) throws { + try meter.work() + switch byte { + case 38: try append("&", meter: &meter) + case 60: try append("<", meter: &meter) + case 62: try append(">", meter: &meter) + case 34: try append(""", meter: &meter) + case 39: try append("'", meter: &meter) + default: try append(byte, meter: &meter) + } + } + package func statistics(_ operation: JSONOperationStatistics) -> JSONHTMLStatistics { + .init(operation: operation, nodes: nodes, dom: dom, bytes: UInt64(bytes.count)) + } +} diff --git a/Sources/JSONHTML/JSONHTMLRenderingOptions.swift b/Sources/JSONHTML/JSONHTMLRenderingOptions.swift new file mode 100644 index 0000000..4d7858b --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLRenderingOptions.swift @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// The color policy for a standalone formatted document. +public enum JSONHTMLTheme: String, Sendable { + /// Follow the browser's light or dark preference. + case automatic + /// Use light colors independently of browser preference. + case light + /// Use dark colors independently of browser preference. + case dark +} + +/// Additional inclusive ceilings for formatted HTML generation. +/// +/// Defaults reuse the balanced core node and output ceilings as initial finite +/// policy. They are not measured browser heap, CPU or process RSS guarantees. +public struct JSONHTMLLimits: Sendable { + /// The one cumulative core budget for admission, formatting, HTML and result copying. + public var core: JSONLimits + /// Maximum JSON values rendered; keys are not separate values. + public var maxRenderedJSONNodes: UInt64 + /// Maximum conservative DOM node upper bound, including fixed document structure. + public var maxDOMNodes: UInt64 + /// Maximum bytes in the complete UTF-8 HTML document. + public var maxDocumentBytes: UInt64 + + /// Creates finite inclusive rendering limits. + /// + /// Zero forbids use. Values must fit supported Int capacity at operation entry. + /// - Parameters: + /// - core: Shared core capacities and cumulative work quotas. + /// - maxRenderedJSONNodes: Inclusive count of rendered JSON values. + /// - maxDOMNodes: Inclusive conservative DOM node upper bound. + /// - maxDocumentBytes: Inclusive complete HTML byte ceiling. + public init(core: JSONLimits = .balanced, + maxRenderedJSONNodes: UInt64 = JSONLimits.balanced[.nodes], + maxDOMNodes: UInt64 = JSONLimits.balanced[.nodes], + maxDocumentBytes: UInt64 = JSONLimits.balanced[.outputBytes]) { + self.core = core + self.maxRenderedJSONNodes = maxRenderedJSONNodes + self.maxDOMNodes = maxDOMNodes + self.maxDocumentBytes = maxDocumentBytes + } + package func validate() throws { + try core.validate() + guard maxRenderedJSONNodes < UInt64(Int.max), maxDOMNodes < UInt64(Int.max), + maxDocumentBytes < UInt64(Int.max) else { throw JSONHTMLFailure.invalidConfiguration } + } +} + +/// Immutable-facade configuration for expanded, highlighted, script-free HTML. +public struct JSONHTMLRenderingOptions: Sendable { + /// Plain document title; control characters are rejected and HTML is escaped. + public var title: String + /// Automatic, light or dark color policy. + public var theme: JSONHTMLTheme + /// Indentation units per level, from zero through eight. + public var indentation: Int + /// Literal space or horizontal-tab indentation units. + public var indentationUnit: JSONIndentationUnit + /// Browser display width of a tab, from one through eight; JSON bytes do not change. + public var tabWidth: Int + /// Finite shared core and HTML capacities. + public var limits: JSONHTMLLimits + + /// Creates the formatted-rendering configuration. + /// + /// Insertion order and preserved numeric lexemes are always used. Constructed + /// numbers without a source lexeme use exact normalized decimal spelling. + /// - Parameters: + /// - title: Escaped plain title, also shown above the complete JSON body. + /// - theme: Browser color policy. + /// - indentation: Indentation unit count per nesting level, zero through eight. + /// - indentationUnit: Space units by default, or literal tabs. + /// - tabWidth: Visual tab width, one through eight. + /// - limits: Inclusive core and HTML capacities and work quotas. + public init(title: String = "JSON", theme: JSONHTMLTheme = .automatic, + indentation: Int = 2, indentationUnit: JSONIndentationUnit = .spaces, + tabWidth: Int = 4, limits: JSONHTMLLimits = .init()) { + self.title = title; self.theme = theme; self.indentation = indentation + self.indentationUnit = indentationUnit; self.tabWidth = tabWidth; self.limits = limits + } +} + +/// Stable identifiers for the three additional HTML capacities. +public enum JSONHTMLResourceMetric: String, Sendable { + /// JSON values in the formatted body. + case renderedJSONNodes + /// Conservative upper bound of document DOM nodes. + case domNodes + /// Complete HTML UTF-8 bytes. + case documentBytes +} + +/// Bounded HTML-specific failures; core resource failures and cancellation propagate unchanged. +public enum JSONHTMLFailure: Error, Sendable, Equatable { + /// Invalid indentation, tab width, capacity or control-bearing title. + case invalidConfiguration + /// An inclusive HTML capacity was exceeded before growth. + case limitExceeded(JSONHTMLResourceMetric, limit: UInt64, attempted: UInt64) + /// Checked HTML counter addition overflowed. + case counterOverflow(JSONHTMLResourceMetric) +} diff --git a/Tests/JSONHTMLTests/FormattedRendererTests.swift b/Tests/JSONHTMLTests/FormattedRendererTests.swift new file mode 100644 index 0000000..52cd146 --- /dev/null +++ b/Tests/JSONHTMLTests/FormattedRendererTests.swift @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +@testable import JSONHTML + +// Independent text extraction: fixed span markup is removed before decoding entities. +private func body(_ html: String) throws -> [UInt8] { + let start = try #require(html.range(of: "
"))
+    let end = try #require(html.range(of: "
", range: start.upperBound..") { + text.removeSubrange(span.lowerBound...close) + } + text = text.replacingOccurrences(of: "", with: "") + for (entity, decoded) in [("<", "<"), (">", ">"), (""", "\""), ("'", "'"), ("&", "&")] { + text = text.replacingOccurrences(of: entity, with: decoded) + } + return Array(text.utf8) +} + +@Test func formattedHTMLHandlesSixRootsAndEmptyContainers() throws { + for text in ["null", "false", "9007199254740993", #""é\u0000""#, "[true,null]", #"{"a":1}"#, "[]", "{}"] { + let root = try JSONParser().parse(text) + let html = try JSONHTMLRenderer().renderString(root) + #expect(html.hasPrefix("")) + #expect(html.hasSuffix("\n")) + #expect(!html.contains("&\"'") + let html = try JSONHTMLRenderer(options: options).renderString(.string(payload)) + #expect(!html.contains("\n", meter: &meter) + try meter.checkpoint() + return output + } + + private func nativeBytes(_ value: JSON, options: JSONWritingOptions, meter: inout JSONWorkMeter) throws -> [UInt8] { + var cursor = JSONWritingCursor(value: value, options: options), bytes: [UInt8] = [] + while let byte = try cursor.next(meter: &meter) { + try meter.work(); try meter.charge(.outputBytes); try meter.retain(1); bytes.append(byte) + } + return bytes + } + private func highlighted(_ value: JSON, options: JSONWritingOptions, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws { + var cursor = JSONWritingCursor(value: value, options: options) + var active: JSONWritingTokenKind? + while let token = try cursor.nextToken(meter: &meter) { + if token.kind != active { + if let active, active != .whitespace { try output.append("", meter: &meter) } + try output.addDOM(4, meter: &meter) + if token.kind != .whitespace { + try output.append("", meter: &meter) + } + active = token.kind + } + try output.escaped(token.byte, meter: &meter) + } + if let active, active != .whitespace { try output.append("", meter: &meter) } + } + private func label(_ component: JSONLocationComponent?, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter, searchable: Bool) throws { + switch component { + case .key(let key): + try output.append(searchable ? "" : "", meter: &meter) + var cursor = JSONWritingCursor(value: .string(key.string), options: .init(numberStyle: .preserved)) + while let byte = try cursor.next(meter: &meter) { try output.escaped(byte, meter: &meter) } + try output.append("", meter: &meter) + case .index(let index): + try output.append("[", meter: &meter); try output.decimal(UInt64(index), meter: &meter); try output.append("]", meter: &meter) + case nil: try output.append("Root", meter: &meter) + } + } + private func pointerBytes(_ path: [JSONLocationComponent], meter: inout JSONWorkMeter) throws -> [UInt8] { + try meter.require(.pointerTokens, UInt64(path.count)) + var result: [UInt8] = [] + func emit(_ byte: UInt8, meter: inout JSONWorkMeter) throws { + try meter.work(); try meter.require(.pointerBytes, UInt64(result.count) + 1) + try meter.charge(.outputBytes); try meter.retain(1); result.append(byte) + } + for component in path { + try emit(47, meter: &meter) + let token: String + switch component { + case .key(let key): token = key.string + case .index(let index): try meter.retain(20); try meter.work(20); token = String(index) + } + for byte in token.utf8 { + try meter.work() + if byte == 126 || byte == 47 { try emit(126, meter: &meter); try emit(byte == 126 ? 48 : 49, meter: &meter) } + else { try emit(byte, meter: &meter) } + } + if case .index = component { try meter.release(20) } + } + return result + } +} + +private enum InteractiveStep { + case node(JSON, JSONLocationComponent?, Int) + case close(Bool, Bool) +} +private struct OutlineEntry { + let ordinal: UInt64 + let component: JSONLocationComponent? + let level: Int +} + +extension JSONHTMLDocumentBuffer { + // Reserve the complete fixed decimal scratch before any owned formatting. + package mutating func decimal(_ value: UInt64, meter: inout JSONWorkMeter) throws { + try meter.retain(20) + var remainder = value, digits: [UInt8] = [] + repeat { try meter.work(); digits.append(UInt8(remainder % 10) + 48); remainder /= 10 } while remainder > 0 + for byte in digits.reversed() { try appendByte(byte, meter: &meter) } + try meter.release(20) + } + package mutating func base64(_ bytes: [UInt8], meter: inout JSONWorkMeter) throws { + let table = JSONHTMLInteractiveAssets.base64Alphabet + var index = 0 + while index < bytes.count { + try meter.work() + let a = bytes[index], b = index + 1 < bytes.count ? bytes[index + 1] : 0, c = index + 2 < bytes.count ? bytes[index + 2] : 0 + try meter.retain(4) + for byte in [table[Int(a >> 2)], table[Int((a & 3) << 4 | b >> 4)], index + 1 < bytes.count ? table[Int((b & 15) << 2 | c >> 6)] : 61, index + 2 < bytes.count ? table[Int(c & 63)] : 61] { + try meter.work(); try addMetadata(meter: &meter); try appendByte(byte, meter: &meter) + } + try meter.release(4) + index += min(3, bytes.count - index) + } + } +} diff --git a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift new file mode 100644 index 0000000..d36f0ed --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: Apache-2.0 +// Generated from Assets; verify with Scripts/verify_html_assets.py. +package enum JSONHTMLInteractiveAssets { + package static let base64Alphabet: [UInt8] = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".utf8) + package static let css = #""" +/* SPDX-License-Identifier: Apache-2.0; Swift-JSON interactive asset v1 */ +.interactive .toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}.interactive button,.interactive input{font:inherit;padding:.35rem .55rem;border:1px solid currentColor;border-radius:.3rem;background:var(--bg);color:var(--fg)}.interactive :focus-visible{outline:3px solid var(--num);outline-offset:3px}.interactive .layout{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:1.5rem}.interactive .layout:not(:has(.outline)){grid-template-columns:minmax(0,1fr)}.interactive .outline{max-height:70vh;overflow:auto;position:sticky;top:1rem;align-self:start}.interactive a{color:var(--num);overflow-wrap:anywhere}.interactive ul{padding-left:1.1rem}.interactive .node{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.7;overflow-wrap:anywhere}.interactive .children{padding-left:1.25rem;border-left:1px solid var(--fg);margin-left:.35rem}.interactive summary{cursor:pointer}.interactive .pointer-label{font-size:.8em;opacity:.85;margin-inline:.5rem}.interactive .pointer-actions{display:inline-flex;gap:.3rem;flex-wrap:wrap}.interactive .pointer-actions button{font-size:.75rem;padding:.1rem .3rem}.interactive .search-hit{outline:2px solid var(--num);outline-offset:2px}.interactive #jh-status{min-height:1.7rem}.interactive .manual-copy textarea{display:block;width:100%;min-height:8rem;font-family:monospace}.interactive .print-json{display:none}.interactive [hidden]{display:none!important}.interactive code{white-space:pre-wrap}.interactive .search-results{max-height:16rem;overflow:auto}.interactive .kind{font-weight:600}@media(max-width:700px){body.interactive{padding:1rem}.interactive .layout{grid-template-columns:1fr}.interactive .outline{position:static;max-height:15rem}.interactive .pointer-actions{display:flex}.interactive .children{padding-left:.75rem}}@media print{.interactive .toolbar,.interactive .layout,.interactive .manual-copy,.interactive #jh-status,.interactive .search-results,.interactive noscript{display:none!important}.interactive .print-json{display:block!important}.interactive .print-json pre{white-space:pre-wrap;overflow:visible}} +"""# + package static let script = #""" +// SPDX-License-Identifier: Apache-2.0 +// Swift-JSON interactive asset v1. UI text only; never a browser JSON model. +(() => { + 'use strict'; + const tree = document.getElementById('jh-tree'); + const status = document.getElementById('jh-status'); + const query = document.getElementById('jh-query'); + const results = document.getElementById('jh-results'); + const manual = document.getElementById('jh-manual'); + const manualText = document.getElementById('jh-manual-text'); + const CHUNK = 128; + const MAX_RESULTS = 100; + let searchEpoch = 0; + let bulkEpoch = 0; + const bytes = encoded => { + const text = atob(encoded); + const result = new Uint8Array(text.length); + for (let i = 0; i < text.length; i++) result[i] = text.charCodeAt(i); + return result; + }; + const text = encoded => new TextDecoder('utf-8', {fatal: true}).decode(bytes(encoded)); + const say = message => { status.textContent = message; }; + const reveal = (id, focus = true) => { + const node = document.getElementById(id); + if (!node || !tree.contains(node)) return; + for (let parent = node.parentElement; parent; parent = parent.parentElement) { + if (parent.tagName === 'DETAILS') parent.open = true; + } + if (node.tagName === 'DETAILS') node.open = true; + node.focus({preventScroll: true}); + node.scrollIntoView({block: 'nearest'}); + }; + const download = (encoded, filename) => { + const url = URL.createObjectURL(new Blob([bytes(encoded)], {type: filename.endsWith('.txt') ? 'text/plain;charset=utf-8' : 'application/json;charset=utf-8'})); + const link = document.createElement('a'); + link.href = url; link.download = filename; + document.body.append(link); link.click(); link.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + say('Download requested: ' + filename + '.'); + }; + const copy = async (encoded, label) => { + const value = text(encoded); + manual.hidden = true; + try { + if (!navigator.clipboard || !navigator.clipboard.writeText) throw new Error('Clipboard unavailable'); + await navigator.clipboard.writeText(value); + say('Browser accepted ' + label + ' copy. Platform clipboard transformations may differ.'); + } catch (_) { + manual.hidden = false; manualText.value = value; + manualText.focus(); manualText.select(); + say('Clipboard unavailable or rejected. Select the text and copy manually, or download exact UTF-8 bytes.'); + } + }; + const bulk = open => { + const epoch = ++bulkEpoch; + const nodes = tree.querySelectorAll('details.node'); + let i = 0; + const step = () => { + if (epoch !== bulkEpoch) return; + const end = Math.min(i + CHUNK, nodes.length); + for (; i < end; i++) nodes[i].open = open; + if (i < nodes.length) setTimeout(step, 0); + else say((open ? 'Expanded ' : 'Collapsed ') + nodes.length + ' containers.'); + }; + step(); + }; + // Full DOM remains present. Scanning is chunked, not a browser CPU/heap quota. + const search = () => { + const epoch = ++searchEpoch; + results.replaceChildren(); + const needle = query.value; + const byteLimit = Number(query.dataset.byteLimit); + let issue = ''; + if (needle.length > byteLimit) issue = 'Search query exceeds the UTF-8 byte limit.'; + else { + for (const scalar of needle) { + const cp = scalar.codePointAt(0); + if (cp >= 0xd800 && cp <= 0xdfff) issue = 'Search requires Unicode scalar text.'; + } + if (new TextEncoder().encode(needle).length > byteLimit) issue = 'Search query exceeds the UTF-8 byte limit.'; + } + const accepted = issue ? '' : needle; + const candidates = tree.querySelectorAll('.node'); + let i = 0, matches = 0, listed = 0; + const step = () => { + if (epoch !== searchEpoch) return; + const end = Math.min(i + CHUNK, candidates.length); + for (; i < end; i++) { + const node = candidates[i]; + const fields = node.tagName === 'DETAILS' ? node.querySelector('summary').querySelectorAll('[data-search]') : node.querySelectorAll(':scope > [data-search]'); + let matched = false; + for (const field of fields) { if (accepted.length > 0 && field.textContent.includes(accepted)) matched = true; } + node.classList.toggle('search-hit', matched); + if (!matched) continue; + matches++; + if (listed < MAX_RESULTS) { + const item = document.createElement('li'); + const link = document.createElement('a'); + link.href = '#' + node.id; + link.dataset.target = node.id; + link.textContent = 'Match ' + matches + ': ' + text(node.dataset.pointerEscaped); + item.append(link); results.append(item); listed++; + } + } + if (i < candidates.length) setTimeout(step, 0); + else { + say(issue || matches + ' matching nodes in rendered JSON key/value text. ' + listed + ' results listed (maximum ' + MAX_RESULTS + ').'); + const first = results.querySelector('a'); + if (first) reveal(first.dataset.target, false); + } + }; + step(); + }; + document.getElementById('jh-toolbar').hidden = false; + query.addEventListener('input', search); + document.addEventListener('click', event => { + const link = event.target.closest('a[data-target]'); + if (link) { event.preventDefault(); reveal(link.dataset.target); return; } + const button = event.target.closest('button[data-action]'); + if (!button) return; + event.preventDefault(); event.stopPropagation(); + const action = button.dataset.action; + if (action === 'expand') { bulk(true); return; } + if (action === 'collapse') { bulk(false); return; } + if (action === 'clear') { query.value = ''; search(); return; } + if (action === 'select') { manualText.focus(); manualText.select(); return; } + if (action === 'copy-json' || action === 'download-json') { + const encoded = document.getElementById('jh-copy-json').dataset.bytes; + if (action === 'copy-json') void copy(encoded, 'JSON'); + else download(encoded, 'document.json'); + return; + } + const node = document.getElementById(button.dataset.node); + if (!node) return; + if (action === 'copy-pointer') void copy(node.dataset.pointer, 'raw RFC 6901 Pointer'); + else if (action === 'copy-pointer-escaped') void copy(node.dataset.pointerEscaped, 'Pointer JSON string'); + else if (action === 'download-pointer') download(node.dataset.pointer, 'pointer.txt'); + }); + say('Interactive controls ready. Search scope: rendered JSON key/value text.'); +})(); +"""# + package static let styleHash = "ZViqigi/3L6QAgh4IHgRZSMK0MTDXXYvGytdlYTJfoQ=" + package static let scriptHash = "FhFTH1Ne5Y99evqwxUMK6h6n0tLakTWXAEcr/iz8KMg=" +} diff --git a/Sources/JSONHTML/JSONHTMLRenderReport.swift b/Sources/JSONHTML/JSONHTMLRenderReport.swift index d243a7f..03638cd 100644 --- a/Sources/JSONHTML/JSONHTMLRenderReport.swift +++ b/Sources/JSONHTML/JSONHTMLRenderReport.swift @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 import JSON -/// Successful counts from one complete formatted rendering invocation. +/// Successful counts from one complete HTML rendering invocation. public struct JSONHTMLStatistics: Sendable, Equatable { /// Shared admission, token formatting, escaping, assets and final-copy accounting. public let operation: JSONOperationStatistics @@ -11,9 +11,16 @@ public struct JSONHTMLStatistics: Sendable, Equatable { public let domNodeUpperBound: UInt64 /// Complete document bytes; final result copies do not add output bytes. public let documentBytes: UInt64 - package init(operation: JSONOperationStatistics, nodes: UInt64, dom: UInt64, bytes: UInt64) { + /// Emitted container outline entries; zero in formatted mode or when disabled. + public let outlineEntries: UInt64 + /// Emitted Base64 metadata bytes; distinct from complete document and raw copy bytes. + public let metadataBytes: UInt64 + /// Raw exact JSON bytes embedded for interactive copy/download; zero in formatted mode. + public let copyJSONBytes: UInt64 + package init(operation: JSONOperationStatistics, nodes: UInt64, dom: UInt64, bytes: UInt64, outline: UInt64, metadata: UInt64, copyBytes: UInt64) { self.operation = operation; renderedJSONNodes = nodes domNodeUpperBound = dom; documentBytes = bytes + outlineEntries = outline; metadataBytes = metadata; copyJSONBytes = copyBytes } } diff --git a/Sources/JSONHTML/JSONHTMLRenderer.swift b/Sources/JSONHTML/JSONHTMLRenderer.swift index ba53080..f329e85 100644 --- a/Sources/JSONHTML/JSONHTMLRenderer.swift +++ b/Sources/JSONHTML/JSONHTMLRenderer.swift @@ -2,16 +2,17 @@ import Foundation import JSON -/// An immutable facade producing complete, expanded, highlighted standalone HTML. +/// An immutable facade producing complete highlighted standalone HTML. /// /// Rendering is actor-independent and executes no browser or UI framework. It uses /// the strict JSON writer's exact bytes, insertion order and preserved lexemes. -/// CSS is fixed and hash-authorized; scripts and external resources are denied. +/// Assets are fixed and hash-authorized. Interactive mode needs no external resources. +/// Formatted mode remains script-free; interactive disclosures work without scripting. public struct JSONHTMLRenderer: Sendable { /// Immutable configuration; finite capacities are checked on every invocation. public let options: JSONHTMLRenderingOptions - /// Creates a formatted renderer without allocating a document or invoking UI. + /// Creates a renderer without allocating a document or invoking UI. /// - Parameters: /// - options: Presentation choices and finite capacities for each independent call. public init(options: JSONHTMLRenderingOptions = .init()) { self.options = options } @@ -41,6 +42,7 @@ public struct JSONHTMLRenderer: Sendable { try chargeCopy(document.bytes, meter: &meter) let result = Data(document.bytes) try meter.release(UInt64(document.bytes.count)) + try meter.checkpoint() return .init(result: result, statistics: document.statistics(meter.statistics())) } @@ -55,6 +57,7 @@ public struct JSONHTMLRenderer: Sendable { try chargeCopy(document.bytes, meter: &meter) let result = String(decoding: document.bytes, as: UTF8.self) try meter.release(UInt64(document.bytes.count)) + try meter.checkpoint() return .init(result: result, statistics: document.statistics(meter.statistics())) } @@ -75,7 +78,8 @@ public struct JSONHTMLRenderer: Sendable { private func renderBody(_ value: JSON, meter: inout JSONWorkMeter) throws -> JSONHTMLDocumentBuffer { try meter.checkpoint() try options.limits.validate() - guard (0...8).contains(options.indentation), (1...8).contains(options.tabWidth) else { + guard (0...8).contains(options.indentation), (1...8).contains(options.tabWidth), + options.initiallyExpandedDepth >= 0, options.outlineDepth.map({ $0 >= 0 }) ?? true else { throw JSONHTMLFailure.invalidConfiguration } try JSONAdmission.text(options.title, meter: &meter) @@ -86,6 +90,7 @@ public struct JSONHTMLRenderer: Sendable { // One admission and one ledger; the cursor never calls a public writer facade. try JSONAdmission.value(value, meter: &meter) try JSONHTMLDocumentBuffer.require(.renderedJSONNodes, meter.used(.nodes), options.limits.maxRenderedJSONNodes) + if options.presentation == .interactive { return try renderInteractive(value, meter: &meter) } var output = JSONHTMLDocumentBuffer(limits: options.limits) try output.addDOM(24, meter: &meter) // Includes fixed tags, text and optional parser-created nodes. try output.append(" JSONHTMLStatistics { - .init(operation: operation, nodes: nodes, dom: dom, bytes: UInt64(bytes.count)) + .init(operation: operation, nodes: nodes, dom: dom, bytes: UInt64(bytes.count), outline: outline, metadata: metadata, copyBytes: copyBytes) } } diff --git a/Sources/JSONHTML/JSONHTMLRenderingOptions.swift b/Sources/JSONHTML/JSONHTMLRenderingOptions.swift index 4d7858b..938da71 100644 --- a/Sources/JSONHTML/JSONHTMLRenderingOptions.swift +++ b/Sources/JSONHTML/JSONHTMLRenderingOptions.swift @@ -11,7 +11,15 @@ public enum JSONHTMLTheme: String, Sendable { case dark } -/// Additional inclusive ceilings for formatted HTML generation. +/// Presentation of the complete standalone JSON document. +public enum JSONHTMLPresentation: String, Sendable { + /// Expanded highlighted JSON without scripting. + case formatted + /// Native disclosures, optional outline and fixed offline enhancement script. + case interactive +} + +/// Additional inclusive ceilings for HTML generation. /// /// Defaults reuse the balanced core node and output ceilings as initial finite /// policy. They are not measured browser heap, CPU or process RSS guarantees. @@ -25,6 +33,15 @@ public struct JSONHTMLLimits: Sendable { /// Maximum bytes in the complete UTF-8 HTML document. public var maxDocumentBytes: UInt64 + /// Maximum container outline entries; overflow fails the whole invocation. + public var maxOutlineEntries: UInt64 = JSONLimits.balanced[.objectMembers] + /// Maximum emitted Base64 metadata bytes, including copy JSON and Pointer forms. + public var maxMetadataBytes: UInt64 = JSONLimits.balanced[.aggregateTextBytes] + /// Browser search query ceiling in UTF-8 bytes, zero through 65,536. + /// + /// This is a query capacity, not a browser instruction or heap quota. + public var maxSearchQueryBytes: UInt64 = JSONLimits.balanced[.cancellationQuantum] + /// Creates finite inclusive rendering limits. /// /// Zero forbids use. Values must fit supported Int capacity at operation entry. @@ -42,14 +59,36 @@ public struct JSONHTMLLimits: Sendable { self.maxDOMNodes = maxDOMNodes self.maxDocumentBytes = maxDocumentBytes } + /// Creates limits with explicit interactive metadata policy. + /// - Parameters: + /// - core: One cumulative owned-operation budget. + /// - maxRenderedJSONNodes: Inclusive rendered JSON values. + /// - maxDOMNodes: Inclusive conservative DOM upper bound. + /// - maxDocumentBytes: Inclusive complete UTF-8 HTML bytes. + /// - maxOutlineEntries: Inclusive emitted container outline entries. + /// - maxMetadataBytes: Inclusive emitted Base64 metadata bytes. + /// - maxSearchQueryBytes: Browser query capacity in UTF-8 bytes, up to 65,536. + public init(core: JSONLimits = .balanced, + maxRenderedJSONNodes: UInt64 = JSONLimits.balanced[.nodes], + maxDOMNodes: UInt64 = JSONLimits.balanced[.nodes], + maxDocumentBytes: UInt64 = JSONLimits.balanced[.outputBytes], + maxOutlineEntries: UInt64, + maxMetadataBytes: UInt64 = JSONLimits.balanced[.aggregateTextBytes], + maxSearchQueryBytes: UInt64 = JSONLimits.balanced[.cancellationQuantum]) { + self.init(core: core, maxRenderedJSONNodes: maxRenderedJSONNodes, + maxDOMNodes: maxDOMNodes, maxDocumentBytes: maxDocumentBytes) + self.maxOutlineEntries = maxOutlineEntries; self.maxMetadataBytes = maxMetadataBytes + self.maxSearchQueryBytes = maxSearchQueryBytes + } package func validate() throws { try core.validate() guard maxRenderedJSONNodes < UInt64(Int.max), maxDOMNodes < UInt64(Int.max), - maxDocumentBytes < UInt64(Int.max) else { throw JSONHTMLFailure.invalidConfiguration } + maxDocumentBytes < UInt64(Int.max), maxOutlineEntries < UInt64(Int.max), + maxMetadataBytes < UInt64(Int.max), maxSearchQueryBytes <= 65_536 else { throw JSONHTMLFailure.invalidConfiguration } } } -/// Immutable-facade configuration for expanded, highlighted, script-free HTML. +/// Configuration for complete highlighted HTML with optional offline interaction. public struct JSONHTMLRenderingOptions: Sendable { /// Plain document title; control characters are rejected and HTML is escaped. public var title: String @@ -64,6 +103,13 @@ public struct JSONHTMLRenderingOptions: Sendable { /// Finite shared core and HTML capacities. public var limits: JSONHTMLLimits + /// Formatted by default; interactive behavior is explicitly selected. + public var presentation: JSONHTMLPresentation = .formatted + /// Containers at levels below this depth start open; root is level zero. + public var initiallyExpandedDepth: Int = 2 + /// Include containers through this level in the outline; nil disables the outline. + public var outlineDepth: Int? = 3 + /// Creates the formatted-rendering configuration. /// /// Insertion order and preserved numeric lexemes are always used. Constructed @@ -81,21 +127,49 @@ public struct JSONHTMLRenderingOptions: Sendable { self.title = title; self.theme = theme; self.indentation = indentation self.indentationUnit = indentationUnit; self.tabWidth = tabWidth; self.limits = limits } + /// Creates an explicitly selected presentation. + /// + /// Interactive copy/download uses the shared writer's exact preserved bytes. + /// Search is case-sensitive literal matching over rendered JSON key/value text. + /// - Parameters: + /// - title: Plain escaped title without control characters. + /// - theme: Browser color policy. + /// - indentation: Indentation units per level, zero through eight. + /// - indentationUnit: Literal spaces or tabs. + /// - tabWidth: Visual tab width, one through eight. + /// - limits: Inclusive core and HTML capacities and work quotas. + /// - presentation: Formatted or interactive standalone document. + /// - initiallyExpandedDepth: Nonnegative initial disclosure depth; root is level zero. + /// - outlineDepth: Nonnegative inclusive container level, or nil to omit the outline. + public init(title: String = "JSON", theme: JSONHTMLTheme = .automatic, + indentation: Int = 2, indentationUnit: JSONIndentationUnit = .spaces, + tabWidth: Int = 4, limits: JSONHTMLLimits = .init(), + presentation: JSONHTMLPresentation, initiallyExpandedDepth: Int = 2, + outlineDepth: Int? = 3) { + self.init(title: title, theme: theme, indentation: indentation, + indentationUnit: indentationUnit, tabWidth: tabWidth, limits: limits) + self.presentation = presentation; self.initiallyExpandedDepth = initiallyExpandedDepth + self.outlineDepth = outlineDepth + } } -/// Stable identifiers for the three additional HTML capacities. +/// Stable identifiers for additional HTML capacities. public enum JSONHTMLResourceMetric: String, Sendable { - /// JSON values in the formatted body. + /// JSON values in the selected presentation. case renderedJSONNodes /// Conservative upper bound of document DOM nodes. case domNodes /// Complete HTML UTF-8 bytes. case documentBytes + /// Emitted container outline entries. + case outlineEntries + /// Emitted Base64 metadata UTF-8 bytes. + case metadataBytes } /// Bounded HTML-specific failures; core resource failures and cancellation propagate unchanged. public enum JSONHTMLFailure: Error, Sendable, Equatable { - /// Invalid indentation, tab width, capacity or control-bearing title. + /// Invalid presentation depth, indentation, tab width, capacity or control-bearing title. case invalidConfiguration /// An inclusive HTML capacity was exceeded before growth. case limitExceeded(JSONHTMLResourceMetric, limit: UInt64, attempted: UInt64) diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 617833c..31f64d2 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -16,3 +16,9 @@ Actual adaptations are recorded in Documentation/Architecture/ExtractionManifest Original copyrights and applicable notices remain in the source and license files. The root package has no third-party runtime dependencies or foreign binary backend. Research and discarded comparison experiments are not distributed dependencies. + +Playwright 1.62.1 is an optional qualification-only development tool (Apache-2.0), +with its full license, NOTICE and third-party notices retained under +Licenses/PlaywrightQualification. No Playwright source or browser binary is adapted +or shipped as a library runtime dependency. The browser tool has its own manifest; +root package dependencies remain empty. diff --git a/Tests/JSONHTMLTests/FormattedRendererTests.swift b/Tests/JSONHTMLTests/FormattedRendererTests.swift index 52cd146..3beabc4 100644 --- a/Tests/JSONHTMLTests/FormattedRendererTests.swift +++ b/Tests/JSONHTMLTests/FormattedRendererTests.swift @@ -96,6 +96,8 @@ private func body(_ html: String) throws -> [UInt8] { case .documentBytes: report.statistics.documentBytes case .renderedJSONNodes: report.statistics.renderedJSONNodes case .domNodes: report.statistics.domNodeUpperBound + case .outlineEntries: report.statistics.outlineEntries + case .metadataBytes: report.statistics.metadataBytes } for delta in [-1, 0, 1] { var limits = JSONHTMLLimits() @@ -104,6 +106,8 @@ private func body(_ html: String) throws -> [UInt8] { case .documentBytes: limits.maxDocumentBytes = cap case .renderedJSONNodes: limits.maxRenderedJSONNodes = cap case .domNodes: limits.maxDOMNodes = cap + case .outlineEntries: limits.maxOutlineEntries = cap + case .metadataBytes: limits.maxMetadataBytes = cap } let renderer = JSONHTMLRenderer(options: .init(limits: limits)) if delta < 0 { @@ -185,7 +189,8 @@ private func body(_ html: String) throws -> [UInt8] { } @Test func sharedFormattedHTMLRendererHasIndependentConcurrentInvocations() async throws { - let renderer = JSONHTMLRenderer() + for mode in [JSONHTMLPresentation.formatted, .interactive] { + let renderer = JSONHTMLRenderer(options: .init(presentation: mode)) let root = try JSONParser().parse(#"{"é":9007199254740993,"é":[-0.00e+2,null]}"#) let expected = try renderer.renderWithReport(root) try await withThrowingTaskGroup(of: JSONHTMLRenderReport.self) { group in @@ -197,6 +202,7 @@ private func body(_ html: String) throws -> [UInt8] { } #expect(count == 16) } + } } @Test func formattedHTMLCSPHashesTheExactEmittedOriginalCSS() throws { diff --git a/Tests/JSONHTMLTests/InteractiveRendererTests.swift b/Tests/JSONHTMLTests/InteractiveRendererTests.swift new file mode 100644 index 0000000..f4ae226 --- /dev/null +++ b/Tests/JSONHTMLTests/InteractiveRendererTests.swift @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +@testable import JSONHTML + +private func interactive(_ limits: JSONHTMLLimits = .init(), depth: Int? = 3) -> JSONHTMLRenderer { + .init(options: .init(limits: limits, presentation: .interactive, outlineDepth: depth)) +} +private func attribute(_ html: String, after marker: String, name: String) throws -> [UInt8] { + let element = try #require(html.range(of: marker)) + let start = try #require(html.range(of: name + "=\"", range: element.lowerBound.. [UInt8] { + Array(try JSONWriter(options: .init(indentation: 2, numberStyle: .preserved)).write(root)) +} + +@Test func interactiveHTMLRetainsOriginalInitializerFunctionShapes() throws { + let optionsFactory: (String, JSONHTMLTheme, Int, JSONIndentationUnit, Int, JSONHTMLLimits) -> JSONHTMLRenderingOptions = JSONHTMLRenderingOptions.init + let limitsFactory: (JSONLimits, UInt64, UInt64, UInt64) -> JSONHTMLLimits = JSONHTMLLimits.init + let limits = limitsFactory(.balanced, 100, 1000, 100_000) + let options = optionsFactory("Old", .light, 2, .spaces, 4, limits) + #expect(options.presentation == .formatted) + let report = try JSONHTMLRenderer(options: options).renderStringWithReport(.null) + #expect(!report.result.contains("\n")) + } +} + +@Test func interactiveHTMLPointersPreserveScalarControlsAndRFC6901Escapes() throws { + let root = try JSONParser().parse(#"{"~1/a\u0000\r\n":[{"0":1}],"é":2,"é":3}"#) + let html = try interactive().renderString(root) + let vectors = ["", "/~01~1a\u{0}\r\n", "/~01~1a\u{0}\r\n/0", "/~01~1a\u{0}\r\n/0/0", "/é", "/é"] + for (index, pointer) in vectors.enumerated() { + #expect(try attribute(html, after: "id=\"jh-n-\(index)\"", name: "data-pointer") == Array(pointer.utf8)) + let quoted = try attribute(html, after: "id=\"jh-n-\(index)\"", name: "data-pointer-escaped") + #expect(try quoted == Array(JSONWriter().write(.string(pointer)))) + } + #expect(!html.contains("id=\"/")) +} + +@Test func interactiveHTMLDisclosuresAndOutlineUseGeneratedOrdinals() throws { + let root = try JSONParser().parse(#"{"a":[{"b":true}],"empty":{}}"#) + let renderer = JSONHTMLRenderer(options: .init(presentation: .interactive, initiallyExpandedDepth: 1, outlineDepth: 1)) + let report = try renderer.renderStringWithReport(root) + #expect(report.statistics.renderedJSONNodes == 5 && report.statistics.operation[.nodes] == 5) + #expect(report.statistics.outlineEntries == 3) + #expect(report.result.components(separatedBy: " open>").count - 1 == 1) + #expect(report.result.components(separatedBy: "
report.statistics.documentBytes + report.statistics.copyJSONBytes) + #expect(report.statistics.operation[.retainedBytes] == report.statistics.documentBytes) + for metric in [JSONResourceMetric.workUnits, .outputBytes, .retainedBytes] { + let boundary = metric == .retainedBytes ? report.statistics.operation.peakLogicalRetainedBytes : report.statistics.operation[metric] + for cap in [boundary - 1, boundary, boundary + 1] { + var limits = JSONHTMLLimits(); limits.core[metric] = cap + if cap < boundary { #expect(throws: JSONFailure.self) { try interactive(limits).render(root) } } + else { #expect(try interactive(limits).render(root) == report.result) } + } + } +} + +@Test func interactiveHTMLRejectsInvalidDepthAndQueryConfiguration() throws { + for options in [JSONHTMLRenderingOptions(presentation: .interactive, initiallyExpandedDepth: -1), .init(presentation: .interactive, outlineDepth: -1)] { + #expect(throws: JSONHTMLFailure.invalidConfiguration) { try JSONHTMLRenderer(options: options).render(.null) } + } + var limits = JSONHTMLLimits(); limits.maxSearchQueryBytes = 65_537 + #expect(throws: JSONHTMLFailure.invalidConfiguration) { try interactive(limits).render(.null) } + limits.maxSearchQueryBytes = 0 + #expect(try interactive(limits).renderString(.null).contains("data-byte-limit=\"0\"")) +} + +@Test func interactiveHTMLFailureReleasesScratchAndPreservesTerminalError() throws { + var meter = try JSONWorkMeter(limits: .balanced, domain: .writing); try meter.retain(17) + var limits = JSONHTMLLimits(); limits.maxMetadataBytes = 20 + let renderer = interactive(limits) + #expect(throws: JSONHTMLFailure.self) { try renderer.render(.array([.string("long string")]), meter: &meter) } + #expect(meter.used(.retainedBytes) == 17) + let work = meter.used(.workUnits) + #expect(throws: JSONHTMLFailure.self) { try renderer.render(.null, meter: &meter) } + #expect(meter.used(.workUnits) == work) + try meter.release(17) +} + +@Test func interactiveHTMLCancellationInsideTreeEmissionIsTerminal() async throws { + let task = Task { + var meter = try JSONWorkMeter(limits: .balanced, domain: .writing); try meter.retain(17) + meter.checkpointObserver = { work in if work >= 20_000 { withUnsafeCurrentTask { $0?.cancel() } } } + #expect(throws: CancellationError.self) { try interactive().render(.array(Array(repeating: .number(1), count: 40)), meter: &meter) } + #expect(meter.used(.workUnits) >= 20_000 && meter.used(.outputBytes) > 0) + #expect(meter.used(.retainedBytes) == 17) + #expect(throws: CancellationError.self) { try meter.checkpoint() } + try meter.release(17) + } + try await task.value +} + +@Test func interactiveHTMLDeepWideAndPrintBodiesAreComplete() throws { + var deep: JSON = .string("last-sentinel"); for _ in 0..<40 { deep = .array([deep]) } + let wide = JSON.array((0..<150).map { .string("literal [needle] \($0)") }) + for root in [deep, wide] { + let data = try interactive().renderWithReport(root) + let text = try interactive().renderStringWithReport(root) + #expect(data.statistics == text.statistics && Array(data.result) == Array(text.result.utf8)) + #expect(try attribute(text.result, after: "id=\"jh-copy-json\"", name: "data-bytes") == exact(root)) + #expect(text.result.contains("class=\"print-json\"")) + #expect(text.result.contains(root == deep ? "last-sentinel" : "literal [needle] 149")) + } +} + +@Test func interactiveHTMLAssetsAreFixedAndDataCannotExecute() throws { + let payload = "" + let html = try interactive().renderString(.string(payload)) + #expect(html.components(separatedBy: "&\u{2028}\u{2029}\u{0}") +var deep: JSON = .string("last-sentinel"); for _ in 0..<40 { deep = .array([deep]) } +let wide: JSON = .array((0..<150).map { .string("literal [needle] \($0)") }) +var zero = JSONHTMLRenderingOptions(presentation: .interactive); zero.limits.maxSearchQueryBytes = 0 +let fixtures: [(String, JSON, JSONHTMLRenderingOptions)] = [ + ("exact", exact, .init(title: "Exact offline JSON", presentation: .interactive)), + ("hostile", hostile, .init(title: "", presentation: .interactive)), + ("deep", deep, .init(presentation: .interactive, initiallyExpandedDepth: 0)), + ("wide", wide, .init(presentation: .interactive)), + ("null", .null, .init(presentation: .interactive)), + ("zero-query", exact, zero), + ("no-outline", exact, .init(presentation: .interactive, outlineDepth: nil)), + ("formatted", exact, .init())] +var records: [JSON] = [] +for (name, root, options) in fixtures { + let report = try JSONHTMLRenderer(options: options).renderWithReport(root) + try report.result.write(to: output.appendingPathComponent(name + ".html")) + let bytes = try JSONWriter(options: .init(indentation: options.indentation, numberStyle: .preserved, indentationUnit: options.indentationUnit)).write(root) + try bytes.write(to: output.appendingPathComponent(name + ".json")) + records.append(try object([ + JSONMember("name", value: .string(name)), + JSONMember("nodes", value: .number(JSONNumber(integer: report.statistics.renderedJSONNodes))), + JSONMember("domUpperBound", value: .number(JSONNumber(integer: report.statistics.domNodeUpperBound))), + JSONMember("documentBytes", value: .number(JSONNumber(integer: report.statistics.documentBytes))), + JSONMember("metadataBytes", value: .number(JSONNumber(integer: report.statistics.metadataBytes))), + JSONMember("workUnits", value: .number(JSONNumber(integer: report.statistics.operation[.workUnits]))), + JSONMember("logicalPeakBytes", value: .number(JSONNumber(integer: report.statistics.operation.peakLogicalRetainedBytes))) ])) +} +try JSONWriter(options: .init(indentation: 2)).write(.array(records)).write(to: output.appendingPathComponent("fixtures.json")) +print("JSONHTML_BROWSER_FIXTURES passed count=8") diff --git a/Tools/JSONHTMLBrowser/browser.cjs b/Tools/JSONHTMLBrowser/browser.cjs new file mode 100644 index 0000000..bcf0cbb --- /dev/null +++ b/Tools/JSONHTMLBrowser/browser.cjs @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +// Qualification-only. No Playwright or Node dependency enters the root package. +const fs = require('node:fs'); +const path = require('node:path'); +const http = require('node:http'); +const crypto = require('node:crypto'); +const assert = require('node:assert/strict'); +const {pathToFileURL} = require('node:url'); +const playwright = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); +const version = require(path.join(path.dirname(require.resolve(process.env.PLAYWRIGHT_MODULE || 'playwright')), 'package.json')).version; +assert.equal(version, '1.62.1', 'Qualification requires the pinned Playwright version'); +const fixtures = path.resolve(process.argv[2]), out = path.resolve(process.argv[3]); +fs.mkdirSync(out, {recursive: true}); +const records = JSON.parse(fs.readFileSync(path.join(fixtures, 'fixtures.json'))); +assert.equal(records.length, 8); +const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); +const receipt = {status: 'running', playwright: version, fixtures: [], engines: [], cases: [], limitations: ['Playwright WebKit is not native Safari qualification.', 'Viewport tests do not qualify physical mobile browsers or VoiceOver.', 'Clipboard API acceptance is separate from platform clipboard transformations; exact downloads are independently compared.', 'Search chunking is not an instruction, CPU, heap or process RSS guarantee.']}; +for (const name of fs.readdirSync(fixtures).sort()) { + const bytes = fs.readFileSync(path.join(fixtures, name)); + receipt.fixtures.push({path: name, bytes: bytes.length, sha256: sha(bytes)}); +} +const server = http.createServer((req, res) => { + const name = path.basename(new URL(req.url, 'http://localhost').pathname); + if (!/^[a-z-]+\.(html|json)$/.test(name) || !fs.existsSync(path.join(fixtures, name))) {res.writeHead(404); res.end(); return;} + res.writeHead(200, {'Content-Type': name.endsWith('.html') ? 'text/html; charset=utf-8' : 'application/json'}); + res.end(fs.readFileSync(path.join(fixtures, name))); +}); +const activeBrowsers = []; +const save = () => fs.writeFileSync(path.join(out, 'browser.json'), JSON.stringify(receipt, null, 2) + '\n'); +async function main() { + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const origin = 'http://127.0.0.1:' + server.address().port; + for (const engine of ['chromium', 'webkit', 'firefox']) { + let browser; + try { browser = await playwright[engine].launch({headless: true, timeout: 30_000}); } + catch (error) { + receipt.engines.push({name: engine, status: 'blocked', error: error.message, executedCases: 0, unexecutedCases: engine === 'chromium' ? 13 : 12}); + save(); continue; + } + activeBrowsers.push(browser); + receipt.engines.push({name: engine, status: 'started', version: browser.version(), executable: path.basename(playwright[engine].executablePath())}); + async function run(name, fixture, protocol, javaScriptEnabled, action, extra = {}) { + const errors = [], requests = [], violations = []; + const context = await browser.newContext({javaScriptEnabled, acceptDownloads: true, viewport: {width: 1440, height: 1000}, ...extra}); + await context.route('**/*', async route => { + const url = route.request().url(); + if (url.startsWith(origin + '/') || url.startsWith('file:') || url.startsWith('blob:')) return route.continue(); + requests.push(url); await route.abort(); + }); + const page = await context.newPage(); + page.on('pageerror', error => errors.push(error.message)); + page.on('console', message => {if (message.type() === 'error') errors.push(message.text());}); + await context.addInitScript(() => { + window.__violations = []; + document.addEventListener('securitypolicyviolation', e => window.__violations.push(e.violatedDirective)); + }); + const entry = {engine, name, fixture, protocol, javaScriptEnabled, status: 'running'}; + receipt.cases.push(entry); save(); + try { + const url = protocol === 'file' ? pathToFileURL(path.join(fixtures, fixture + '.html')).href : origin + '/' + fixture + '.html'; + await page.goto(url); + assert.ok((await page.title()).length > 0); + assert.equal(await page.locator('h1').count(), 1); + const record = records.find(r => r.name === fixture); + const dom = await page.evaluate(() => { + let count = 0; const iterator = document.createTreeWalker(document, NodeFilter.SHOW_ALL); + while (iterator.nextNode()) count++; return count; + }); + assert.ok(dom <= record.domUpperBound, 'DOM bound undercounts actual nodes'); + entry.actualDOMNodes = dom; entry.domUpperBound = record.domUpperBound; + if (fixture !== 'formatted') { + assert.equal(await page.locator('#jh-tree .node').count(), record.nodes); + assert.equal(await page.locator('#jh-toolbar').isVisible(), javaScriptEnabled); + } + await action(page, context, record); + if (javaScriptEnabled) violations.push(...await page.evaluate(() => window.__violations)); + const enhancedDOM = await page.evaluate(() => {let count = 0; const it = document.createTreeWalker(document, NodeFilter.SHOW_ALL); while (it.nextNode()) count++; return count;}); + assert.ok(enhancedDOM <= record.domUpperBound, 'Enhanced DOM bound undercounts'); + entry.enhancedDOMNodes = enhancedDOM; + assert.deepEqual(errors, [], 'Unexpected browser console/page errors'); + assert.deepEqual(requests, [], 'Unexpected external resource request'); + assert.deepEqual(violations, [], 'CSP violation'); + entry.status = 'passed'; entry.consoleErrors = errors; entry.externalRequests = requests; entry.cspViolations = violations; + } catch (e) { + entry.status = 'failed'; entry.error = e.stack; entry.consoleErrors = errors; entry.externalRequests = requests; entry.cspViolations = violations; + await page.screenshot({path: path.join(out, engine + '-' + name + '-failure.png'), fullPage: true}).catch(() => {}); + throw e; + } finally { await context.close(); save(); } + } + async function status(page, text) { await page.waitForFunction(text => document.getElementById('jh-status').textContent.includes(text), text); } + async function downloaded(page, selector) { + const event = page.waitForEvent('download'); await page.locator(selector).first().click(); + const download = await event; const file = await download.path(); assert.ok(file); + return {bytes: fs.readFileSync(file), filename: download.suggestedFilename()}; + } + for (const protocol of ['file', 'http']) for (const enabled of [false, true]) { + await run(protocol + '-disclosure-' + enabled, 'exact', protocol, enabled, async page => { + const root = page.locator('#jh-n-0'); + assert.equal(await root.evaluate(n => n.open), true); + await root.locator(':scope > summary').click({position: {x: 5, y: 8}}); + assert.equal(await root.evaluate(n => n.open), false); + await root.locator(':scope > summary').focus(); await page.keyboard.press('Enter'); + assert.equal(await root.evaluate(n => n.open), true); + assert.equal(await page.locator('details[name]').count(), 0); + }); + } + await run('keyboard-outline', 'deep', 'http', true, async page => { + await page.locator('.outline a').last().click(); + assert.equal(await page.locator('details[open]').count(), 4); + assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-n-3'); + await page.getByRole('button', {name: 'Expand all', exact: true}).click(); await status(page, 'Expanded 40'); + assert.equal(await page.locator('details[open]').count(), 40); + await page.getByRole('button', {name: 'Collapse all', exact: true}).click(); await status(page, 'Collapsed 40'); + assert.equal(await page.locator('details[open]').count(), 0); + }); + await run('literal-search', 'wide', 'http', true, async page => { + await page.locator('#jh-query').fill('[needle]'); await status(page, '150 matching nodes'); + assert.equal(await page.locator('#jh-results a').count(), 100); + assert.equal(await page.locator('.search-hit').count(), 150); + const peakDOM = await page.evaluate(() => {let n = 0; const it = document.createTreeWalker(document, NodeFilter.SHOW_ALL); while (it.nextNode()) n++; return n;}); + assert.ok(peakDOM <= records.find(r => r.name === 'wide').domUpperBound); + await page.locator('#jh-query').fill('Needle'); await status(page, '0 matching nodes'); + await page.locator('#jh-query').fill(''); + await page.locator('#jh-query').pressSequentially('literal [needle] 149'); await status(page, '1 matching nodes'); + assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-query'); + await page.locator('#jh-results a').click(); + assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-n-150'); + await page.getByRole('button', {name: 'Clear search', exact: true}).click(); await status(page, '0 matching nodes'); + assert.equal(await page.locator('.search-hit').count(), 0); + }); + await run('exact-copy-download', 'exact', 'http', true, async page => { + await page.evaluate(() => Object.defineProperty(navigator, 'clipboard', {configurable: true, value: {writeText: value => new Promise(resolve => {window.__copied = value; window.__resolveCopy = resolve;})}})); + await page.getByRole('button', {name: 'Copy JSON', exact: true}).click(); + assert.equal((await page.locator('#jh-status').textContent()).includes('Browser accepted JSON'), false); + await page.evaluate(() => window.__resolveCopy()); await status(page, 'Browser accepted JSON'); + const expected = fs.readFileSync(path.join(fixtures, 'exact.json')); + assert.deepEqual(Buffer.from(await page.evaluate(() => window.__copied), 'utf8'), expected); + const download = await downloaded(page, '[data-action="download-json"]'); + assert.equal(download.filename, 'document.json'); assert.deepEqual(download.bytes, expected); + }); + await run('pointer-manual-fallback', 'exact', 'file', true, async page => { + await page.evaluate(() => Object.defineProperty(navigator, 'clipboard', {configurable: true, value: {writeText: async () => {throw new Error('Rejected for qualification');}}})); + const raw = '/~01~1a\0\r\n'; + const download = await downloaded(page, '#jh-n-1 > summary [data-action="download-pointer"]'); + assert.equal(download.filename, 'pointer.txt'); assert.deepEqual(download.bytes, Buffer.from(raw, 'utf8')); + await page.locator('#jh-n-1 > summary [data-action="copy-pointer"]').click(); await status(page, 'Clipboard unavailable or rejected'); + // Textareas normalize CRLF; fallback is visibly manual and does not promise byte fidelity. + assert.equal(await page.locator('#jh-manual-text').inputValue(), raw.replace(/\r\n/g, '\n')); + await page.locator('#jh-n-1 > summary [data-action="copy-pointer-escaped"]').click(); + await page.waitForFunction(() => document.getElementById('jh-manual-text').value.startsWith('"')); + assert.equal(await page.locator('#jh-manual-text').inputValue(), '"/~01~1a\\u0000\\u000d\\u000a"'); + assert.equal(await page.locator('#jh-n-1').evaluate(n => n.open), true); + await page.evaluate(() => Object.defineProperty(navigator, 'clipboard', {configurable: true, value: undefined})); + await page.getByRole('button', {name: 'Copy JSON', exact: true}).click(); await status(page, 'Clipboard unavailable or rejected'); + assert.equal(await page.locator('#jh-manual-text').inputValue(), fs.readFileSync(path.join(fixtures, 'exact.json'), 'utf8')); + }); + await run('hostile-csp', 'hostile', 'http', true, async page => { + assert.equal(await page.locator('img').count(), 0); + assert.equal(await page.locator('script').count(), 1); + assert.equal(await page.evaluate(() => typeof window.pwned), 'undefined'); + const download = await downloaded(page, '[data-action="download-json"]'); + assert.deepEqual(download.bytes, fs.readFileSync(path.join(fixtures, 'hostile.json'))); + assert.equal(await page.locator('code[data-search]').textContent(), fs.readFileSync(path.join(fixtures, 'hostile.json'), 'utf8')); + }); + await run('mobile-layout', 'exact', 'http', true, async page => { + assert.ok(await page.getByRole('button', {name: 'Download JSON (UTF-8)', exact: true}).isVisible()); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth); + assert.equal(overflow, false, 'Mobile horizontal overflow'); + await page.screenshot({path: path.join(out, engine + '-mobile.png'), fullPage: true}); + await page.setViewportSize({width: 1440, height: 1000}); + await page.screenshot({path: path.join(out, engine + '-desktop.png'), fullPage: true}); + }, {viewport: {width: 390, height: 844}}); + await run('complete-print', 'deep', 'http', true, async page => { + assert.equal(await page.locator('details[open]').count(), 0); + await page.emulateMedia({media: 'print'}); + assert.equal(await page.locator('#jh-tree').isVisible(), false); + assert.equal(await page.locator('.print-json').isVisible(), true); + assert.equal(await page.locator('.print-json code').textContent(), fs.readFileSync(path.join(fixtures, 'deep.json'), 'utf8')); + if (engine === 'chromium') await page.pdf({path: path.join(out, 'chromium-complete.pdf'), format: 'A4'}); + assert.equal(await page.locator('details[open]').count(), 0); + }); + await run('query-limit-no-outline', 'zero-query', 'http', true, async page => { + await page.locator('#jh-query').fill('é'); await status(page, 'exceeds the UTF-8 byte limit'); + assert.equal(await page.locator('#jh-results a').count(), 0); + await page.goto(origin + '/no-outline.html'); + assert.equal(await page.locator('.outline').count(), 0); + await page.locator('#jh-query').fill('é'); await status(page, '1 matching nodes'); + await page.locator('#jh-query').fill('é'); await status(page, '1 matching nodes'); + await page.locator('#jh-query').fill('é'.repeat(513)); await status(page, 'exceeds the UTF-8 byte limit'); + assert.equal(await page.locator('.search-hit').count(), 0); + await page.evaluate(() => {const q = document.getElementById('jh-query'); q.value = '\ud800'; q.dispatchEvent(new Event('input'));}); + await status(page, 'Unicode scalar text'); + }); + if (engine === 'chromium') await run('native-browser-clipboard', 'exact', 'http', true, async (page, context) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + await page.getByRole('button', {name: 'Copy JSON', exact: true}).click(); await status(page, 'Browser accepted JSON'); + assert.deepEqual(Buffer.from(await page.evaluate(() => navigator.clipboard.readText()), 'utf8'), fs.readFileSync(path.join(fixtures, 'exact.json'))); + }); + await browser.close(); + } + receipt.expectedCases = 37; receipt.passedCases = receipt.cases.filter(c => c.status === 'passed').length; + receipt.unexecutedCases = receipt.engines.reduce((sum, e) => sum + (e.unexecutedCases || 0), 0); + if (receipt.unexecutedCases) {receipt.status = 'blocked'; save(); console.error('Browser startup blocked; unexecuted cases=' + receipt.unexecutedCases); process.exitCode = 2; return;} + assert.equal(receipt.cases.length, 37); + assert.equal(receipt.cases.filter(c => c.status === 'passed').length, 37); + receipt.status = 'passed'; receipt.expectedCases = 37; receipt.passedCases = 37; receipt.unexpectedSkips = 0; + save(); console.log('JSONHTML_BROWSER passed engines=3 cases=37 skips=0'); +} +main().catch(error => {receipt.status = 'failed'; receipt.error = error.stack; save(); console.error(error); process.exitCode = 1;}).finally(async () => {await Promise.all(activeBrowsers.map(b => b.close().catch(() => {}))); server.close();}); diff --git a/Tools/JSONHTMLBrowser/package.json b/Tools/JSONHTMLBrowser/package.json new file mode 100644 index 0000000..975376d --- /dev/null +++ b/Tools/JSONHTMLBrowser/package.json @@ -0,0 +1,6 @@ +{ + "name": "swift-json-html-browser-qualification", + "private": true, + "license": "Apache-2.0", + "devDependencies": {"playwright": "1.62.1"} +} From d1f63624a92966ee5d1b91182dcbcdd8dacc401a Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:36:07 +0200 Subject: [PATCH 05/59] fix(JSONHTML): preserve search focus and enforce browser CSP tests --- .../Release1.3/InteractiveIncrement.md | 5 ++++ Sources/JSONHTML/Assets/Interactive.js | 6 +++-- .../JSONHTML/JSONHTMLInteractiveAssets.swift | 8 +++--- Tools/JSONHTMLBrowser/README.md | 5 ++++ Tools/JSONHTMLBrowser/browser.cjs | 26 ++++++++++++++----- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/Documentation/Architecture/Release1.3/InteractiveIncrement.md b/Documentation/Architecture/Release1.3/InteractiveIncrement.md index 6255d28..c4b0717 100644 --- a/Documentation/Architecture/Release1.3/InteractiveIncrement.md +++ b/Documentation/Architecture/Release1.3/InteractiveIncrement.md @@ -68,3 +68,8 @@ headless and windowed modes; do not claim its unexecuted matrix passed. Annotations/source locations, transfer/RTF/PDF preparation and measured final presets remain future increments. Existing release/performance history and the 10% review trigger are unchanged. No PR merge, tag or publication is performed. + +Chromium supplies desktop/mobile screenshots. Playwright WebKit screenshot +synchronization injects a `body {}` stylesheet, which the document correctly blocks; +WebKit therefore uses DOM/layout/interaction checks without that screenshot hook. +CSP and console assertions remain enforced, and no unsafe-inline bypass is added. diff --git a/Sources/JSONHTML/Assets/Interactive.js b/Sources/JSONHTML/Assets/Interactive.js index 5f5606b..d7adbaa 100644 --- a/Sources/JSONHTML/Assets/Interactive.js +++ b/Sources/JSONHTML/Assets/Interactive.js @@ -27,8 +27,10 @@ if (parent.tagName === 'DETAILS') parent.open = true; } if (node.tagName === 'DETAILS') node.open = true; - node.focus({preventScroll: true}); - node.scrollIntoView({block: 'nearest'}); + if (focus) { + node.focus({preventScroll: true}); + node.scrollIntoView({block: 'nearest'}); + } }; const download = (encoded, filename) => { const url = URL.createObjectURL(new Blob([bytes(encoded)], {type: filename.endsWith('.txt') ? 'text/plain;charset=utf-8' : 'application/json;charset=utf-8'})); diff --git a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift index d36f0ed..8ad947d 100644 --- a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift +++ b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift @@ -36,8 +36,10 @@ package enum JSONHTMLInteractiveAssets { if (parent.tagName === 'DETAILS') parent.open = true; } if (node.tagName === 'DETAILS') node.open = true; - node.focus({preventScroll: true}); - node.scrollIntoView({block: 'nearest'}); + if (focus) { + node.focus({preventScroll: true}); + node.scrollIntoView({block: 'nearest'}); + } }; const download = (encoded, filename) => { const url = URL.createObjectURL(new Blob([bytes(encoded)], {type: filename.endsWith('.txt') ? 'text/plain;charset=utf-8' : 'application/json;charset=utf-8'})); @@ -149,5 +151,5 @@ package enum JSONHTMLInteractiveAssets { })(); """# package static let styleHash = "ZViqigi/3L6QAgh4IHgRZSMK0MTDXXYvGytdlYTJfoQ=" - package static let scriptHash = "FhFTH1Ne5Y99evqwxUMK6h6n0tLakTWXAEcr/iz8KMg=" + package static let scriptHash = "CefCjyzNV1pVYg7GAqXuWfG1Q+aJ7xTdozRxnHyb+H4=" } diff --git a/Tools/JSONHTMLBrowser/README.md b/Tools/JSONHTMLBrowser/README.md index 57e93ec..a4300a2 100644 --- a/Tools/JSONHTMLBrowser/README.md +++ b/Tools/JSONHTMLBrowser/README.md @@ -21,3 +21,8 @@ seams qualify UI outcome handling separately from the Chromium clipboard test. Textareas can normalize line endings; exact UTF-8 Pointer downloads remain the byte-preserving path. WebKit is not a native Safari result, mobile viewports are not device qualification, and VoiceOver remains a separate manual release gate. + +Chromium supplies desktop/mobile screenshots. Playwright WebKit screenshot +synchronization injects a `body {}` stylesheet, which the document correctly blocks; +WebKit therefore uses DOM/layout/interaction checks without that screenshot hook. +CSP and console assertions remain enforced, and no unsafe-inline bypass is added. diff --git a/Tools/JSONHTMLBrowser/browser.cjs b/Tools/JSONHTMLBrowser/browser.cjs index bcf0cbb..8ddcff5 100644 --- a/Tools/JSONHTMLBrowser/browser.cjs +++ b/Tools/JSONHTMLBrowser/browser.cjs @@ -14,7 +14,7 @@ fs.mkdirSync(out, {recursive: true}); const records = JSON.parse(fs.readFileSync(path.join(fixtures, 'fixtures.json'))); assert.equal(records.length, 8); const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); -const receipt = {status: 'running', playwright: version, fixtures: [], engines: [], cases: [], limitations: ['Playwright WebKit is not native Safari qualification.', 'Viewport tests do not qualify physical mobile browsers or VoiceOver.', 'Clipboard API acceptance is separate from platform clipboard transformations; exact downloads are independently compared.', 'Search chunking is not an instruction, CPU, heap or process RSS guarantee.']}; +const receipt = {status: 'running', playwright: version, fixtures: [], engines: [], cases: [], limitations: ['Screenshots are captured in Chromium; WebKit screenshots would inject a CSP-blocked synchronization stylesheet. WebKit DOM/layout/interaction checks still enforce CSP.', 'Playwright WebKit is not native Safari qualification.', 'Viewport tests do not qualify physical mobile browsers or VoiceOver.', 'Clipboard API acceptance is separate from platform clipboard transformations; exact downloads are independently compared.', 'Search chunking is not an instruction, CPU, heap or process RSS guarantee.']}; for (const name of fs.readdirSync(fixtures).sort()) { const bytes = fs.readFileSync(path.join(fixtures, name)); receipt.fixtures.push({path: name, bytes: bytes.length, sha256: sha(bytes)}); @@ -83,7 +83,7 @@ async function main() { entry.status = 'passed'; entry.consoleErrors = errors; entry.externalRequests = requests; entry.cspViolations = violations; } catch (e) { entry.status = 'failed'; entry.error = e.stack; entry.consoleErrors = errors; entry.externalRequests = requests; entry.cspViolations = violations; - await page.screenshot({path: path.join(out, engine + '-' + name + '-failure.png'), fullPage: true}).catch(() => {}); + if (engine === 'chromium') await page.screenshot({path: path.join(out, engine + '-' + name + '-failure.png'), fullPage: true, caret: 'initial'}).catch(() => {}); throw e; } finally { await context.close(); save(); } } @@ -166,9 +166,12 @@ async function main() { assert.ok(await page.getByRole('button', {name: 'Download JSON (UTF-8)', exact: true}).isVisible()); const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth); assert.equal(overflow, false, 'Mobile horizontal overflow'); - await page.screenshot({path: path.join(out, engine + '-mobile.png'), fullPage: true}); + // Playwright WebKit inserts an unauthorized body{} style during screenshot synchronization. + // Keep enforcement intact and capture visual evidence in Chromium; all engines retain DOM/layout checks. + if (engine === 'chromium') await page.screenshot({path: path.join(out, engine + '-mobile.png'), fullPage: true, caret: 'initial'}); await page.setViewportSize({width: 1440, height: 1000}); - await page.screenshot({path: path.join(out, engine + '-desktop.png'), fullPage: true}); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth), false); + if (engine === 'chromium') await page.screenshot({path: path.join(out, engine + '-desktop.png'), fullPage: true, caret: 'initial'}); }, {viewport: {width: 390, height: 844}}); await run('complete-print', 'deep', 'http', true, async page => { assert.equal(await page.locator('details[open]').count(), 0); @@ -193,8 +196,19 @@ async function main() { }); if (engine === 'chromium') await run('native-browser-clipboard', 'exact', 'http', true, async (page, context) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); - await page.getByRole('button', {name: 'Copy JSON', exact: true}).click(); await status(page, 'Browser accepted JSON'); - assert.deepEqual(Buffer.from(await page.evaluate(() => navigator.clipboard.readText()), 'utf8'), fs.readFileSync(path.join(fixtures, 'exact.json'))); + // Keep previous clipboard text in browser memory only; never put it in receipts. + await page.evaluate(async () => {window.__previousClipboard = await navigator.clipboard.readText();}); + try { + await page.getByRole('button', {name: 'Copy JSON', exact: true}).click(); await status(page, 'Browser accepted JSON'); + assert.deepEqual(Buffer.from(await page.evaluate(() => navigator.clipboard.readText()), 'utf8'), fs.readFileSync(path.join(fixtures, 'exact.json'))); + } finally { + await page.evaluate(async () => { + const current = await navigator.clipboard.readText(); + const expected = new TextDecoder().decode(Uint8Array.from(atob(document.getElementById('jh-copy-json').dataset.bytes), c => c.charCodeAt(0))); + if (current === expected) await navigator.clipboard.writeText(window.__previousClipboard); + delete window.__previousClipboard; + }); + } }); await browser.close(); } From c2f1819ec03754ea5b3c0ae3533c2c04b3d00713 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Tue, 13 Oct 2026 23:59:23 +0200 Subject: [PATCH 06/59] Qualify JSONHTML browsers and hide unavailable no-script actions --- CHANGELOG.md | 2 +- .../Architecture/Release1.3/Acceptance.json | 48 ++++++---- .../Release1.3/BrowserQualification.md | 91 +++++++++++++++++++ .../Release1.3/InteractiveIncrement.md | 4 + .../Architecture/Release1.3/Progress.json | 27 +++++- Sources/JSONHTML/Assets/Interactive.css | 2 +- Sources/JSONHTML/Assets/Interactive.js | 3 +- .../JSONHTML/JSONHTMLInteractiveAssets.swift | 9 +- Tools/JSONHTMLBrowser/README.md | 2 + Tools/JSONHTMLBrowser/browser.cjs | 41 +++++++-- 10 files changed, 193 insertions(+), 36 deletions(-) create mode 100644 Documentation/Architecture/Release1.3/BrowserQualification.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b204c91..4ca58ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Develop JSONHTML as the fifth product for 1.3: exact formatted standalone HTML, shared strict-writer token formatting, literal tab indentation, cumulative finite reports, bounded script-free output and a fifth native DocC catalogue. - Interactive rendering and transfer/RTF/PDF export remain later increments. + Annotations and transfer/RTF/PDF export remain later increments. - Add local full-language wasm32 WASI support, preserving all four products and the empty root dependency graph. Twelve cumulative UInt64 quotas can exceed Int.max without changing profiles; allocation capacities retain Int guards. diff --git a/Documentation/Architecture/Release1.3/Acceptance.json b/Documentation/Architecture/Release1.3/Acceptance.json index 9f46ebc..2ad0ad2 100644 --- a/Documentation/Architecture/Release1.3/Acceptance.json +++ b/Documentation/Architecture/Release1.3/Acceptance.json @@ -113,9 +113,10 @@ "status": "partial", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH13", @@ -123,9 +124,10 @@ "requirement": "Default expansion, bounded outline, ancestor opening, focus, root/empty/numeric/special-key paths and generated IDs behave coherently.", "status": "partial", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH14", @@ -133,9 +135,10 @@ "requirement": "Copy/download bytes decode to the owned formatted JSON; RFC Pointer metadata preserves controls, CR/LF, NUL and exact scalar sequences.", "status": "partial", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH15", @@ -143,9 +146,10 @@ "requirement": "Raw and escaped Pointer copy modes have explicit labels; unavailable clipboard behavior is reported and manual/UTF-8 alternatives work.", "status": "partial", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH16", @@ -153,9 +157,10 @@ "requirement": "Literal search is bounded/chunked, preserves scalar distinction, opens matched ancestors and reports its rendered-text scope honestly.", "status": "partial", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH17", @@ -164,9 +169,10 @@ "status": "partial", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH18", @@ -175,9 +181,10 @@ "status": "partial", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH19", @@ -186,23 +193,28 @@ "status": "partial", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." }, { "id": "JH20", "gate": "H2", "requirement": "Annotations attach by exact typed path or identify missing targets at their parent; no empty list asserts schema validity.", "status": "not_run", - "evidence": [] + "evidence": [ + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ] }, { "id": "JH21", "gate": "H2", "requirement": "JSONDocument locations remain coherent, unmapped/replaced roots invent none, and annotation messages/paths are bounded and escaped.", "status": "not_run", - "evidence": [] + "evidence": [ + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ] }, { "id": "JH22", diff --git a/Documentation/Architecture/Release1.3/BrowserQualification.md b/Documentation/Architecture/Release1.3/BrowserQualification.md new file mode 100644 index 0000000..8ab63f5 --- /dev/null +++ b/Documentation/Architecture/Release1.3/BrowserQualification.md @@ -0,0 +1,91 @@ +# JSONHTML browser qualification closure + +This increment preserves the formatted and interactive implementation history. +The feature integrates upstream main `25fb04f747e583209b4c4511abf39e11ef5b4f80` +through merge `e6bca54`; the original interactive implementation remains +`d1f63624a92966ee5d1b91182dcbcdd8dacc401a`. Main and the sibling worktree were +not edited. Integration resolves CI conflicts by preserving both WASI and browser +lanes, five products, exact discovery counts and all 49 baseline metric probes. + +## Firefox and the automated matrix + +Pinned Playwright 1.62.1 uses Chromium 151.0.7922.34, WebKit 26.5 and Firefox +153.0. The 37 required cases passed: 13/12/12, with zero skips. Release-native +fixtures cover eight documents and exact JSON bytes. Final-source lane receipts +record source identities, fixture hashes, versions, assertions and actual exits. + +On macOS 27, Firefox receives an invocation-owned temporary `MOZ_APP_DATA` +directory in addition to Playwright's disposable profile. This follows the +[Mozilla app-data isolation workaround](https://bugzilla.mozilla.org/show_bug.cgi?id=2069536). +No browser signing, sandbox preference, personal profile permission or HOME value +is changed. Direct launches previously failed; that failed evidence is preserved. +The owned application-data directory is removed after browser closure. + +Firefox can attempt its automatic favicon from its internal +`resource:///modules/FaviconLoader.sys.mjs`. The document's `default-src 'none'` +correctly denies the test server's `/favicon.ico`. These precise console messages +and matching `img-src` violation events are recorded as `browserPolicyEvents`. +They are not silently discarded or reported as zero total CSP events. All other +console/page errors, CSP violations and external requests fail the case. This +classification requires the internal module, exact local favicon URL and denied +directive; it does not relax the generated document's CSP. + +Chromium supplies screenshots and print PDF evidence. Playwright WebKit is not +native Safari. Its screenshot synchronization stylesheet is prohibited by CSP; +its DOM/layout/interaction tests run without that screenshot hook. Automated +viewport tests do not qualify physical devices or VoiceOver announcements. + +## Native Safari and VoiceOver + +Native Safari 27.0 (22625.1.29.11.24), macOS 27 arm64, uses the same exact fixture +bytes. Observed served-page checks include ready controls, exact preserved number +spellings and scalar-distinct keys; collapse reports four containers; selecting +Level 2 opens ancestors and focuses its container; the 15-digit search remains +focused and finds one value; selecting the result focuses that value. Copy JSON +reports awaited browser acceptance. Saved UTF-8 JSON is exactly 181 bytes, SHA-256 +`9fc1eeffbcd7adf39e11dae20805b5d79cf17842e786ea12526252490cf2e088`. +The 10-byte Pointer download includes NUL/CR/LF and has SHA-256 +`35ebf28bfdfc5b2ca6fa66b68bd285de39ca485bb162e83b1c52e8e6a233a15e`. +Clipboard acceptance is not a guarantee about receiving application transforms. + +VoiceOver was enabled only with explicit maintainer authorization. Its first-run +welcome needed completion before the runtime started. Automation could inspect +Safari accessibility and focus, but the VoiceOver caption/rotor interface timed +out. After restoring it off, the maintainer performed the real manual check and +reported all four checks passed: labeled controls and expanded/collapsed summary +states; announced destinations after outline/search selection; search/copy/collapse +status announcements; no keyboard trap. The maintainer confirmed VoiceOver off +afterward. These are maintainer-observed speech/navigation results, not automated +caption assertions, a physical-mobile result or an exhaustive accessibility audit. + +## Script-disabled correction and print evidence + +Native Safari discovered that Pointer action buttons remained visible when +JavaScript was disabled. The fixed CSS hides them until the original script +installs handlers and marks enhancement active. The automated matrix now asserts +Pointer-action visibility in every applicable case, alongside toolbar visibility; +its 37-case cardinality is unchanged. Updated mirrors preserve exact CSP hashes. +Native Safari directly opened the corrected file: disclosures and explanatory +noscript text remain; unavailable toolbar/Pointer actions are absent. Restoring +JavaScript and reloading restores enhanced controls. The original enabled +JavaScript preference was restored; VoiceOver remains off. + +Safari's actual collapsed-tree print PDF was saved and independently inspected: +one A4 page, all values including the last null and exact number spellings, +no toolbar clutter, readable highlighted text without clipping. Its 10,937 bytes +have SHA-256 `b9edd3ae158ee468881f6fcd3cd702d55aa920adf84feac029f27d2d36a59e2a`. +This is Safari print evidence, not implementation of the future PDF preparation +API. PDF text extraction renders the combining-key glyph as its composed form; +it is not evidence of lossless PDF scalar round-trip. Exact JSON/Pointer downloads +are checked separately. These print/manual speech observations preceded the +no-script visibility fix; their normal-script layout/content/labels are unchanged. +The corrected Safari no-script and enhancement-restoration checks follow it. + +## Evidence scope + +Native/safety/docs/browser lanes refresh after the source commit. Any failing lane +remains open. Materialized receipts, screenshots, fixtures and reproduction bytes +are outside public Git. Existing sealed increment archives remain immutable. +Annotations, transfer/RTF/PDF product preparation, final mobile/portable five-product +qualification and measured final release profiles remain separate later gates. +No PR merge, release tag or publication is performed by this increment. diff --git a/Documentation/Architecture/Release1.3/InteractiveIncrement.md b/Documentation/Architecture/Release1.3/InteractiveIncrement.md index c4b0717..9d36541 100644 --- a/Documentation/Architecture/Release1.3/InteractiveIncrement.md +++ b/Documentation/Architecture/Release1.3/InteractiveIncrement.md @@ -73,3 +73,7 @@ Chromium supplies desktop/mobile screenshots. Playwright WebKit screenshot synchronization injects a `body {}` stylesheet, which the document correctly blocks; WebKit therefore uses DOM/layout/interaction checks without that screenshot hook. CSP and console assertions remain enforced, and no unsafe-inline bypass is added. + +## Subsequent browser closure + +See [BrowserQualification.md](BrowserQualification.md) for the isolated Firefox solution, all 37 executed browser cases, native Safari and maintainer-observed VoiceOver results. The original failed/unexecuted evidence above remains historical. WASI main is integrated on the feature branch only; final-source native/safety/docs/browser receipts refresh for the combined source. diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index 7a0d5cd..23d12f6 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -6,7 +6,7 @@ "captured_briefing_base": "e9c6693c7cb29df5125e1699fef70f98229ccfdf", "feature_branch": "codex/1.3-jsonhtml", "pr": null, - "release_train_dependency": "Integrate final qualified 1.2.0 Linux/WASI source before final feature gates", + "release_train_dependency": "WASI main 25fb04f integrated; final five-product portable release qualification remains a later gate", "expected_final_products": [ "JSON", "JSONSchema", @@ -24,7 +24,8 @@ "Five-product public consumer and 628 documented declarations", "JSON/JSONHTML iPhoneOS and iPhoneSimulator module/public-interface typechecks only", "12 interactive behavior functions; initial native debug/release 217 functions and 173 parameter cases passed", - "Pinned browser harness: 37 required cases, exact byte downloads and explicit unexecuted engine counts" + "Pinned browser harness: 37 required cases, exact byte downloads and explicit unexecuted engine counts", + "All 37 browser cases executed; native Safari exact downloads/focus and four maintainer-observed VoiceOver checks" ], "implementation_qualified": false, "next_increment": "Complete blocked browser/accessibility/final-platform qualification; continue separately scoped annotation and transfer/export work", @@ -54,7 +55,7 @@ "entries": 8 } ], - "updated_utc": "2026-10-13T11:26:47.617801+00:00", + "updated_utc": "2026-10-13T21:53:26.614788+00:00", "evidence_locations": [ "Scripts/ci.py --lane native --output .build/ci/jsonhtml-native-compatible", "Scripts/ci.py --lane safety --output .build/ci/jsonhtml-safety-compatible", @@ -72,8 +73,26 @@ "expected_root_functions": 217, "expected_parameter_cases": 173, "expected_browser_cases": 37, - "browser_blocker": "Pinned Firefox startup times out with host sandbox-extension/graphics errors; unexecuted cases are not successes", + "browser_blocker": "Resolved using invocation-owned MOZ_APP_DATA; earlier failed evidence retained.", "preserved_baseline_metrics": 49, "final_source_refresh": "Native/safety/docs/browser lane receipts refresh after this source commit; any failures remain open in the materialized handoff" + }, + "browser_closure": { + "upstream_main": "25fb04f747e583209b4c4511abf39e11ef5b4f80", + "integration_commit": "e6bca54", + "automated_matrix": { + "passed": 37, + "engines": { + "chromium": 13, + "webkit": 12, + "firefox": 12 + }, + "skips": 0 + }, + "voiceover": "Maintainer manual observations: four checks passed; restored off. Automated caption access unavailable.", + "evidence": "BrowserQualification.md; materialized exact-source lane receipts", + "final_source_lanes": "Refresh required after this source commit; failures remain open.", + "native_safari": "Served/file exact content, collapse/reveal/search focus, accepted copy, exact JSON/Pointer downloads, complete collapsed print; corrected no-script Pointer controls and restored enabled scripting.", + "regression_fix": "Hide Pointer actions until handlers installed; all applicable 37-case matrix entries assert visibility." } } diff --git a/Sources/JSONHTML/Assets/Interactive.css b/Sources/JSONHTML/Assets/Interactive.css index 93d003d..abf9442 100644 --- a/Sources/JSONHTML/Assets/Interactive.css +++ b/Sources/JSONHTML/Assets/Interactive.css @@ -1,2 +1,2 @@ /* SPDX-License-Identifier: Apache-2.0; Swift-JSON interactive asset v1 */ -.interactive .toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}.interactive button,.interactive input{font:inherit;padding:.35rem .55rem;border:1px solid currentColor;border-radius:.3rem;background:var(--bg);color:var(--fg)}.interactive :focus-visible{outline:3px solid var(--num);outline-offset:3px}.interactive .layout{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:1.5rem}.interactive .layout:not(:has(.outline)){grid-template-columns:minmax(0,1fr)}.interactive .outline{max-height:70vh;overflow:auto;position:sticky;top:1rem;align-self:start}.interactive a{color:var(--num);overflow-wrap:anywhere}.interactive ul{padding-left:1.1rem}.interactive .node{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.7;overflow-wrap:anywhere}.interactive .children{padding-left:1.25rem;border-left:1px solid var(--fg);margin-left:.35rem}.interactive summary{cursor:pointer}.interactive .pointer-label{font-size:.8em;opacity:.85;margin-inline:.5rem}.interactive .pointer-actions{display:inline-flex;gap:.3rem;flex-wrap:wrap}.interactive .pointer-actions button{font-size:.75rem;padding:.1rem .3rem}.interactive .search-hit{outline:2px solid var(--num);outline-offset:2px}.interactive #jh-status{min-height:1.7rem}.interactive .manual-copy textarea{display:block;width:100%;min-height:8rem;font-family:monospace}.interactive .print-json{display:none}.interactive [hidden]{display:none!important}.interactive code{white-space:pre-wrap}.interactive .search-results{max-height:16rem;overflow:auto}.interactive .kind{font-weight:600}@media(max-width:700px){body.interactive{padding:1rem}.interactive .layout{grid-template-columns:1fr}.interactive .outline{position:static;max-height:15rem}.interactive .pointer-actions{display:flex}.interactive .children{padding-left:.75rem}}@media print{.interactive .toolbar,.interactive .layout,.interactive .manual-copy,.interactive #jh-status,.interactive .search-results,.interactive noscript{display:none!important}.interactive .print-json{display:block!important}.interactive .print-json pre{white-space:pre-wrap;overflow:visible}} +.interactive .toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}.interactive button,.interactive input{font:inherit;padding:.35rem .55rem;border:1px solid currentColor;border-radius:.3rem;background:var(--bg);color:var(--fg)}.interactive :focus-visible{outline:3px solid var(--num);outline-offset:3px}.interactive .layout{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:1.5rem}.interactive .layout:not(:has(.outline)){grid-template-columns:minmax(0,1fr)}.interactive .outline{max-height:70vh;overflow:auto;position:sticky;top:1rem;align-self:start}.interactive a{color:var(--num);overflow-wrap:anywhere}.interactive ul{padding-left:1.1rem}.interactive .node{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.7;overflow-wrap:anywhere}.interactive .children{padding-left:1.25rem;border-left:1px solid var(--fg);margin-left:.35rem}.interactive summary{cursor:pointer}.interactive .pointer-label{font-size:.8em;opacity:.85;margin-inline:.5rem}.interactive:not(.enhanced) .pointer-actions{display:none}.interactive .pointer-actions{display:inline-flex;gap:.3rem;flex-wrap:wrap}.interactive .pointer-actions button{font-size:.75rem;padding:.1rem .3rem}.interactive .search-hit{outline:2px solid var(--num);outline-offset:2px}.interactive #jh-status{min-height:1.7rem}.interactive .manual-copy textarea{display:block;width:100%;min-height:8rem;font-family:monospace}.interactive .print-json{display:none}.interactive [hidden]{display:none!important}.interactive code{white-space:pre-wrap}.interactive .search-results{max-height:16rem;overflow:auto}.interactive .kind{font-weight:600}@media(max-width:700px){body.interactive{padding:1rem}.interactive .layout{grid-template-columns:1fr}.interactive .outline{position:static;max-height:15rem}.interactive .pointer-actions{display:flex}.interactive .children{padding-left:.75rem}}@media print{.interactive .toolbar,.interactive .layout,.interactive .manual-copy,.interactive #jh-status,.interactive .search-results,.interactive noscript{display:none!important}.interactive .print-json{display:block!important}.interactive .print-json pre{white-space:pre-wrap;overflow:visible}} diff --git a/Sources/JSONHTML/Assets/Interactive.js b/Sources/JSONHTML/Assets/Interactive.js index d7adbaa..45f431a 100644 --- a/Sources/JSONHTML/Assets/Interactive.js +++ b/Sources/JSONHTML/Assets/Interactive.js @@ -113,7 +113,6 @@ }; step(); }; - document.getElementById('jh-toolbar').hidden = false; query.addEventListener('input', search); document.addEventListener('click', event => { const link = event.target.closest('a[data-target]'); @@ -138,5 +137,7 @@ else if (action === 'copy-pointer-escaped') void copy(node.dataset.pointerEscaped, 'Pointer JSON string'); else if (action === 'download-pointer') download(node.dataset.pointer, 'pointer.txt'); }); + document.body.classList.add('enhanced'); + document.getElementById('jh-toolbar').hidden = false; say('Interactive controls ready. Search scope: rendered JSON key/value text.'); })(); diff --git a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift index 8ad947d..7b014ac 100644 --- a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift +++ b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift @@ -4,7 +4,7 @@ package enum JSONHTMLInteractiveAssets { package static let base64Alphabet: [UInt8] = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".utf8) package static let css = #""" /* SPDX-License-Identifier: Apache-2.0; Swift-JSON interactive asset v1 */ -.interactive .toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}.interactive button,.interactive input{font:inherit;padding:.35rem .55rem;border:1px solid currentColor;border-radius:.3rem;background:var(--bg);color:var(--fg)}.interactive :focus-visible{outline:3px solid var(--num);outline-offset:3px}.interactive .layout{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:1.5rem}.interactive .layout:not(:has(.outline)){grid-template-columns:minmax(0,1fr)}.interactive .outline{max-height:70vh;overflow:auto;position:sticky;top:1rem;align-self:start}.interactive a{color:var(--num);overflow-wrap:anywhere}.interactive ul{padding-left:1.1rem}.interactive .node{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.7;overflow-wrap:anywhere}.interactive .children{padding-left:1.25rem;border-left:1px solid var(--fg);margin-left:.35rem}.interactive summary{cursor:pointer}.interactive .pointer-label{font-size:.8em;opacity:.85;margin-inline:.5rem}.interactive .pointer-actions{display:inline-flex;gap:.3rem;flex-wrap:wrap}.interactive .pointer-actions button{font-size:.75rem;padding:.1rem .3rem}.interactive .search-hit{outline:2px solid var(--num);outline-offset:2px}.interactive #jh-status{min-height:1.7rem}.interactive .manual-copy textarea{display:block;width:100%;min-height:8rem;font-family:monospace}.interactive .print-json{display:none}.interactive [hidden]{display:none!important}.interactive code{white-space:pre-wrap}.interactive .search-results{max-height:16rem;overflow:auto}.interactive .kind{font-weight:600}@media(max-width:700px){body.interactive{padding:1rem}.interactive .layout{grid-template-columns:1fr}.interactive .outline{position:static;max-height:15rem}.interactive .pointer-actions{display:flex}.interactive .children{padding-left:.75rem}}@media print{.interactive .toolbar,.interactive .layout,.interactive .manual-copy,.interactive #jh-status,.interactive .search-results,.interactive noscript{display:none!important}.interactive .print-json{display:block!important}.interactive .print-json pre{white-space:pre-wrap;overflow:visible}} +.interactive .toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}.interactive button,.interactive input{font:inherit;padding:.35rem .55rem;border:1px solid currentColor;border-radius:.3rem;background:var(--bg);color:var(--fg)}.interactive :focus-visible{outline:3px solid var(--num);outline-offset:3px}.interactive .layout{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:1.5rem}.interactive .layout:not(:has(.outline)){grid-template-columns:minmax(0,1fr)}.interactive .outline{max-height:70vh;overflow:auto;position:sticky;top:1rem;align-self:start}.interactive a{color:var(--num);overflow-wrap:anywhere}.interactive ul{padding-left:1.1rem}.interactive .node{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.7;overflow-wrap:anywhere}.interactive .children{padding-left:1.25rem;border-left:1px solid var(--fg);margin-left:.35rem}.interactive summary{cursor:pointer}.interactive .pointer-label{font-size:.8em;opacity:.85;margin-inline:.5rem}.interactive:not(.enhanced) .pointer-actions{display:none}.interactive .pointer-actions{display:inline-flex;gap:.3rem;flex-wrap:wrap}.interactive .pointer-actions button{font-size:.75rem;padding:.1rem .3rem}.interactive .search-hit{outline:2px solid var(--num);outline-offset:2px}.interactive #jh-status{min-height:1.7rem}.interactive .manual-copy textarea{display:block;width:100%;min-height:8rem;font-family:monospace}.interactive .print-json{display:none}.interactive [hidden]{display:none!important}.interactive code{white-space:pre-wrap}.interactive .search-results{max-height:16rem;overflow:auto}.interactive .kind{font-weight:600}@media(max-width:700px){body.interactive{padding:1rem}.interactive .layout{grid-template-columns:1fr}.interactive .outline{position:static;max-height:15rem}.interactive .pointer-actions{display:flex}.interactive .children{padding-left:.75rem}}@media print{.interactive .toolbar,.interactive .layout,.interactive .manual-copy,.interactive #jh-status,.interactive .search-results,.interactive noscript{display:none!important}.interactive .print-json{display:block!important}.interactive .print-json pre{white-space:pre-wrap;overflow:visible}} """# package static let script = #""" // SPDX-License-Identifier: Apache-2.0 @@ -122,7 +122,6 @@ package enum JSONHTMLInteractiveAssets { }; step(); }; - document.getElementById('jh-toolbar').hidden = false; query.addEventListener('input', search); document.addEventListener('click', event => { const link = event.target.closest('a[data-target]'); @@ -147,9 +146,11 @@ package enum JSONHTMLInteractiveAssets { else if (action === 'copy-pointer-escaped') void copy(node.dataset.pointerEscaped, 'Pointer JSON string'); else if (action === 'download-pointer') download(node.dataset.pointer, 'pointer.txt'); }); + document.body.classList.add('enhanced'); + document.getElementById('jh-toolbar').hidden = false; say('Interactive controls ready. Search scope: rendered JSON key/value text.'); })(); """# - package static let styleHash = "ZViqigi/3L6QAgh4IHgRZSMK0MTDXXYvGytdlYTJfoQ=" - package static let scriptHash = "CefCjyzNV1pVYg7GAqXuWfG1Q+aJ7xTdozRxnHyb+H4=" + package static let styleHash = "fi44l6KCKEBAprWu6rC4hvdhuRlXkKrLPRfZZl6TXrw=" + package static let scriptHash = "JPrAwRyMk+9Ugeh/SMuDKPXdJRom5+Hq7qc4Xo4GLPs=" } diff --git a/Tools/JSONHTMLBrowser/README.md b/Tools/JSONHTMLBrowser/README.md index a4300a2..0670de3 100644 --- a/Tools/JSONHTMLBrowser/README.md +++ b/Tools/JSONHTMLBrowser/README.md @@ -26,3 +26,5 @@ Chromium supplies desktop/mobile screenshots. Playwright WebKit screenshot synchronization injects a `body {}` stylesheet, which the document correctly blocks; WebKit therefore uses DOM/layout/interaction checks without that screenshot hook. CSP and console assertions remain enforced, and no unsafe-inline bypass is added. + +On macOS, the harness gives Firefox a disposable `MOZ_APP_DATA` directory to avoid personal app-data access (Mozilla bug 2069536). Sandbox defaults remain intact. Internal automatic-favicon denials and their exact matching CSP events are retained separately in `browserPolicyEvents`; other errors/violations still fail. See [browser qualification closure](../../Documentation/Architecture/Release1.3/BrowserQualification.md). diff --git a/Tools/JSONHTMLBrowser/browser.cjs b/Tools/JSONHTMLBrowser/browser.cjs index 8ddcff5..3a8fc4b 100644 --- a/Tools/JSONHTMLBrowser/browser.cjs +++ b/Tools/JSONHTMLBrowser/browser.cjs @@ -3,6 +3,7 @@ const fs = require('node:fs'); const path = require('node:path'); const http = require('node:http'); +const os = require('node:os'); const crypto = require('node:crypto'); const assert = require('node:assert/strict'); const {pathToFileURL} = require('node:url'); @@ -26,21 +27,29 @@ const server = http.createServer((req, res) => { res.end(fs.readFileSync(path.join(fixtures, name))); }); const activeBrowsers = []; +const isolatedDataDirectories = []; const save = () => fs.writeFileSync(path.join(out, 'browser.json'), JSON.stringify(receipt, null, 2) + '\n'); async function main() { await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const origin = 'http://127.0.0.1:' + server.address().port; for (const engine of ['chromium', 'webkit', 'firefox']) { let browser; - try { browser = await playwright[engine].launch({headless: true, timeout: 30_000}); } + const launch = {headless: true, timeout: 30_000}; + if (engine === 'firefox' && process.platform === 'darwin') { + // Mozilla bug 2069536: bare launches must not access personal app-data directories. + const data = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonhtml-firefox-app-data-')); + isolatedDataDirectories.push(data); + launch.env = {...process.env, MOZ_APP_DATA: data}; + } + try { browser = await playwright[engine].launch(launch); } catch (error) { receipt.engines.push({name: engine, status: 'blocked', error: error.message, executedCases: 0, unexecutedCases: engine === 'chromium' ? 13 : 12}); save(); continue; } activeBrowsers.push(browser); - receipt.engines.push({name: engine, status: 'started', version: browser.version(), executable: path.basename(playwright[engine].executablePath())}); + receipt.engines.push({name: engine, status: 'started', version: browser.version(), executable: path.basename(playwright[engine].executablePath()), isolatedApplicationData: engine === 'firefox' && process.platform === 'darwin'}); async function run(name, fixture, protocol, javaScriptEnabled, action, extra = {}) { - const errors = [], requests = [], violations = []; + const errors = [], requests = [], violations = [], browserPolicyEvents = []; const context = await browser.newContext({javaScriptEnabled, acceptDownloads: true, viewport: {width: 1440, height: 1000}, ...extra}); await context.route('**/*', async route => { const url = route.request().url(); @@ -49,10 +58,17 @@ async function main() { }); const page = await context.newPage(); page.on('pageerror', error => errors.push(error.message)); - page.on('console', message => {if (message.type() === 'error') errors.push(message.text());}); + page.on('console', message => { + if (message.type() !== 'error') return; + const text = message.text(); + if (engine === 'firefox' && text.includes('resource:///modules/FaviconLoader.sys.mjs') && + text.includes(origin + '/favicon.ico') && text.includes('(img-src)') && text.includes("default-src 'none'")) { + browserPolicyEvents.push({kind: 'browser_automatic_favicon_denied', message: text}); + } else errors.push(text); + }); await context.addInitScript(() => { window.__violations = []; - document.addEventListener('securitypolicyviolation', e => window.__violations.push(e.violatedDirective)); + document.addEventListener('securitypolicyviolation', e => window.__violations.push({directive: e.violatedDirective, blockedURI: e.blockedURI, sourceFile: e.sourceFile})); }); const entry = {engine, name, fixture, protocol, javaScriptEnabled, status: 'running'}; receipt.cases.push(entry); save(); @@ -67,13 +83,24 @@ async function main() { while (iterator.nextNode()) count++; return count; }); assert.ok(dom <= record.domUpperBound, 'DOM bound undercounts actual nodes'); + entry.browserPolicyEvents = browserPolicyEvents; entry.actualDOMNodes = dom; entry.domUpperBound = record.domUpperBound; if (fixture !== 'formatted') { assert.equal(await page.locator('#jh-tree .node').count(), record.nodes); assert.equal(await page.locator('#jh-toolbar').isVisible(), javaScriptEnabled); + assert.ok(await page.locator('.pointer-actions').count() > 0); + assert.equal((await page.locator('.pointer-actions:visible').count()) > 0, javaScriptEnabled, 'Pointer actions require installed enhancement handlers'); } await action(page, context, record); - if (javaScriptEnabled) violations.push(...await page.evaluate(() => window.__violations)); + if (javaScriptEnabled) { + for (const event of await page.evaluate(() => window.__violations)) { + if (engine === 'firefox' && event.directive === 'img-src' && + event.blockedURI === origin + '/favicon.ico' && + browserPolicyEvents.some(e => e.kind === 'browser_automatic_favicon_denied')) { + browserPolicyEvents.push({kind: 'browser_automatic_favicon_csp_event', ...event}); + } else violations.push(event); + } + } const enhancedDOM = await page.evaluate(() => {let count = 0; const it = document.createTreeWalker(document, NodeFilter.SHOW_ALL); while (it.nextNode()) count++; return count;}); assert.ok(enhancedDOM <= record.domUpperBound, 'Enhanced DOM bound undercounts'); entry.enhancedDOMNodes = enhancedDOM; @@ -220,4 +247,4 @@ async function main() { receipt.status = 'passed'; receipt.expectedCases = 37; receipt.passedCases = 37; receipt.unexpectedSkips = 0; save(); console.log('JSONHTML_BROWSER passed engines=3 cases=37 skips=0'); } -main().catch(error => {receipt.status = 'failed'; receipt.error = error.stack; save(); console.error(error); process.exitCode = 1;}).finally(async () => {await Promise.all(activeBrowsers.map(b => b.close().catch(() => {}))); server.close();}); +main().catch(error => {receipt.status = 'failed'; receipt.error = error.stack; save(); console.error(error); process.exitCode = 1;}).finally(async () => {await Promise.all(activeBrowsers.map(b => b.close().catch(() => {}))); for (const data of isolatedDataDirectories) fs.rmSync(data, {recursive: true, force: true}); server.close();}); From 3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:09:46 +0200 Subject: [PATCH 07/59] Account for all integrated WASI tests in exact CI discovery --- .../Architecture/Release1.3/BrowserQualification.md | 12 ++++++++++++ Documentation/Architecture/Release1.3/Progress.json | 12 +++++++++++- Scripts/ci.py | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Documentation/Architecture/Release1.3/BrowserQualification.md b/Documentation/Architecture/Release1.3/BrowserQualification.md index 8ab63f5..19c9206 100644 --- a/Documentation/Architecture/Release1.3/BrowserQualification.md +++ b/Documentation/Architecture/Release1.3/BrowserQualification.md @@ -81,6 +81,18 @@ are checked separately. These print/manual speech observations preceded the no-script visibility fix; their normal-script layout/content/labels are unchanged. The corrected Safari no-script and enhancement-restoration checks follow it. +## Integrated test discovery + +The WASI parent adds three JSON functions: `retiredWorkOverflowIsAtomicAndTerminal`, +`capacityAndUInt64QuotaConfigurationBoundariesRemainDistinct` and +`UInt64QuotasCrossIntBoundaryWithoutNarrowingOrReset`. The inherited CI count +expectation remained 104, so the first combined native lane rejected a successful +Swift test exit: actual JSON 107 versus expected 104. Its failed receipt remains. +The feature corrects only this exact expectation: six bundles are 107 JSON, +51 schema, 14 regex, 12 streaming, 11 patch and 25 HTML, total **220**; the existing +173 parameter invocations and all 49 cap probes remain required. No tests are +removed, skipped, weakened or replaced with a nonzero/exit-only discovery check. + ## Evidence scope Native/safety/docs/browser lanes refresh after the source commit. Any failing lane diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index 23d12f6..a2aa14c 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -93,6 +93,16 @@ "evidence": "BrowserQualification.md; materialized exact-source lane receipts", "final_source_lanes": "Refresh required after this source commit; failures remain open.", "native_safari": "Served/file exact content, collapse/reveal/search focus, accepted copy, exact JSON/Pointer downloads, complete collapsed print; corrected no-script Pointer controls and restored enabled scripting.", - "regression_fix": "Hide Pointer actions until handlers installed; all applicable 37-case matrix entries assert visibility." + "regression_fix": "Hide Pointer actions until handlers installed; all applicable 37-case matrix entries assert visibility.", + "expected_integrated_root_functions": 220, + "exact_bundle_counts": [ + 107, + 51, + 14, + 12, + 11, + 25 + ], + "discovery_correction": "Inherited WASI three added JSON tests require 107, not 104; actual successful test exit initially failed stale harness cardinality. Failure preserved; 173 parameter cases and 49 probes unchanged." } } diff --git a/Scripts/ci.py b/Scripts/ci.py index 93d8eca..5897252 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -100,7 +100,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [13] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 4] if 'thread' in command else [11, 12, 14, 25, 51, 104] + expected = [13] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 4] if 'thread' in command else [11, 12, 14, 25, 51, 107] passed = passed and sorted(counts) == expected if '--filter' not in command and not any(x.startswith('Tools/') for x in command): passed = passed and sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log)) == 173 From f469f1d878529f300131e77903e515e9c72a5dc2 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:01:48 +0200 Subject: [PATCH 08/59] feat(cli): add bounded strict JSON command foundation --- AGENTS.md | 5 +- .../Architecture/ExtractionManifest.json | 3 +- .../Architecture/Release1.4/Acceptance.json | 1186 +++++++++++++++++ .../Architecture/Release1.4/Foundation.md | 103 ++ Documentation/Architecture/Release1.4/Plan.md | 40 + .../Architecture/Release1.4/Progress.json | 41 + .../Architecture/Release1.4/Qualification.md | 50 + Documentation/README.md | 2 + Package.swift | 7 +- README.md | 16 + Scripts/ci.py | 36 +- Scripts/qualify_cli.py | 109 ++ Scripts/qualify_cli_ownership.py | 21 + Scripts/testing_events.py | 82 ++ Scripts/verify_testing_events.py | 80 ++ .../JSON/Documentation.docc/CommandLine.md | 36 + Sources/JSON/Limits/JSONWorkMeter.swift | 19 +- Sources/JSON/Pointer/JSONPointer.swift | 29 +- Sources/JSONCLI/Main.swift | 26 + Sources/JSONCLIEngine/CLIArguments.swift | 89 ++ Sources/JSONCLIEngine/CLIDiagnostics.swift | 115 ++ Sources/JSONCLIEngine/CLIEngine.swift | 225 ++++ Sources/JSONCLIEngine/CLIFileIO.swift | 154 +++ .../JSONCLIEngine/CLIProcessArguments.swift | 46 + Sources/JSONFilterRuntime/CLIJobContext.swift | 153 +++ Sources/JSONPatch/JSONPatch.swift | 14 +- .../Compilation/JSONSchemaCompiler.swift | 7 + .../Evaluation/JSONSchemaValidator.swift | 6 +- Tests/JSONCLITests/FoundationTests.swift | 135 ++ 29 files changed, 2815 insertions(+), 20 deletions(-) create mode 100644 Documentation/Architecture/Release1.4/Acceptance.json create mode 100644 Documentation/Architecture/Release1.4/Foundation.md create mode 100644 Documentation/Architecture/Release1.4/Plan.md create mode 100644 Documentation/Architecture/Release1.4/Progress.json create mode 100644 Documentation/Architecture/Release1.4/Qualification.md create mode 100644 Scripts/qualify_cli.py create mode 100644 Scripts/qualify_cli_ownership.py create mode 100644 Scripts/testing_events.py create mode 100644 Scripts/verify_testing_events.py create mode 100644 Sources/JSON/Documentation.docc/CommandLine.md create mode 100644 Sources/JSONCLI/Main.swift create mode 100644 Sources/JSONCLIEngine/CLIArguments.swift create mode 100644 Sources/JSONCLIEngine/CLIDiagnostics.swift create mode 100644 Sources/JSONCLIEngine/CLIEngine.swift create mode 100644 Sources/JSONCLIEngine/CLIFileIO.swift create mode 100644 Sources/JSONCLIEngine/CLIProcessArguments.swift create mode 100644 Sources/JSONFilterRuntime/CLIJobContext.swift create mode 100644 Tests/JSONCLITests/FoundationTests.swift diff --git a/AGENTS.md b/AGENTS.md index 443ee58..b8aa949 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,9 @@ Swift-JSON requires Swift tools/compiler 6.4+, Swift language mode 6 and macOS/SDK 27+. Read README.md, CONTRIBUTING.md and the relevant contracts in Documentation/Architecture before changing behavior. -- Keep five public products: JSON, JSONSchema, JSONStreaming, JSONPatch and JSONHTML. - JSONHTML depends on JSON only; the maintainer-approved 1.3 addition preserves historical four-product releases. +- Keep six public products: JSON, JSONSchema, JSONStreaming, JSONPatch, JSONHTML + and the swift-json executable. JSONFilterRuntime and JSONCLIEngine are package-private. + JSONHTML depends on JSON only. The approved 1.4 CLI addition preserves historical product contracts. JSONRegexRuntime is package-private. Root package dependencies remain empty. - Preserve exact bounded decimals, decoded Unicode scalar identity, ordered objects with order-independent equality, one strict UTF-8 grammar, native diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index cbf2680..1272e09 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -195,7 +195,8 @@ "Native codec, Codable, Pointer, Patch and streaming are original implementations from the integrated contract, not automatically attributed to candidates.", "Schema graph, explicit evaluator, exact sets/assertions and scalar format recognizers are original implementations from Draft 2020-12, RFC3986/3987/3339/3492/5321/5891/5892/5893/6531/6570/6901 and fixed Unicode13 data.", "Private FIPS 180-4 SHA-256 is an original Swift implementation with checked cumulative production accounting, bounded scratch and inner cancellation. The separate trusted fixture tool mirrors its primitive core byte-for-byte; public_check verifies parity. No CryptoKit fallback or runtime dependency is introduced.", - "JSONHTML formatted rendering, original fixed CSS and shared strict-writer token classification are original Apache-2.0 code. No third-party renderer, browser numeric authority or runtime dependency is adapted." + "JSONHTML formatted rendering, original fixed CSS and shared strict-writer token classification are original Apache-2.0 code. No third-party renderer, browser numeric authority or runtime dependency is adapted.", + "CLI foundation, argv parser, POSIX adapters, prepaid bounded diagnostics and shared-meter package adapters are original Apache-2.0 Swift code. No legacy filter engine or jq source is copied. See Release1.4/Foundation.md for native test-harness reuse provenance." ], "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies." } diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json new file mode 100644 index 0000000..5b17493 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -0,0 +1,1186 @@ +{ + "schema": "swift-json-cli-acceptance/1", + "prepared": "2026-10-14", + "implementation_status": "foundation boundary only; integration unqualified", + "requirements": [ + { + "id": "CL001", + "gate": "C0", + "title": "Live source/prerequisite reconciliation", + "verification": "Record actual main/base/HTML/provider heads and compare pins; preserve active work and dirty state.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL002", + "gate": "C0", + "title": "Six public products with correct kinds", + "verification": "Package dump contains five libraries plus swift-json executable; private targets have no products.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL003", + "gate": "C0", + "title": "Empty root dependency set", + "verification": "Dump/package resolution and clean-source build prove no external dependency was introduced.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL004", + "gate": "C0", + "title": "Dependency direction and mobile consumers", + "verification": "Selected library consumers have no CLI/filter dependencies; core has no HTML/provider/UI imports.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL005", + "gate": "C0", + "title": "Forward policy and historical records", + "verification": "Update AGENTS/README/CI prospective inventory; historical releases remain accurate.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL006", + "gate": "C0", + "title": "Private compiler/IR ownership", + "verification": "Compiler probes prove immutable checked-Sendable plans and invocation-owned noncopyable context.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL007", + "gate": "C0", + "title": "Attribution and extraction manifest", + "verification": "Every actual adaptation has pinned origin/status/notices; inherited files retain identity.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL008", + "gate": "C0", + "title": "Prior brief conflict resolution", + "verification": "Record reused HTML/tab/provider APIs and authoritative amendments; no duplicate conformance or restarted portability.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL009", + "gate": "C1", + "title": "Argv parsing and limits", + "verification": "Exercise long equals, grouped Boolean flags, leading-dash filenames, --, duplicates, unknown/conflicting options and bounded args.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [ + "unknown-option" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL010", + "gate": "C1", + "title": "Truthful help/version", + "verification": "Only available qualified commands appear; missing command/config/capability is 2; help/version is 0.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL011", + "gate": "C1", + "title": "Strict native format", + "verification": "Single root, full EOF, native UTF-8/duplicate-key grammar, exact lexemes and default two-space formatting.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "format-preserved-lexemes", + "reject-duplicate-key", + "reject-trailing-root", + "reject-trailing-comma" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL012", + "gate": "C1", + "title": "Explicit presentation options", + "verification": "Compact/indent/tab/sort/number-style options preserve library defaults and scalar key order.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "conflicting-style-options" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL013", + "gate": "C1", + "title": "Syntax validation outcomes", + "verification": "Completed valid/invalid result and issues; malformed input 5, resource 6, IO 7 are separate.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL014", + "gate": "C1", + "title": "Offline schema validation", + "verification": "Compile once per job; explicit immutable resources; instance invalid 1; no fetch/default insertion.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [ + "validate-instance-invalid" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL015", + "gate": "C1", + "title": "Pointer presence", + "verification": "Present null versus missing, escaped tokens, root pointer, allow-missing exit4 and type/domain errors.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "pointer-present-null", + "pointer-missing-allowed", + "pointer-escaped-key" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL016", + "gate": "C1", + "title": "Patch transactional candidate", + "verification": "Successful operations emit full result; failed test/path never emits staged mutation or changes source.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "patch-success", + "patch-failed-test" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL017", + "gate": "C1", + "title": "Incremental local IO", + "verification": "Owned chunk reads, explicit closure, split UTF-8 and partial/no-progress writes; no read-to-end job arrays.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL018", + "gate": "C1", + "title": "Stdout/stderr separation", + "verification": "All data is clean; JSON diagnostics/reports are tagged stderr lines with no ANSI/banner.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL019", + "gate": "C1", + "title": "Shared compound accounting", + "verification": "Nested parse/validate/patch/write and multiple records cannot reset work or retained overlap; failure latches.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL020", + "gate": "C1", + "title": "Every CLI cap boundary", + "verification": "Inclusive-cap and cap+1 sweeps for all 16 CLI caps; checked counters and conversion overflow.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL021", + "gate": "C1", + "title": "Existing native limits preserved", + "verification": "49 metric inventory/meaning and existing independent boundary tests remain intact.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL022", + "gate": "C1", + "title": "Actual in-loop cancellation", + "verification": "Checkpoint rendezvous inside long owned work and source/sink suspend; no further output after terminal failure.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL023", + "gate": "C1", + "title": "Atomic file success", + "verification": "Owned same-directory stage installed only after EOF/flush/close; report committed completion.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL024", + "gate": "C1", + "title": "Atomic file failure", + "verification": "Late invalid tail/resource/cancel/IO preserves destination and removes only owned stage.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL025", + "gate": "C1", + "title": "Overwrite/path policy", + "verification": "Existing file requires --overwrite; input/output alias and output symlink reject; no silent cross-FS fallback.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL026", + "gate": "C1", + "title": "Exit precedence", + "verification": "Codes0/1/2/3/4/5/6/7/130 and completed-result precedence verified in actual subprocesses.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL027", + "gate": "C2", + "title": "Native query lexer/literals", + "verification": "Exact numbers and scalar text/keys with source byte ranges; malformed source compiles to exit3.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL028", + "gate": "C2", + "title": "Grammar precedence", + "verification": "Pipeline/comma/default/assignment/operator precedence and delimiters tested beyond old parser goldens.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "pipeline-comma-precedence" + ] + }, + { + "id": "CL029", + "gate": "C2", + "title": "Pull VM cardinality", + "verification": "Empty/one/many outputs without whole-job array; next() demand limits observable reads/retention.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL030", + "gate": "C2", + "title": "Pipeline and comma order", + "verification": "Every upstream result is evaluated and ordered; late errors do not resume a failed branch.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL031", + "gate": "C2", + "title": "Cartesian operators/constructors", + "verification": "No .first collapse; object versus arithmetic nesting orders and shared input effects are tested.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "object-product-order" + ] + }, + { + "id": "CL032", + "gate": "C2", + "title": "Navigation and wrong-type", + "verification": "Missing/null convention, has, wrong-type error, optional catch and checked indices/slices.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "missing-field-null", + "wrong-type-field", + "optional-wrong-type-empty", + "has-present-null", + "negative-index" + ] + }, + { + "id": "CL033", + "gate": "C2", + "title": "Exact Unicode identity", + "verification": "Scalar-distinct strings/keys, scalar length/slicing/order and non-BMP behavior.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "scalar-key-lookup", + "scalar-string-inequality", + "scalar-length", + "utf8-length", + "scalar-slice" + ] + }, + { + "id": "CL034", + "gate": "C2", + "title": "Truth/default/Boolean generators", + "verification": "Only false/null falsey; // handles full streams; left-driven short-circuit and if generators.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "truthiness", + "fallback-whole-generator", + "fallback-empty", + "Boolean-short-circuit", + "if-condition-stream" + ] + }, + { + "id": "CL035", + "gate": "C2", + "title": "Constructed duplicate keys", + "verification": "Explicit replace-preserve-position policy; parsed exact duplicate keys still reject.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "constructed-key-replacement" + ] + }, + { + "id": "CL036", + "gate": "C2", + "title": "Exact comparison/order", + "verification": "Metered decimal/string/array/object equality and total order with stable ties.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL037", + "gate": "C2", + "title": "Common builtin inventory", + "verification": "All C2 builtins and type/domain/empty cases; map collects and map_values updates/deletes correctly.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "empty-array-collector", + "map-all-results", + "map-values-first", + "map-values-delete", + "normals-nonzero-native" + ] + }, + { + "id": "CL038", + "gate": "C2", + "title": "Lexical bindings/arguments", + "verification": "as, --arg/argjson/rawfile/slurpfile, $ARGS, duplicate/reserved names, strict auxiliary-file admission.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "binding-original-input", + "argjson-exact" + ] + }, + { + "id": "CL039", + "gate": "C2", + "title": "Shared input cursor", + "verification": "Driver/input/inputs share consumption; EOF catchable only for input; malformed tails/IO remain fatal.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "null-input-once", + "shared-inputs" + ] + }, + { + "id": "CL040", + "gate": "C2", + "title": "JSON/raw framing", + "verification": "values/document/NDJSON and strict raw UTF-8 lines/slurp; files have independent boundaries.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "values-framing", + "adjacent-roots-rejected", + "ndjson-blank-line-rejected", + "raw-line-crlf" + ] + }, + { + "id": "CL041", + "gate": "C2", + "title": "Raw output/NUL/join", + "verification": "Correct string bytes and nonstring JSON; embedded NUL rejected; delimiters/errors/counts tested.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "raw-lf", + "raw-nul-delimiter", + "raw-nul-value-rejected", + "join-no-delimiter" + ] + }, + { + "id": "CL042", + "gate": "C2", + "title": "ASCII output and sort keys", + "verification": "Owned escaping, surrogate pairs and exact scalar order; option conflicts tested.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "ascii-surrogate-pair" + ] + }, + { + "id": "CL043", + "gate": "C2", + "title": "Query exit-status", + "verification": "Final semantic value only after completion; truthy0/empty string/collections; false/null1 and noresult4.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "exit-status-truthy-zero", + "exit-status-false", + "exit-status-empty" + ] + }, + { + "id": "CL044", + "gate": "C2", + "title": "Plan reuse and isolation", + "verification": "Concurrent independent invocations share immutable plan only; mutable iterator not concurrently accessed.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL045", + "gate": "C3", + "title": "Exact add/subtract/multiply", + "verification": "Carry/borrow/sign/zero/large integer cases against independent exact expectations.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "Cartesian-add-order", + "exact-large-integer", + "exact-decimal-add", + "exact-borrow", + "exact-multiply" + ] + }, + { + "id": "CL046", + "gate": "C3", + "title": "Numeric admission and lexemes", + "verification": "Computed values normalized with no false originalLexeme; untouched inputs preserve spelling.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL047", + "gate": "C3", + "title": "Terminating exact division", + "verification": "Reduced denominators containing only2/5, large common factors and mixed signs.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "terminating-division" + ] + }, + { + "id": "CL048", + "gate": "C3", + "title": "Repeating division default", + "verification": "1/3 and repeating values give language inexactDivision; no floating fallback.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "repeating-division-error" + ] + }, + { + "id": "CL049", + "gate": "C3", + "title": "Explicit scale/half-even", + "verification": "Required option pair, decimal places, finite caps, signed ties and report rounding counts.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "rounded-third", + "half-even-ties", + "scale-pair-required" + ] + }, + { + "id": "CL050", + "gate": "C3", + "title": "Zero division and integral remainder", + "verification": "Typed catchable domain errors; signed truncated remainder; fractional remainder rejected.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "negative-remainder", + "fractional-remainder-rejected" + ] + }, + { + "id": "CL051", + "gate": "C3", + "title": "Exact numeric builtins", + "verification": "abs/floor/ceil/trunc/round/range/add with negative and empty/domain cases; round ties away from zero.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "round-away-from-zero", + "range-negative-step", + "range-zero-step" + ] + }, + { + "id": "CL052", + "gate": "C3", + "title": "Exponent/growth preflight", + "verification": "Large sparse exponent operations fail finite caps before dense allocation; exact zero shortcut allowed.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "exponent-gap-cap" + ] + }, + { + "id": "CL053", + "gate": "C3", + "title": "Numeric work and cancellation", + "verification": "Charged GCD/division/carry inner loops; boundaries and checkpoint cancellation; terminal errors not catchable.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL054", + "gate": "C3", + "title": "32-bit arithmetic safety", + "verification": "No Int/UInt64 negation/narrowing/product overflow traps; wasm32 kernel tests or explicit gate blocker.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL055", + "gate": "C3", + "title": "No Decimal/Double authority", + "verification": "Source audit plus precision cases prove no foreign arithmetic/fallback authority.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL056", + "gate": "C4", + "title": "Reduce/foreach streams", + "verification": "Last-update state, every foreach extraction, multiple inits, empty updates and actual evaluation order.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "reduce-last-update", + "reduce-empty-update", + "foreach-every-update", + "reduce-multiple-inits" + ] + }, + { + "id": "CL057", + "gate": "C4", + "title": "Lexical filter functions", + "verification": "Filter/value parameters, arity/scopes, self-recursion, no forward refs and finite explicit frames.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "filter-parameter-code", + "forward-definition-rejected", + "recursive-definition" + ] + }, + { + "id": "CL058", + "gate": "C4", + "title": "Loops/recurse/walk", + "verification": "Correct order/null handling/parent-after-child behavior with bounded frames/work and no fixed hidden10k cap.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "walk-child-before-parent", + "recurse-null-leaves" + ] + }, + { + "id": "CL059", + "gate": "C4", + "title": "Typed try/catch/optional", + "verification": "Catch only language errors; prior yields/handler streams correct; syntax/resource/cancel/IO never suppressed.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "input-eof-catch", + "malformed-input-cannot-catch", + "repeating-division-caught", + "division-zero-caught", + "try-prior-yield" + ] + }, + { + "id": "CL060", + "gate": "C4", + "title": "Collection builtin inventory", + "verification": "sort/group/unique/min/max/by forms, stable ties, multi-key generator lists, finite materialization.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "sort-exact-integers", + "total-order", + "group-scalar-distinct" + ] + }, + { + "id": "CL061", + "gate": "C4", + "title": "Generator first/last/nth", + "verification": "Correct pull/empty behavior and completion of remaining input; no hidden job early success.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "first-generator" + ] + }, + { + "id": "CL062", + "gate": "C4", + "title": "Flatten/any/all", + "verification": "Depth bounds, empty identities, short circuit and explicit deep traversal.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "all-any-empty" + ] + }, + { + "id": "CL063", + "gate": "C4", + "title": "Literal string helpers", + "verification": "Scalar contains/search/split/join/ASCII trim/case; empty patterns/type errors and bounded scan.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "split-empty-scalars", + "index-empty-pattern" + ] + }, + { + "id": "CL064", + "gate": "C4", + "title": "Conversions/explode/implode", + "verification": "Strict tonumber, preserved tostring, non-BMP scalar ints, invalid surrogate/range/type rejection.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "explode-non-BMP", + "implode-surrogate-rejected", + "tonumber-exact", + "tonumber-whitespace-rejected" + ] + }, + { + "id": "CL065", + "gate": "C4", + "title": "Path enumeration/admission", + "verification": "Exact segments, missing paths, generated paths, shape/domain restrictions and no Pointer conflation.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "setpath-created-array" + ] + }, + { + "id": "CL066", + "gate": "C4", + "title": "Assignment = versus |=", + "verification": "Original versus current-value RHS input; generator output, first update/empty delete semantics.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "assignment-set-generator", + "assignment-update-first", + "assignment-update-delete" + ] + }, + { + "id": "CL067", + "gate": "C4", + "title": "Transactional mutation/deletion", + "verification": "Stable array removals, deduplicated targets, overlap rejection and input unchanged on failure.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "assignment-overlap-rejected" + ] + }, + { + "id": "CL068", + "gate": "C4", + "title": "Sparse path growth", + "verification": "setpath container creation/null fill bounded before allocation and overflow.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "sparse-growth-fails" + ] + }, + { + "id": "CL069", + "gate": "C4", + "title": "String interpolation streams", + "verification": "Cartesian holes/literal identity/empty hole; formatter holes only with no eager unbounded strings.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "interpolation-generators", + "interpolation-empty" + ] + }, + { + "id": "CL070", + "gate": "C4", + "title": "Bounded format encoders", + "verification": "All C4 formatters, strict base64/UTF8, flat CSV/TSV, percent/HTML/POSIX quoting; no shell execution.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "format-html", + "format-base64" + ] + }, + { + "id": "CL071", + "gate": "C4", + "title": "Regex deferral", + "verification": "Compile-time unavailable diagnostics; no ICU/ECMA substitutions or compatibility claim.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "regex-not-advertised" + ] + }, + { + "id": "CL072", + "gate": "C4", + "title": "Other exclusions", + "verification": "Modules/seq/jq-stream/dates/env/halt/remote/server/converters rejected or absent from truthful help.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "numeric-transcendental-deferred", + "env-not-available" + ] + }, + { + "id": "CL073", + "gate": "C4", + "title": "Every inherited fixture disposition", + "verification": "119+49 originals retained, actual per-row status/reason/gate/claim, exclusions separated from successes.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL074", + "gate": "C5", + "title": "Stream completion commands", + "verification": "Check/cat/root-array reader, strict line/tail behavior and completed summary exactly once.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "stream-root-array", + "stream-root-array-bad-tail" + ] + }, + { + "id": "CL075", + "gate": "C5", + "title": "Static HTML rendering", + "verification": "JSONHTML integration with no script, expanded nodes, exact values/keys, escaping and finite shared accounting.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "render-static-inert" + ] + }, + { + "id": "CL076", + "gate": "C5", + "title": "Interactive HTML rendering", + "verification": "Approved fixed CSS/JS self-contained document with inert payloads; no server or external assets.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL077", + "gate": "C5", + "title": "Text/Markdown output", + "verification": "Tab pretty body, safe JSON fence and exact tokens; shared qualified writer options/defaults unchanged.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "render-markdown" + ] + }, + { + "id": "CL078", + "gate": "C5", + "title": "Offline schema/provider commands", + "verification": "Native schema check plus qualified local OpenAI helpers; schema object not quoted string, no network/key.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "schema-provider-object-envelope" + ] + }, + { + "id": "CL079", + "gate": "C5", + "title": "Provider incompatibility and availability", + "verification": "No constraint loss/lowering; compatibility false exit1; Apple signatures guarded and advertised only when qualified.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "schema-provider-optionality-reject" + ] + }, + { + "id": "CL080", + "gate": "C5", + "title": "Native macOS executable qualification", + "verification": "Actual SDK27/Swift6.4 build/run/subprocess tests at final head.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL081", + "gate": "C5", + "title": "Linux x86_64 executable qualification", + "verification": "Actual Ubuntu24.04 x86_64 runtime and Linux/linux-safety receipts; not crosscompile-only.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL082", + "gate": "C5", + "title": "Linux ARM64 executable qualification", + "verification": "Actual Ubuntu24.04 ARM64 runtime and Linux/linux-safety receipts.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL083", + "gate": "C5", + "title": "Mobile library compatibility", + "verification": "Selected-product iOS/iPadOS consumers and existing mobile qualification preserved; no CLI process support claim.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL084", + "gate": "C5", + "title": "WASI CLI disposition", + "verification": "Explicit final supported/blocked/limited evidence with resources/argv/preopen/counters; no inherited CLI pass.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL085", + "gate": "C5", + "title": "Baseline WASI/core preservation", + "verification": "Existing all-four-product resource/integrity/49metrics/platform lanes retain exact truthful evidence.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL086", + "gate": "C5", + "title": "Installed resources/clean extraction", + "verification": "Source archive rebuilt, executable run outside repo cwd, sibling schema bundle and missing/tampered resource tests.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL087", + "gate": "C5", + "title": "Fresh oracle and comparisons", + "verification": "Pinned jq1.8.2 build/hash; native exact comparison plus byte/exit modes; missing oracle blocks that lane.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL088", + "gate": "C5", + "title": "Independent exact expectations", + "verification": "Integer/rational/Unicode/cardinality expectations separate from native self-comparison and jq floats.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL089", + "gate": "C5", + "title": "Streaming retention/performance", + "verification": "Generated chunks/slow sink/large record counts and bounded collectors; release-mode identities/variance, no blanket speedup.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL090", + "gate": "C5", + "title": "Six-product CI/documentation", + "verification": "Existing native/safety/docs plus affected Linux lanes, exact product kinds and public CLI help/docs/examples.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL091", + "gate": "C5", + "title": "Final-head review/return evidence", + "verification": "Actual PR/base/head topology, nonzero counts, immutable IDs/log hashes/fixture results, blockers and release recommendation.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL092", + "gate": "C5", + "title": "Optional parallel evaluator", + "verification": "Only independent record filters; reserved bounded tasks/reorder buffers, preserved order/errors and measured material gain.", + "obligation": "optional-extension", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL093", + "gate": "C5", + "title": "Optional Apple PDF/RTF CLI export", + "verification": "Only after existing export backend qualification; actual host, explicit async preparation/page capabilities and unsupported-host errors.", + "obligation": "optional-extension", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL094", + "gate": "C5", + "title": "Optional native binary distribution", + "verification": "Explicit packaging policy, architecture/resource/signature receipts; no publication/tag performed by this task.", + "obligation": "optional-extension", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + } + ], + "notes": [ + "Package integrity is separate from native qualification.", + "Optional extensions do not block the scoped candidate when not claimed.", + "WASI CLI supported/blocked/limited disposition is mandatory; full CLI support is separately earned." + ] +} diff --git a/Documentation/Architecture/Release1.4/Foundation.md b/Documentation/Architecture/Release1.4/Foundation.md new file mode 100644 index 0000000..9d5f7ac --- /dev/null +++ b/Documentation/Architecture/Release1.4/Foundation.md @@ -0,0 +1,103 @@ +# CLI foundation + +This feature boundary implements format, strict validation, RFC 6901 selection, +staged RFC 6902 application and HTML/text/Markdown rendering. It does not advertise +the query engine, stream/schema command adapters or provider helpers before their +own increments are qualified. The target release is 1.4.0; this is development source. + +## Composition and ownership + +`CLIJobContext: ~Copyable` lives in the private runtime target. Argv, native parsing, +number projection, registry/plan admission, validation, Patch, rendering and writing +borrow its one `JSONWorkMeter`. Package-only borrowed entry points introduce no new +public library signatures. Switching the operation domain changes subsystem work +accounting without replacing health or counters. Only per-value shape capacities +reset; consumed work never resets. Patch refunds its local staging reservation and +preserves ownership that preceded its invocation. + +CLI policy has 16 additional finite quotas. Core policy keeps exactly 49 metric +identifiers. Effective work and logical-retention ceilings are the intersection of +CLI and core ceilings. Configuration bootstraps under the finite offline profile, +then validates requested caps against already consumed work and admitted capacities. +Explicit finite overrides reuse the ledger. They cannot erase bootstrap input. + +Error/cancellation latches terminal health. Data paths cannot resume after failure. +Diagnostics reserve finite work and buffer ownership before command execution; +the bounded terminal serializer spends that reservation and cannot reopen the job. +Reports distinguish provisional results from fully accepted stdout results and +atomically installed file results. Logical retained bytes are not process RSS. + +## Native I/O + +The adapters poll nonblocking POSIX descriptors, check cancellation between polls, +and handle short reads/writes, EINTR, EAGAIN, EOF and broken pipes explicitly. +Filesystem/kernel work is foreign work; cancellation does not claim to interrupt +a filesystem operation already executing in the kernel. Borrowed stdio flags are +restored. The signal callback is explicitly `@Sendable`; a real subprocess test +caught and fixed an inferred-actor dispatch assertion on SIGINT. + +File output uses an exclusive owned temporary in the destination directory. +Installation happens after parsing, command evaluation, writing and close succeed. +No-overwrite installation uses link/unlink; explicit overwrite uses rename. +Existing destinations, final symlinks and input aliases/hard links are checked. +Failures remove only the invocation's temporary and preserve the destination. +Concurrent pathname replacement by another process is outside this adapter's +transactional guarantee. There is no cross-filesystem fallback, mkdir, fsync or +power-loss durability claim. Previously accepted stdout bytes cannot be recalled. + +## Checks + +Twelve meaningful Swift Testing functions include four parameter cases. The executable +subprocess harness has 53 cases covering exact lexemes/scalar keys, strict errors, +schema invalidity versus operational failure, bounded issues, Pointer null/absence, +root removal, failed Patch staging, renderer selection, limits/reports, filesystem +aliases, atomic cleanup and SIGINT while stdin is idle. The initial failure receipts +are retained; the corrected subprocess lane passes all 53. + +The event receipt reader is reused from the separately qualified 1.3 export work +at `1e0b86d2580bb9678039597523aa3b99d2905abf`. No export/PDF feature source is copied. +The six-product checks retain exact target/dependency/toolchain/platform assertions, +all baseline test bundle counts, 173 inherited parameter cases plus four CLI cases, +and all 49 cap probe outcomes. Native/safety/docs qualification remains required +for the committed source. + +## Compiler probes and review corrections + +The supported native compiler accepts an inout job borrow, rejects consuming the +same job twice with the expected ownership diagnostic, and rejects access from an +external package. The noncopyable initializer constructs its meter before the +other stored assignments; this follows actual Swift 6.4 initialization rules. +An initial default-CLT/cache attempt is unqualified and retained as blocked evidence; +its negative exits are not ownership passes. + +Strict memory-safety compilation marks individual synchronous POSIX pointer and +CString expressions `unsafe`. Byte pointers stay within `withUnsafeBytes` scopes; +counts are checked against the array slice, poll/stat pointers address live local +structures, and C strings are borrowed only for a synchronous syscall. No pointer +escapes, suspension crosses a borrow, unsafe compiler option or blanket annotation +is introduced. + +The SIGINT acceptance test waits for its exclusive staging file as a readiness +signal and holds stdin open until exit. Closing stdin through `communicate()` +immediately after SIGINT introduced a competing EOF and was corrected. The test +still requires exit 130, no output, and complete staging cleanup. + +The final argument adapter reads process-owned C argv under the finite bootstrap +ledger, rejects invalid UTF-8 instead of accepting replacement characters, and +checks each byte before copying. Structured diagnostic preferences survive usage +errors. Malformed/overflowing limit configuration is usage failure; exhaustion, +cancellation and I/O remain operational. Missing array Pointer targets use a +package-only presence adapter over the shared strict index decoder; malformed +indices and wrong kinds stay errors. Existing public Pointer resolution retains +its previous error contract. These review findings have dedicated regressions. + +Named inputs open with O_NONBLOCK from the initial syscall, so FIFO open does not +wait indefinitely before cancellation handling begins. Subprocess cancellation +covers both idle borrowed stdin and an owned named FIFO. Reports include actual +enforced native caps, remaining cumulative work and profile/override origin. + +The terminal diagnostic builder checks each append before growth and discards an +incomplete report before handoff. Zero forbids diagnostic output; small diagnostic +quotas cannot produce broken JSONL. HTML-specific quotas map to resource exit 6; +renderer configuration maps to usage exit 2. Typed schema compile/evaluation errors +stay operational exit 5, with stable subsystem codes. These have explicit regressions. diff --git a/Documentation/Architecture/Release1.4/Plan.md b/Documentation/Architecture/Release1.4/Plan.md new file mode 100644 index 0000000..9ae86d6 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Plan.md @@ -0,0 +1,40 @@ +# CLI release plan + +The maintainer-approved 1.4.0 train adds `swift-json` as an executable beside the +five libraries. JSONFilterRuntime and JSONCLIEngine are package-private targets; +JSONRegexRuntime remains private. Root external dependencies remain empty. +Historical four/five-product releases and their evidence are unchanged. + +The CLI stack starts at the qualified JSONHTML renderer commit +`3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7`, PR #21. Its native, safety, docs and +Linux x86_64/arm64 lanes passed. Main at orientation was +`25fb04f747e583209b4c4511abf39e11ef5b4f80`. This train uses dependent feature +branches and draft PRs; it does not change the parallel transfer/PDF or 1.2 work. + +| Gate | Cohesive implementation and acceptance | +| --- | --- | +| C0 | Source/protection/prerequisite audit, executable/private target shapes, one noncopyable job ledger | +| C1 | Bounded argv/stdio/files, diagnostics/exits, atomic output, format/validate/Pointer/Patch, six-product inventory | +| C2 | Original flat filter IR and pull VM, input framing, generators/paths/control/bindings/common builtins | +| C3 | Private exact decimal arithmetic; explicit scale plus half-even division policy | +| C4 | Qualified richer filters, lexical functions/reductions/path updates; explicit unsupported features | +| C5 | Qualified renderer/schema/provider adapters, consumers, independent corpus/oracle, platform/distribution and final candidate | + +The planned stack is foundation, query-core, arithmetic, richer filters and +integration. Each has a compiling boundary, exact acceptance receipts and a draft +PR against its actual dependency. No public query library, eager legacy evaluator, +remote resolver, shell executor, daemon or release action is introduced. + +The input briefing SHA-256 is +`43f8819c57e3d93bc6f7cf561d2d111442e089f2973cb7c62e16e3bd3fd73d0d`. +Its structural verifier counted 94 requirements, 111 new cases and 168 inherited +cases; those are inventory counts, not execution successes. The jq 1.7 observations +are historical references; differential claims require an actual pinned jq 1.8.2 +runner. Missing provider APIs/platform runners remain explicit blockers. + +Swift 6.4, language 6 and macOS/SDK 27 remain the native baseline. Mobile libraries +must continue to compile independently of the executable. Linux is a process +host; WASI executable feasibility is separate from the already qualified libraries. +Native and sanitizer receipts, subprocess outcomes, commands, toolchain, source and +binary identities accompany every qualified milestone. All 49 baseline probes and +independent-suite exclusions remain required and separate from CLI additions. diff --git a/Documentation/Architecture/Release1.4/Progress.json b/Documentation/Architecture/Release1.4/Progress.json new file mode 100644 index 0000000..176a9e8 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Progress.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "target_release": "1.4.0", + "renderer_base": "3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7", + "main_at_orientation": "25fb04f747e583209b4c4511abf39e11ef5b4f80", + "branch": "codex/1.4-json-cli-foundation", + "status": "Compiling foundation boundary qualified on native macOS; C0/C1 complete closure and C2-C5 remain pending", + "gates": { + "C0": "partial", + "C1": "partial", + "C2": "not_run", + "C3": "not_run", + "C4": "not_run", + "C5": "not_run" + }, + "foundation_functions": 12, + "foundation_parameter_cases": 4, + "subprocess_cases": 53, + "subprocess_passed": 53, + "baseline_parameter_cases": 173, + "core_metrics": 49, + "cli_metrics": 16, + "publication_performed": false, + "root_test_functions": 232, + "root_parameter_cases": 177, + "native_source_manifest_sha256": "864d9b3dd8e1d79a0a6cbec862476843ddca2ff8a1582471b80fba54b863459e", + "qualified_lanes": [ + "native", + "safety", + "docs" + ], + "remaining_foundation_requirements": [ + "clean-source reproduction", + "selected mobile SDK consumers", + "immutable filter-plan compiler probe and implemented IR", + "all 16 CLI cap boundaries", + "multi-record shared accounting", + "slow/partial/no-progress sink probes", + "query compile exit 3 and full exit precedence" + ] +} diff --git a/Documentation/Architecture/Release1.4/Qualification.md b/Documentation/Architecture/Release1.4/Qualification.md new file mode 100644 index 0000000..460214c --- /dev/null +++ b/Documentation/Architecture/Release1.4/Qualification.md @@ -0,0 +1,50 @@ +# Foundation qualification boundary + +The foundation code and harness were qualified together under source manifest +`864d9b3dd8e1d79a0a6cbec862476843ddca2ff8a1582471b80fba54b863459e`. +The receipts identify the renderer base HEAD plus the actual edited-file hashes; +this is pre-commit evidence, not a claim that the parent commit contains the CLI. +Only this ledger, progress and qualification prose were subsequently updated. +Production Swift, the package manifest and executed scripts match those receipts. + +Native environment: arm64 macOS 27.0; Swift 6.4 +(swiftlang-6.4.0.34.1), language mode 6, Xcode 27.2 (27B5019j), SDK 27.2. +The configured beta Xcode is required on this host; the default CLT attempt was +unqualified and is retained separately. No baseline or compiler check was lowered. + +| Command | Actual outcome | +| --- | --- | +| `python3 Scripts/ci.py --lane native --output .build/ci/cli-c1-reviewed-native` | Exit 0; all 18 commands passed; debug/release each 232 functions, 177 parameter invocations | +| `python3 Scripts/ci.py --lane safety --output .build/ci/cli-c1-reviewed-safety` | Exit 0; ASan 232 functions/177 parameter invocations; TSan seven shared-state functions; strict-memory-safety build with warnings as errors | +| `python3 Scripts/ci.py --lane docs --output .build/ci/cli-c1-reviewed-docs` | Exit 0; five DocC catalogues, coverage checks and compiled examples | +| `python3 Scripts/qualify_cli.py --binary .build/release/swift-json --output …` | Exit 0; 53 real subprocess cases, no exclusions | +| `python3 Scripts/qualify_cli_ownership.py --output …` | Exit 0; three compiler probes: borrow compiles; double consume and cross-package access fail with required diagnostics | + +The inherited 173 parameter cases remain unchanged; four CLI parameter cases are +additional. All 49 baseline cap probes keep their recorded outcomes. Independent +syntax qualification observes 318 cases: 287 evaluated successes and 31 policy +cases. Schema qualification observes 2,337: 2,300 evaluated successes and 37 policy +cases. Authored suites retain 82 public semantics, nine budget/counter cases and +4,396 deterministic malformed-input replays. The downstream consumer retains 20 +explicitly rooted outcomes: eight positive, twelve negative, with two operational +error outcomes distinguished from ordinary invalidity. These do not establish MCP +server conformance. + +The three final lane receipts report unchanged source inputs while running. Earlier +failed compiler, SIGINT actor-isolation, EOF-race, normalized-spelling expectation +and strict-pointer-annotation attempts are retained alongside successful corrections. +The expected normalized `1e-1` spelling follows the existing coefficient/exponent +contract; no existing test was weakened. + +This is a reviewable foundation milestone. All 94 acceptance IDs remain in +Acceptance.json. Partial/not_run rows are not successes: complete filter IR/VM, +arithmetic/richer filters, all 16 CLI cap boundaries, split/slow/no-progress I/O, +selected mobile/portable consumers and clean candidate reproduction remain pending. +The renderer prerequisite is PR #21 at `3c1e8a1`; transfer/PDF work and the parallel +1.2 train are unchanged. Provider bridge APIs are absent at this base; qualification +and integration must precede their advertisement. No tag, merge or publication is +performed by this boundary. + +Materialized receipts and their exact source-input records are in the local +foundation evidence bundle. Run the listed commands to regenerate evidence; local +build outputs and private input packets are excluded from public Git/source archives. diff --git a/Documentation/README.md b/Documentation/README.md index 44892ae..9e022d6 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -33,6 +33,8 @@ Compiled archives and public symbol graphs remain available in CI artifacts. - [Linux platform and portable schema integrity](Architecture/Linux.md) - [WebAssembly audit and unresolved qualification boundary](Architecture/WebAssemblyAudit.md) +- [Prospective 1.4 CLI plan](Architecture/Release1.4/Plan.md) and [foundation contract](Architecture/Release1.4/Foundation.md) + ## Consumers, qualification and maintenance - [Five-product compiled example](../Examples/Consumer/README.md) diff --git a/Package.swift b/Package.swift index 4fbde36..c68db49 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,8 @@ let package = Package( .library(name: "JSONSchema", targets: ["JSONSchema"]), .library(name: "JSONStreaming", targets: ["JSONStreaming"]), .library(name: "JSONPatch", targets: ["JSONPatch"]), - .library(name: "JSONHTML", targets: ["JSONHTML"]) + .library(name: "JSONHTML", targets: ["JSONHTML"]), + .executable(name: "swift-json", targets: ["JSONCLI"]) ], targets: [ .target(name: "JSON"), @@ -19,6 +20,10 @@ let package = Package( .target(name: "JSONStreaming", dependencies: ["JSON"]), .target(name: "JSONPatch", dependencies: ["JSON"]), .target(name: "JSONHTML", dependencies: ["JSON"], exclude: ["Assets"]), + .target(name: "JSONFilterRuntime", dependencies: ["JSON"]), + .target(name: "JSONCLIEngine", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch", "JSONHTML", "JSONFilterRuntime"]), + .executableTarget(name: "JSONCLI", dependencies: ["JSONCLIEngine"]), + .testTarget(name: "JSONCLITests", dependencies: ["JSONCLIEngine", "JSONFilterRuntime", "JSON", "JSONPatch", "JSONSchema", "JSONHTML"]), .testTarget(name: "JSONHTMLTests", dependencies: ["JSONHTML", "JSON"]), .testTarget(name: "JSONTests", dependencies: ["JSON"], exclude: ["ImplementationPlan.md"]), .testTarget(name: "JSONSchemaTests", dependencies: ["JSONSchema"], exclude: ["ImplementationPlan.md"]), diff --git a/README.md b/README.md index 68ea248..3f8522f 100644 --- a/README.md +++ b/README.md @@ -436,3 +436,19 @@ following [SECURITY](SECURITY.md); see [SUPPORT](SUPPORT.md) for maintenance and Original code is licensed under [Apache-2.0](LICENSE). Adapted code and qualification inputs retain their applicable [notices](NOTICE), [third-party notices](THIRD_PARTY_NOTICES.md), [licenses](Licenses) and [extraction provenance](Documentation/Architecture/ExtractionManifest.json). + +## Prospective 1.4 command-line tool + +This feature train adds `swift-json` beside the five libraries. Its compiling +foundation provides strict formatting, validation, Pointer selection, staged +Patch and qualified HTML/text/Markdown rendering. See the [CLI plan](Documentation/Architecture/Release1.4/Plan.md) +and [foundation contract](Documentation/Architecture/Release1.4/Foundation.md) +for command semantics, cumulative budgets and qualification boundaries. Query and +provider commands are advertised only after their separate gates pass. + +```sh +swift build -c release --product swift-json +.build/release/swift-json format --compact input.json +.build/release/swift-json validate --schema schema.json input.json +.build/release/swift-json render input.json --output inspection.html +``` diff --git a/Scripts/ci.py b/Scripts/ci.py index 5897252..eedfd8d 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -4,6 +4,7 @@ import argparse, datetime, hashlib, json, os, pathlib, re, subprocess, sys from archive_candidate import files from preflight import collect, collect_linux +from testing_events import read_events, ROOT_PARAMETER_CASES ROOT = pathlib.Path(__file__).resolve().parents[1] SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes|sha256InputCapacityAndConcurrentInvocationsRemainIndependent|sharedFormattedHTMLRendererHasIndependentConcurrentInvocations' @@ -24,7 +25,7 @@ def commands(lane, output): # Root behavior/corpora remain required. SwiftData and Apple SDK/lifetime # probes are separate Apple acceptance; they never become Linux passes. return [c for c in commands('native', ROOT / output) - if not any(x in c for x in ['Tools/MobileQualification', '--mode'])] + [ + if not any(x in c for x in ['Tools/MobileQualification', '--mode', 'Scripts/qualify_cli_ownership.py'])] + [ ['python3', '-B', 'Scripts/qualify_linux_reproduction.py', '--output', str(output / 'reproduction')]] if lane == 'linux-safety': # Every behavior case uses Swift Testing. Do not start the generated @@ -44,13 +45,18 @@ def commands(lane, output): ['swift', 'run', '-c', 'release', '--package-path', 'Tools/JSONHTMLBrowser', 'BrowserFixtures', str(output / 'fixtures')], ['python3', '-B', 'Scripts/qualify_html_browser.py', '--fixtures', str(output / 'fixtures'), '--output', str(output / 'browser')], ] + if lane == 'cli': + return [['swift', 'build', '-c', 'release', '--product', 'swift-json'], ['swift', 'test', '--filter', 'cli'], ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')]] if lane == 'native': return [ ['python3', '-B', 'Scripts/public_check.py'], ['swift', 'package', 'dump-package'], ['swift', 'package', 'show-dependencies', '--format', 'json'], ['swift', 'build', '-c', 'release'], + ['python3', '-B', 'Scripts/verify_testing_events.py'], ['swift', 'test'], ['swift', 'test', '-c', 'release'], + ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')], + ['python3', '-B', 'Scripts/qualify_cli_ownership.py', '--output', str(output / 'cli-ownership')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'ownership', '--output', str(output / 'ownership.json')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'reader', '--output', str(output / 'reader.json')], ['swift', 'run', '-c', 'release', '--package-path', 'Examples/Consumer', 'Consumer'], @@ -96,14 +102,17 @@ def materialized_outcomes(output): raise ValueError('Missing or failing rooted consumer outcomes') return {name: {'sha256': hashlib.sha256((output / name).read_bytes()).hexdigest()} for name in ['qualification.json', 'consumer.json', 'watchdog.json']} -def check_result(command, code, log): +def check_result(command, code, log, events=None): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 + if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [13] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 4] if 'thread' in command else [11, 12, 14, 25, 51, 107] + expected = [12] if 'cli' in command else [13] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 4] if 'thread' in command else [11, 12, 12, 14, 25, 51, 107] passed = passed and sorted(counts) == expected if '--filter' not in command and not any(x.startswith('Tools/') for x in command): - passed = passed and sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log)) == 173 + if events is not None: + passed = passed and events['module_function_counts'] == {'JSONTests':107,'JSONSchemaTests':51,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONRegexRuntimeTests':12,'JSONHTMLTests':25,'JSONCLITests':12} + 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)) == 177) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) matrix = json.loads((ROOT / 'Documentation/Qualification/LimitsMatrix.json').read_text()) @@ -111,10 +120,10 @@ def check_result(command, code, log): passed = passed and len(probes) == 49 and dict(probes) == expected_probes if command == ['swift', 'package', 'dump-package'] and passed: d = json.loads(log) - passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] - passed = passed and {p['name']: p['targets'] for p in d['products']} == {name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML']} + passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming', 'swift-json'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] + passed = passed and {p['name']: p['targets'] for p in d['products']} == dict({name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML']}, **{'swift-json': ['JSONCLI']}) targets = {t['name']: t for t in d['targets']} - passed = passed and set(targets) == {'JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] + passed = passed and set(targets) == {'JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' if command == ['swift', 'package', 'show-dependencies', '--format', 'json'] and passed: passed = json.loads(log)['dependencies'] == [] if command[:2] == ['swift', 'run'] and 'Tools/MobileQualification' in command: @@ -129,7 +138,7 @@ def check_result(command, code, log): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser'], required=True) + parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'cli'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) @@ -146,6 +155,10 @@ def save(): return 2 receipt['status'] = 'running' for index, command in enumerate(commands(args.lane, output)): + event_path = output / ('command-%02d.events.jsonl' % index) if command[:2] == ['swift', 'test'] else None + if event_path is not None: + event_path.unlink(missing_ok=True) + command = command + ['--event-stream-output-path', str(event_path.relative_to(ROOT))] print('Running ' + ' '.join(command), flush=True) log_path = output / ('command-%02d.log' % index) with log_path.open('w') as stream: @@ -157,11 +170,16 @@ def save(): code = None log = log_path.read_text(errors='replace').replace(str(ROOT), '.') log_path.write_text(log) + events = None try: - passed, counts = check_result(command, code, log) + events = read_events(event_path.read_text()) if event_path is not None else None + passed, counts = check_result(command, code, log, events=events) except (KeyError, ValueError, TypeError): passed, counts = False, [] entry = {'command': [x.replace(str(ROOT), '.') for x in command], 'exit_code': code, 'status': 'passed' if passed else 'failed', 'log': log_path.relative_to(ROOT).as_posix(), 'log_sha256': hashlib.sha256(log_path.read_bytes()).hexdigest(), 'swift_testing_function_counts': counts, 'parameterized_case_invocations': sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log))} + if events is not None: + entry['swift_testing_events'] = dict(events, path=event_path.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(event_path.read_bytes()).hexdigest()) + entry['parameterized_case_invocations'] = events['parameter_case_invocations'] receipt['commands'].append(entry) print(json.dumps(entry), flush=True) if not passed: diff --git a/Scripts/qualify_cli.py b/Scripts/qualify_cli.py new file mode 100644 index 0000000..8ce7f81 --- /dev/null +++ b/Scripts/qualify_cli.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Exercise the real CLI subprocess, exact bytes, exits, stderr and atomic files.""" +import argparse, hashlib, json, os, pathlib, signal, subprocess, tempfile, time + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser = argparse.ArgumentParser(); parser.add_argument('--binary', required=True); parser.add_argument('--output', required=True) + args = parser.parse_args(); binary = (ROOT / args.binary).resolve(); rows = [] + def run(name, argv, data=b'', exit=0, expected=None, check=None): + command = [str(binary), *argv]; process = subprocess.run(command, input=data, capture_output=True, timeout=30) + ok = process.returncode == exit and (expected is None or process.stdout == expected) + if check: ok = ok and check(process) + rows.append({'id': name, 'command': ['swift-json', *[a if isinstance(a,str) else {'bytes_hex':a.hex()} for a in argv]], 'exit_code': process.returncode, 'expected_exit': exit, + 'stdout_sha256': hashlib.sha256(process.stdout).hexdigest(), 'stdout_bytes': len(process.stdout), + 'stderr_sha256': hashlib.sha256(process.stderr).hexdigest(), 'stderr_bytes': len(process.stderr), 'status': 'passed' if ok else 'failed'}) + if not ok: print(name, process.returncode, repr(process.stdout[:250]), repr(process.stderr[:500]), flush=True) + return process + with tempfile.TemporaryDirectory(prefix='swift-json-cli-') as temp: + temp = pathlib.Path(temp) + def file(name, data): + path = temp / name; path.write_bytes(data); return str(path) + def limit(**caps): return file('limits.json', json.dumps({'version':1,'cli':caps}).encode()) + run('help', ['--help'], check=lambda p: b'format' in p.stdout and not p.stderr) + run('invalid-argv-utf8', ['format', b'\xff'], exit=2, expected=b'', check=lambda p: b'invalidArgvUTF8' in p.stderr) + run('version', ['--version'], expected=b'swift-json 1.4.0-dev\n') + run('version-json', ['--version','--json'], check=lambda p: json.loads(p.stdout)['revision'] == 'unknown') + run('missing-command', [], exit=2, expected=b'') + run('unknown-option', ['format','--unknown'], exit=2, expected=b'') + run('usage-json-diagnostic', ['format','--diagnostics=json','--unknown'], exit=2, expected=b'', check=lambda p: json.loads(p.stderr)['kind'] == 'diagnostic' and json.loads(p.stderr)['category'] == 'usage') + run('duplicate-option', ['format','--compact','--compact'], exit=2, expected=b'') + run('indent-conflict', ['format','--compact','--tab'], exit=2, expected=b'') + run('preserved-numbers', ['format','--compact'], b'[9007199254740993,0.10,-0,1e1000]', expected=b'[9007199254740993,0.10,-0,1e1000]\n') + run('normalized-numbers', ['format','--compact','--number-style=normalized'], b'[0.10,-0]', expected=b'[1e-1,0]\n') + run('tab-indentation', ['format','--tab'], b'{"a":[1]}', expected=b'{\n\t"a": [\n\t\t1\n\t]\n}\n') + scalar = '{"é":1,"é":2}'.encode() + run('scalar-distinct-keys', ['format','-cS'], scalar, expected='{"é":2,"é":1}\n'.encode()) + run('duplicate-decoded-key', ['format','--compact'], b'{"a":1,"\\u0061":2}', exit=5, expected=b'') + run('malformed-utf8', ['format'], b'"\xff"', exit=5, expected=b'') + run('trailing-root', ['format'], b'{} []', exit=5, expected=b'') + run('validation-syntax', ['validate'], b'{}', check=lambda p: json.loads(p.stdout) == {'valid':True,'issues':[],'truncated':False}) + run('validation-malformed', ['validate'], b'{', exit=5, expected=b'') + schema = file('schema.json', b'{"type":"integer","minimum":3}') + run('schema-valid', ['validate','--schema',schema], b'4', check=lambda p: json.loads(p.stdout)['valid'] is True) + run('schema-invalid', ['validate','--schema',schema], b'2', exit=1, check=lambda p: json.loads(p.stdout)['valid'] is False and len(json.loads(p.stdout)['issues']) > 0) + run('schema-bounded-issues', ['validate','--schema',schema,'--max-issues','0'], b'2', exit=1, check=lambda p: json.loads(p.stdout) == {'valid':False,'issues':[],'truncated':True}) + run('pointer-null', ['pointer','/a'], b'{"a":null}', expected=b'null\n') + run('pointer-missing', ['pointer','/a'], b'{}', exit=5, expected=b'') + run('pointer-allow-missing', ['pointer','/a','--allow-missing'], b'{}', exit=4, expected=b'') + run('pointer-array-absence', ['pointer','/0','--allow-missing'], b'[]', exit=4, expected=b'') + run('pointer-malformed-index-stays-error', ['pointer','/01','--allow-missing'], b'[]', exit=5, expected=b'') + run('pointer-wrong-type', ['pointer','/a','--allow-missing'], b'[]', exit=5, expected=b'') + run('pointer-escaped-key', ['pointer','/a~1b~0c'], b'{"a/b~c":1}', expected=b'1\n') + patch = file('patch.json', b'[{"op":"remove","path":""}]') + run('patch-root-absence', ['patch','--patch',patch], b'null', expected=b'') + patch = file('patch.json', b'[{"op":"add","path":"/b","value":9007199254740993}]') + run('patch-exact-value', ['patch','--patch',patch], b'{"a":1}', check=lambda p: b'9007199254740993' in p.stdout) + patch = file('patch.json', b'[{"op":"add","path":"/b","value":2},{"op":"test","path":"/a","value":0}]') + run('patch-failure-no-candidate', ['patch','--patch',patch], b'{"a":1}', exit=5, expected=b'') + run('html-invalid-title-is-usage', ['render','--title','\x01'], b'{}', exit=2, expected=b'') + run('static-html', ['render'], b'{"x":"\",\"n\":9007199254740993}\n", + "expected_exit": 0, + "comparison": "process-contract", + "status": "not_run", + "probe": { + "assert_no_script_element": true, + "assert_exact_value": true, + "assert_no_external_assets": true + } + }, + { + "id": "render-markdown", + "gate": "C5", + "requirement_ids": [ + "CL077" + ], + "command": "render", + "options": [ + "--format", + "markdown" + ], + "operands": [], + "stdin_utf8": "{\"a\":[1.2300e+2]}\n", + "expected_exit": 0, + "comparison": "stdout-byte-exact", + "status": "not_run", + "expected_stdout_utf8": "```json\n{\n\t\"a\": [\n\t\t1.2300e+2\n\t]\n}\n```\n" + } + ] +} diff --git a/Tests/JSONCLITests/QueryBudgetTests.swift b/Tests/JSONCLITests/QueryBudgetTests.swift new file mode 100644 index 0000000..e7e6e2f --- /dev/null +++ b/Tests/JSONCLITests/QueryBudgetTests.swift @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +@testable import JSONFilterRuntime + +@Test(arguments: CLIResourceMetric.allCases) +func cliEveryCounterHasInclusiveCheckedBoundaryAndTerminalFailure(metric: CLIResourceMetric) throws { + var limits = CLILimits(); limits[metric] = 3 + var context = try CLIJobContext(limits: limits) + try context.charge(metric, 3) + #expect(context.used(metric) == 3) + do { try context.charge(metric); Issue.record("Expected cap+1 rejection") } + catch let error as CLIError { #expect(error.category == .resource && error.metric == metric.rawValue) } + #expect(throws: CLIError.self) { try context.checkpoint() } +} +@Test(arguments: [CLIResourceMetric.querySourceBytes, .queryTokens, .astNodes, .queryDepth]) +func cliCompilerEnforcesCapsAtActualSourceTokenAndIRGrowth(metric: CLIResourceMetric) throws { + var limits = CLILimits(); limits[metric] = 1 + var allowed = try CLIJobContext(limits: limits) + let program = try FilterCompiler.compile(".", context: &allowed) + #expect(program.nodes.count == 1) + limits[metric] = 0 + var blocked = try CLIJobContext(limits: limits) + do { _ = try FilterCompiler.compile(".", context: &blocked); Issue.record("Expected actual compiler admission failure") } + catch let failure as CLIError { #expect(failure.category == .resource && failure.metric == metric.rawValue) } +} +@Test func cliParserDepthUsesConfiguredWorkStackAndRejectsWithoutRecursion() throws { + var limits = CLILimits(); limits[.queryDepth] = 64 + var context = try CLIJobContext(limits: limits) + let source = String(repeating: "(", count: 65) + "." + String(repeating: ")", count: 65) + do { _ = try FilterCompiler.compile(source, context: &context); Issue.record("Expected depth cap") } + catch let failure as CLIError { #expect(failure.metric == "queryDepth") } +} +@Test func cliRuntimeFramesAndBindingsAreLiveOwnedCaps() throws { + var limits = CLILimits(); limits[.runtimeFrames] = 0 + var context = try CLIJobContext(limits: limits) + let program = try FilterCompiler.compile(".", context: &context) + do { _ = try FilterVM(program: program, input: .init(value: .null), context: &context); Issue.record("Expected frame cap before growth") } + catch let failure as CLIError { #expect(failure.metric == "runtimeFrames") } +} +@Test func cliQueryPlanIsImmutableAndCheckedSendable() throws { + func requireSendable(_ value: T) {} + var context = try CLIJobContext() + let program = try FilterCompiler.compile(".a, .b", context: &context) + requireSendable(program) + #expect(program.nodes.count > 1 && program.sourceRanges.count == program.nodes.count) +} + +@Test func cliQueryOperationalFailureBypassesTryAndCleansInvocationStorage() throws { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile("try [1,2,3] catch null", context: &compilation) + var limits = CLILimits(); limits[.collectionValues] = 2 + var context = try CLIJobContext(limits: limits) + let baseline = context.meter.used(.retainedBytes) + var vm = try FilterVM(program: program, input: .init(value: .null), context: &context) + struct NoInput: FilterInput { mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } } + var input = NoInput() + do { _ = try vm.next(input: &input, context: &context); Issue.record("Resource failure must not become catch null") } + catch let failure as CLIError { #expect(failure.category == .resource && failure.metric == "collectionValues") } + try vm.close(context: &context) + #expect(context.used(.runtimeFrames) == 0 && context.used(.collectionValues) == 0) + #expect(context.meter.used(.retainedBytes) == baseline) + #expect(throws: CLIError.self) { try context.checkpoint() } +} +@Test func cliQueryCancellationHappensInsideOwnedEvaluationAndCleansFrames() async throws { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile(".[]", context: &compilation) + let task = Task { + var context = try CLIJobContext() + let baseline = context.meter.used(.retainedBytes) + var value = try FilterValues.produced(.array((0..<100).map { .number(JSONNumber(integer: $0)) }), context: &context) + var vm = try FilterVM(program: program, input: value, context: &context) + try value.release(context: &context) + struct NoInput: FilterInput { mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } } + var input = NoInput() + var pending = try vm.next(input: &input, context: &context) + var first = try #require(pending); pending = nil + try first.release(context: &context) + context.meter.checkpointObserver = { _ in withUnsafeCurrentTask { $0?.cancel() } } + #expect(throws: CancellationError.self) { _ = try vm.next(input: &input, context: &context) } + try vm.close(context: &context) + #expect(context.used(.runtimeFrames) == 0 && context.used(.collectionValues) == 0) + #expect(context.meter.used(.retainedBytes) == baseline) + } + try await task.value +} + +@Test func cliQueryConstructorsAndSelectorsReleaseScratchAfterMidOperationFailure() throws { + let object = try JSONParser().parse(Array("{\"a\":1,\"b\":2,\"c\":3,\"d\":4,\"e\":5,\"f\":6}".utf8)) + let array = JSON.array((0..<20).map { .number(JSONNumber(integer: $0)) }) + struct NoInput: FilterInput { mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } } + for (source, value) in [("keys", object), (".[1:15]", array), ("map_values(.)", object), ("{a: .a,b: .b}", object), ("try error(.) catch .", object)] { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile(source, context: &compilation) + var rejected = 0 + for cap: UInt64 in [360, 720, 1440, 2880] { + var limits = CLILimits(); limits[.retainedBytes] = cap + var context = try CLIJobContext(limits: limits) + var vm = try FilterVM(program: program, input: .init(value: value), context: &context) + var input = NoInput() + do { while var result = try vm.next(input: &input, context: &context) { try result.release(context: &context) } } + catch { rejected += 1 } + try vm.close(context: &context) + #expect(context.used(.runtimeFrames) == 0 && context.used(.collectionValues) == 0) + #expect(context.meter.used(.retainedBytes) == 0, "\(source) cap=\(cap)") + } + #expect(rejected > 0) + } +} diff --git a/Tests/JSONCLITests/QueryCoreTests.swift b/Tests/JSONCLITests/QueryCoreTests.swift new file mode 100644 index 0000000..ef22c05 --- /dev/null +++ b/Tests/JSONCLITests/QueryCoreTests.swift @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +@testable import JSONFilterRuntime + +private struct EmptyFilterInput: FilterInput { + mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } +} +private func evaluateFilter(_ source: String, input: String = "null") throws -> [JSON] { + var context = try CLIJobContext() + let program = try FilterCompiler.compile(source, context: &context) + let entry = context.meter.used(.retainedBytes) + let value = try JSONParser().parse(Array(input.utf8), meter: &context.meter) + let owned = context.meter.used(.retainedBytes) - entry + let lease = try CLIValueLease(ownedRetained: owned, context: &context) + var vm = try FilterVM(program: program, input: .init(value: value, lease: lease), context: &context) + defer { try? vm.close(context: &context) } + var input = EmptyFilterInput(), values: [JSON] = [] + while var result = try vm.next(input: &input, context: &context) { + values.append(result.value) + try result.release(context: &context) + } + return values +} +@Test func cliQueryCommaAndPipeAreOrderedPullStreams() throws { + #expect(try evaluateFilter("1,2 | .") == [.number(1), .number(2)]) + #expect(try evaluateFilter(".[] | .", input: "[1,2,3]") == [.number(1), .number(2), .number(3)]) + #expect(try evaluateFilter("[1,2,3]") == [.array([.number(1), .number(2), .number(3)])]) +} +@Test func cliQueryDecodedScalarKeysAndPresenceStayExact() throws { + let input = "{\"é\":1,\"é\":2,\"null\":null}" + #expect(try evaluateFilter(".[\"é\"], .[\"é\"]", input: input) == [.number(1), .number(2)]) + #expect(try evaluateFilter("has(\"null\"),has(\"missing\")", input: input) == [.boolean(true), .boolean(false)]) + #expect(try evaluateFilter(".missing?", input: input) == [.null]) +} +@Test func cliQueryWrongTypesAreCatchableButEmptyIsNotNull() throws { + #expect(try evaluateFilter(".[]?", input: "null").isEmpty) + #expect(try evaluateFilter("try .[] catch .", input: "null") == [.string("cannotIterate")]) + #expect(try evaluateFilter("empty // 9") == [.number(9)]) + #expect(try evaluateFilter("(false,null,0) // 9") == [.number(0)]) +} +@Test func cliQueryLogicalShortCircuitAndBranchesUseOriginalInput() throws { + #expect(try evaluateFilter("false and error(\"unreachable\")") == [.boolean(false)]) + #expect(try evaluateFilter("true or error(\"unreachable\")") == [.boolean(true)]) + #expect(try evaluateFilter("if (true,false) then . else null end", input: "7") == [.number(7), .null]) + #expect(try evaluateFilter("if . == 0 then 0 elif . == 1 then 2 else 3 end", input: "1") == [.number(2)]) +} +@Test func cliQueryMapsKeepAllResultsAndFirstUpdatePolicy() throws { + #expect(try evaluateFilter("map((.,.))", input: "[1,2]") == [.array([.number(1), .number(1), .number(2), .number(2)])]) + #expect(try evaluateFilter("map_values((.,9))", input: "[1,2]") == [.array([.number(1), .number(2)])]) + #expect(try evaluateFilter("map_values(empty)", input: "[1,2]") == [.array([])]) +} +@Test func cliQueryUnicodeLengthCountsScalarsAndRecursiveNullLeaves() throws { + #expect(try evaluateFilter("length,utf8bytelength", input: "\"é🦊\"") == [.number(3), .number(7)]) + #expect(try evaluateFilter("..", input: "[null,1]") == [.array([.null, .number(1)]), .null, .number(1)]) +} +@Test func cliQueryAliasesKeepSourceChargedUntilTheirLastRelease() throws { + var context = try CLIJobContext() + let baseline = context.meter.used(.retainedBytes) + var value = try FilterValues.produced(.array([.string(String(repeating: "x", count: 1000))]), context: &context) + var alias = value.selecting(value.value.arrayValue![0]) + try value.release(context: &context) + #expect(context.meter.used(.retainedBytes) > baseline) + try alias.release(context: &context) + #expect(context.meter.used(.retainedBytes) == baseline) +} +@Test func cliQueryTerminalQuotaStillReleasesValueStorage() throws { + var limits = CLILimits(); limits[.totalWorkUnits] = 2000 + var context = try CLIJobContext(limits: limits) + let baseline = context.meter.used(.retainedBytes) + var value = try FilterValues.produced(.string("owned"), context: &context) + do { try context.work(2000); Issue.record("Expected terminal work failure") } catch let failure as CLIError { #expect(failure.category == .resource) } + try value.release(context: &context) + #expect(context.meter.used(.retainedBytes) == baseline) + #expect(throws: CLIError.self) { try context.checkpoint() } +} +@Test func cliQueryObjectsUseCartesianOrderAndExactReplacement() throws { + let values = try evaluateFilter("{a:(1,2),b:(3,4)}") + let expected = try ["{\"a\":1,\"b\":3}","{\"a\":1,\"b\":4}","{\"a\":2,\"b\":3}","{\"a\":2,\"b\":4}"].map { try JSONParser().parse($0) } + #expect(values == expected) + let replacement = try evaluateFilter("{a:1,b:2,a:3}") + #expect(replacement[0].objectValue!.members.map(\.key.string) == ["a","b"]) + #expect(replacement[0].objectValue![JSONKey("a")] == .number(3)) + #expect(try evaluateFilter("{\"é\":1,\"é\":2}")[0].objectValue!.count == 2) +} +@Test func cliQuerySlicesUseScalarBoundariesAndCheckedNegativeIndices() throws { + #expect(try evaluateFilter(".[1:3]", input: "\"é🦊x\"") == [.string("́🦊")]) + #expect(try evaluateFilter(".[-2:]", input: "[1,2,3]") == [.array([.number(2),.number(3)])]) + #expect(try evaluateFilter(".[-1],.[1e1000]", input: "[1,2,3]") == [.number(3),.null]) +} +@Test func sharedFilterPlanHasIndependentConcurrentInvocations() async throws { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile(".a, .b", context: &compilation) + let outputs = try await withThrowingTaskGroup(of: [JSON].self, returning: [[JSON]].self) { group in + for _ in 0..<8 { + group.addTask { + var context = try CLIJobContext() + let input = try JSONParser().parse("{\"a\":1,\"b\":2}") + var value = try FilterValues.produced(input, context: &context) + var vm = try FilterVM(program: program, input: value, context: &context), source = EmptyFilterInput() + defer { try? vm.close(context: &context) } + try value.release(context: &context) + var results: [JSON] = [] + while var result = try vm.next(input: &source, context: &context) { results.append(result.value); try result.release(context: &context) } + #expect(context.used(.totalWorkUnits) > 0) + #expect(context.meter.used(.retainedBytes) == 0) + return results + } + } + var values: [[JSON]] = [] + for try await value in group { values.append(value) } + return values + } + #expect(outputs.count == 8 && outputs.allSatisfy { $0 == [.number(1), .number(2)] }) +} diff --git a/Tests/JSONCLITests/QueryInputTests.swift b/Tests/JSONCLITests/QueryInputTests.swift new file mode 100644 index 0000000..ab77db2 --- /dev/null +++ b/Tests/JSONCLITests/QueryInputTests.swift @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +@testable import JSONCLIEngine +@testable import JSONFilterRuntime + +private func withInputFile(_ bytes: [UInt8], _ body: (String) throws -> T) throws -> T { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("json-query-" + UUID().uuidString) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + defer { try? FileManager.default.removeItem(at: root) } + let path = root.appendingPathComponent("input.json") + try Data(bytes).write(to: path) + return try body(path.path) +} +@Test func cliQueryValueFramingRequiresWhitespaceAndIndependentFileEOF() throws { + try withInputFile(Array("1 2\n{}".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path,path]) + defer { try? cursor.close(context: &context) } + var values: [JSON] = [] + while var value = try cursor.next(context: &context) { values.append(value.value); try value.release(context: &context) } + #expect(values.count == 6 && values[0] == .number(1) && values[3] == .number(1)) + } + try withInputFile(Array("{}[]".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path]) + defer { try? cursor.close(context: &context) } + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + } +} +@Test func cliQueryDocumentWaitsForEOFAndTailErrorsAreTerminal() throws { + try withInputFile(Array("1 2".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .document) + defer { try? cursor.close(context: &context) } + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + #expect(throws: CLIError.self) { _ = try cursor.next(context: &context) } + } + try withInputFile(Array("1\n{\"bad\":]".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path]) + defer { try? cursor.close(context: &context) } + let maybeFirst = try cursor.next(context: &context); var first = try #require(maybeFirst); #expect(first.value == .number(1)); try first.release(context: &context) + #expect(throws: JSONFailure.self) { try cursor.drain(context: &context) } + } +} +@Test func cliQueryStrictNDJSONAndRawLinesPreserveTheirDifferentContracts() throws { + try withInputFile(Array("1\n\n2\n".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .ndjson) + defer { try? cursor.close(context: &context) } + let maybeFirst = try cursor.next(context: &context); var first = try #require(maybeFirst); #expect(first.value == .number(1)); try first.release(context: &context) + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + } + try withInputFile(Array("a\r\n\n🦊".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .rawLines) + defer { try? cursor.close(context: &context) } + var values: [JSON] = [] + while var value = try cursor.next(context: &context) { values.append(value.value); try value.release(context: &context) } + #expect(values == [.string("a"), .string(""), .string("🦊")]) + } +} +@Test func cliQueryRootArrayYieldsProvisionallyAndRequiresClosingEOF() throws { + try withInputFile(Array("[1,2".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .array) + defer { try? cursor.close(context: &context) } + let maybeFirst = try cursor.next(context: &context); var first = try #require(maybeFirst); #expect(first.value == .number(1)); try first.release(context: &context) + let maybeSecond = try cursor.next(context: &context); var second = try #require(maybeSecond); #expect(second.value == .number(2)); try second.release(context: &context) + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + } +} +@Test(arguments: [2000, 20000]) func cliQueryManyTinyRecordsDoNotRetainTheWholeInputJob(records: Int) throws { + try withInputFile(Array(String(repeating: "0\n", count: records).utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path]) + defer { try? cursor.close(context: &context) } + var count = 0 + while var value = try cursor.next(context: &context) { count += 1; try value.release(context: &context) } + #expect(count == records) + #expect(context.meter.used(.retainedBytes) == 0) + // One 16 KiB syscall buffer may overlap one accepted 16 KiB chunk; + // the completed scalar/lease has a small fixed reservation. This bound + // follows the actual adapter, rather than an arbitrary RSS threshold. + let chunk = min(UInt64(16384), context.limits.core[.chunkBytes]) + #expect(context.meter.statistics().peakLogicalRetainedBytes <= 2 * chunk + 256) + #expect(context.used(.totalRecords) == UInt64(records) && context.used(.totalWorkUnits) > UInt64(records)) + } +} diff --git a/Tools/CLIQualification/Package.swift b/Tools/CLIQualification/Package.swift new file mode 100644 index 0000000..7d084fa --- /dev/null +++ b/Tools/CLIQualification/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "CLIQualification", platforms: [.macOS("27.0")], + products: [.executable(name: "CLIExactCompare", targets: ["CLIExactCompare"])], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "CLIExactCompare", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONStreaming", package: "Swift-JSON")])], + swiftLanguageModes: [.v6]) diff --git a/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift new file mode 100644 index 0000000..8b656bb --- /dev/null +++ b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONStreaming +#if canImport(Darwin) +import Darwin +#else +import Glibc +#endif + +private struct Bytes: AsyncSequence { + typealias Element = JSONByteChunk + let bytes: [UInt8] + struct Iterator: AsyncIteratorProtocol { + let bytes: [UInt8] + var index = 0 + mutating func next() async -> JSONByteChunk? { + guard index < bytes.count else { return nil } + let end = Swift.min(index + 16384, bytes.count) + defer { index = end } + return .init(Array(bytes[index.. Iterator { .init(bytes: bytes) } +} +@main struct CLIExactCompare { + static func main() async { + do { + let arguments = CommandLine.arguments + guard arguments.count == 3 else { throw NSError(domain: "qualification", code: 2) } + let actual = try bounded(arguments[1]), expected = try bounded(arguments[2]) + let declaration = try JSONParser().parse(expected) + guard case .array(let texts) = declaration else { throw NSError(domain: "qualification", code: 2) } + // CLI result streams may be empty. The library's value sequence + // requires a record; verify the zero-result contract explicitly. + if texts.isEmpty { + guard actual.allSatisfy({ [9,10,13,32].contains($0) }) else { throw NSError(domain: "qualification", code: 1) } + print("NATIVE_EXACT_COMPARE passed values=0"); return + } + var iterator = JSONValueSequence(Bytes(bytes: actual), options: .init(framing: .whitespaceSeparatedValues)).makeAsyncIterator() + var count = 0 + for text in texts { + guard case .string(let raw) = text, let actual = try await iterator.next() else { throw NSError(domain: "qualification", code: 1) } + let expected = try JSONParser().parse(raw) + guard try JSONComparison.equals(actual, expected) else { throw NSError(domain: "qualification", code: 1) } + count += 1 + } + guard try await iterator.next() == nil else { throw NSError(domain: "qualification", code: 1) } + print("NATIVE_EXACT_COMPARE passed values=\(count)") + } catch { fputs("NATIVE_EXACT_COMPARE failed\n", stderr); exit(1) } + } + private static func bounded(_ path: String) throws -> [UInt8] { + let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path)) + defer { try? handle.close() } + var bytes: [UInt8] = [] + while let data = try handle.read(upToCount: 16384), !data.isEmpty { + guard bytes.count + data.count <= 33554432 else { throw NSError(domain: "qualification", code: 2) } + bytes.append(contentsOf: data) + } + return bytes + } +} From 6e8f145d84efcd60f2256f693f0bbbc38f40b16a Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:09:05 +0200 Subject: [PATCH 10/59] fix(qualification): avoid mutable Glibc stderr in exact consumer --- Documentation/Architecture/Release1.4/Qualification.md | 10 ++++++++++ .../Sources/CLIExactCompare/Main.swift | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Documentation/Architecture/Release1.4/Qualification.md b/Documentation/Architecture/Release1.4/Qualification.md index ededc33..b3d4622 100644 --- a/Documentation/Architecture/Release1.4/Qualification.md +++ b/Documentation/Architecture/Release1.4/Qualification.md @@ -79,3 +79,13 @@ source remains identical; final C5 qualification will use the integration head. The inherited 168 corpus rows and auxiliary binding modes still need their complete mode-specific dispositions. Arithmetic and advanced filters have separate gates. No simulator/device, Linux or WASI CLI success is inferred from this local run. + + +Hosted C2 Linux runs on x86_64/ARM64 passed 256 root functions/199 parameter cases, +ASan/TSan safety and the 53 CLI subprocess cases, then the comparator failed to +compile: Glibc's mutable `stderr` global is not concurrency-safe in async main. +The qualification tool now writes bounded failure text through +FileHandle.standardError. No unsafe annotation/flag or library behavior changed. +Failed run 38593758299 and its actual compiler log are retained; a fresh hosted +run is required before Linux closure. Local comparator rebuild and all 38 C2 +vectors verify the diagnostic-only fix on the supported Mac. diff --git a/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift index 8b656bb..0a9ae2f 100644 --- a/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift +++ b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift @@ -47,7 +47,7 @@ private struct Bytes: AsyncSequence { } guard try await iterator.next() == nil else { throw NSError(domain: "qualification", code: 1) } print("NATIVE_EXACT_COMPARE passed values=\(count)") - } catch { fputs("NATIVE_EXACT_COMPARE failed\n", stderr); exit(1) } + } catch { FileHandle.standardError.write(Data("NATIVE_EXACT_COMPARE failed\n".utf8)); exit(1) } } private static func bounded(_ path: String) throws -> [UInt8] { let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path)) From 10e585dec9f09d64b52df7037b9d131a578863dd Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:03:23 +0200 Subject: [PATCH 11/59] feat(cli): add bounded exact decimal arithmetic and pull range --- .../Architecture/ExtractionManifest.json | 5 +- .../Architecture/Release1.4/Acceptance.json | 76 +- .../Architecture/Release1.4/Arithmetic.md | 59 + .../Architecture/Release1.4/Progress.json | 34 +- .../Architecture/Release1.4/Qualification.md | 32 + README.md | 7 +- Scripts/ci.py | 20 +- Scripts/generate_arithmetic_oracle.py | 58 + Scripts/qualify_cli_cases.py | 12 +- Scripts/qualify_cli_limits.py | 42 + .../JSON/Documentation.docc/CommandLine.md | 25 +- Sources/JSONCLIEngine/CLIDiagnostics.swift | 24 +- Sources/JSONCLIEngine/CLIEngine.swift | 5 +- Sources/JSONCLIEngine/CLIQueryCommand.swift | 11 +- Sources/JSONFilterRuntime/CLIJobContext.swift | 19 +- .../JSONFilterRuntime/DecimalMagnitude.swift | 160 + Sources/JSONFilterRuntime/ExactDecimal.swift | 184 + .../JSONFilterRuntime/FilterCompiler.swift | 22 +- .../JSONFilterRuntime/FilterOperators.swift | 134 + Sources/JSONFilterRuntime/FilterVM.swift | 57 +- Sources/JSONFilterRuntime/FilterValues.swift | 6 + Sources/JSONFilterRuntime/RangeState.swift | 28 + Tests/Fixtures/CLI/ArithmeticOracle.json | 5858 +++++++++++++++++ Tests/JSONCLITests/ArithmeticTests.swift | 147 + 24 files changed, 6964 insertions(+), 61 deletions(-) create mode 100644 Documentation/Architecture/Release1.4/Arithmetic.md create mode 100644 Scripts/generate_arithmetic_oracle.py create mode 100644 Scripts/qualify_cli_limits.py create mode 100644 Sources/JSONFilterRuntime/DecimalMagnitude.swift create mode 100644 Sources/JSONFilterRuntime/ExactDecimal.swift create mode 100644 Sources/JSONFilterRuntime/FilterOperators.swift create mode 100644 Sources/JSONFilterRuntime/RangeState.swift create mode 100644 Tests/Fixtures/CLI/ArithmeticOracle.json create mode 100644 Tests/JSONCLITests/ArithmeticTests.swift diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 214011c..8bf1b18 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -199,5 +199,8 @@ "CLI foundation, argv parser, POSIX adapters, prepaid bounded diagnostics and shared-meter package adapters are original Apache-2.0 Swift code. No legacy filter engine or jq source is copied. See Release1.4/Foundation.md for native test-harness reuse provenance.", "Tests/Fixtures/CLI/NewCases.json is the byte-identical 111-case authored CLI briefing fixture, SHA-256 6f863f3e98d2dea12e59018e7cf3a074ae0ebee5f1ab950a651df72e5eb18a4c. Fixture expected JSON texts use native exact comparison, not a floating-point oracle. The query compiler/VM and lifetime ledger are original Apache-2.0 implementations; no legacy eager evaluator or jq implementation is copied." ], - "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies." + "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies.", + "original_notes": [ + "1.4 C3 decimal magnitude, rational/quantization kernel, pull range and type-directed overloads are original Apache-2.0 code. The 738-case deterministic oracle uses Python integer/Fraction arithmetic; no jq/Rhoe arithmetic implementation was copied." + ] } diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json index d61ac68..e9dcb45 100644 --- a/Documentation/Architecture/Release1.4/Acceptance.json +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -1,7 +1,7 @@ { "schema": "swift-json-cli-acceptance/1", "prepared": "2026-10-14", - "implementation_status": "Foundation and pull query-core milestones; complete C0-C5 integration remains unqualified", + "implementation_status": "Foundation, query core and exact arithmetic milestones; C4/C5 integration remains unqualified", "requirements": [ { "id": "CL001", @@ -666,7 +666,7 @@ "title": "Exact add/subtract/multiply", "verification": "Carry/borrow/sign/zero/large integer cases against independent exact expectations.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "Cartesian-add-order", @@ -674,6 +674,10 @@ "exact-decimal-add", "exact-borrow", "exact-multiply" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -682,9 +686,13 @@ "title": "Numeric admission and lexemes", "verification": "Computed values normalized with no false originalLexeme; untouched inputs preserve spelling.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL047", @@ -692,10 +700,14 @@ "title": "Terminating exact division", "verification": "Reduced denominators containing only2/5, large common factors and mixed signs.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "terminating-division" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -704,10 +716,14 @@ "title": "Repeating division default", "verification": "1/3 and repeating values give language inexactDivision; no floating fallback.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "repeating-division-error" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -716,12 +732,16 @@ "title": "Explicit scale/half-even", "verification": "Required option pair, decimal places, finite caps, signed ties and report rounding counts.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "rounded-third", "half-even-ties", "scale-pair-required" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -730,11 +750,15 @@ "title": "Zero division and integral remainder", "verification": "Typed catchable domain errors; signed truncated remainder; fractional remainder rejected.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "negative-remainder", "fractional-remainder-rejected" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -743,12 +767,16 @@ "title": "Exact numeric builtins", "verification": "abs/floor/ceil/trunc/round/range/add with negative and empty/domain cases; round ties away from zero.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "round-away-from-zero", "range-negative-step", "range-zero-step" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -757,10 +785,14 @@ "title": "Exponent/growth preflight", "verification": "Large sparse exponent operations fail finite caps before dense allocation; exact zero shortcut allowed.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "exponent-gap-cap" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -769,9 +801,13 @@ "title": "Numeric work and cancellation", "verification": "Charged GCD/division/carry inner loops; boundaries and checkpoint cancellation; terminal errors not catchable.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL054", @@ -779,9 +815,13 @@ "title": "32-bit arithmetic safety", "verification": "No Int/UInt64 negation/narrowing/product overflow traps; wasm32 kernel tests or explicit gate blocker.", "obligation": "mandatory", - "status": "not_run", + "status": "partial", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL055", @@ -789,9 +829,13 @@ "title": "No Decimal/Double authority", "verification": "Source audit plus precision cases prove no foreign arithmetic/fallback authority.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL056", diff --git a/Documentation/Architecture/Release1.4/Arithmetic.md b/Documentation/Architecture/Release1.4/Arithmetic.md new file mode 100644 index 0000000..2ec8f69 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Arithmetic.md @@ -0,0 +1,59 @@ +# Exact filter arithmetic + +C3 builds on query core 6e8f145 / draft PR #24. Original private code implements +base-ten unsigned working magnitudes plus native sign/coefficient/exponent values. +No Double, Float or Foundation Decimal is an arithmetic authority. Numeric binary +operators clear originalLexeme and normalize computed zero. Untouched selected +values preserve their admitted spelling; null's identity overload selects a value. + +Addition bounds exponent alignment before dense storage. Schoolbook multiplication +uses UInt16: digit product + stored digit + carry <=99; addition <=19. Subtraction +fits Int16. Long division maintains r" : ">", meter: &meter) try label(component, output: &output, meter: &meter, searchable: true) switch node { @@ -137,7 +138,9 @@ extension JSONHTMLRenderer { outline = [] try output.append("", meter: &meter) } - try output.append("
", meter: &meter)
+        try output.append("", meter: &meter)
+        try annotationSection(annotations, output: &output, meter: &meter)
+        try output.append("
", meter: &meter)
         try highlighted(value, options: writing, output: &output, meter: &meter)
         try output.append("
\"'&" + let message = "
\r\n\u{0}"\u{7f}\u{85}\u{202e}" + let root = JSON.object(try JSONObject(members: [JSONMember(key, value: .null)])) + for mode in [JSONHTMLPresentation.formatted, .interactive] { + let html = try annotated([issue([.key(JSONKey(key))], message)], mode: mode).renderString(root) + let text = try diagnostic(0, in: html) + #expect(!text.contains("")) + #expect(text.contains("<img") && text.contains("\\u000d\\u000a\\u0000")) + #expect(text.contains("&quot;")) + #expect(text.contains("\\u007f\\u0085\\u202e")) + #expect(text.contains("~1/a\\u0000\\u000d\\u000a</script>")) + #expect(!text.utf8.contains(0) && !text.utf8.contains(13)) + #expect(text.contains("data-missing=\"false\"")) + } + let empty = try annotated([]).renderString(.null) + #expect(!empty.contains("Caller diagnostics") && !empty.contains("schema validity")) +} + +@Test func annotationHTMLDocumentSpansAreCoherentAndReplacementInventsNone() throws { + let bytes = Data(" \n{\"é\":9007199254740993,\"e\u{301}\": [true,null]} \n".utf8) + let document = try JSONParser().parseDocument(bytes, source: "private-source-not-archived") + let paths: [[JSONLocationComponent]] = [[], [.key("é")], [.key("é"), .index(1)], [.key("é"), .index(9)]] + let renderer = annotated(paths.map { issue($0) }) + let html = try renderer.renderString(document) + // Known byte intervals independently derived from the exact input bytes. + let scalarStart = try #require(bytes.range(of: Data("9007199254740993".utf8))).lowerBound + let scalar = try diagnostic(1, in: html) + #expect(scalar.contains("Value source span: \(scalarStart)..<\(scalarStart+16) bytes")) + let nullStart = try #require(bytes.range(of: Data("null".utf8))).lowerBound + #expect(try diagnostic(2, in: html).contains("Value source span: \(nullStart)..<\(nullStart+4) bytes")) + let arrayStart = try #require(bytes.firstIndex(of: 91)), arrayEnd = try #require(bytes.firstIndex(of: 93)) + 1 + let parentText = try diagnostic(3, in: html) + #expect(parentText.contains("Parent source span: \(arrayStart)..<\(arrayEnd) bytes")) + #expect(!html.contains("private-source-not-archived")) + #expect(!html.contains("sourceBytes") && !html.contains("line 1")) + for unmapped in [JSONDocument(root: document.root), document.replacingRoot(document.root), document.replacingRoot(.null)] { + #expect(try !renderer.renderString(unmapped).contains("source span:")) + } +} + +@Test func annotationHTMLPublicDocumentOverloadsRetainValueFunctionShapes() throws { + let renderer = annotated([issue([])]) + let bytes: (JSON) throws -> Data = renderer.render + let text: (JSON) throws -> String = renderer.renderString + let byteReport: (JSON) throws -> JSONHTMLRenderReport = renderer.renderWithReport + let textReport: (JSON) throws -> JSONHTMLRenderReport = renderer.renderStringWithReport + let docBytes: (JSONDocument) throws -> Data = renderer.render + let docText: (JSONDocument) throws -> String = renderer.renderString + let docByteReport: (JSONDocument) throws -> JSONHTMLRenderReport = renderer.renderWithReport + let docTextReport: (JSONDocument) throws -> JSONHTMLRenderReport = renderer.renderStringWithReport + let document = JSONDocument(root: .null) + #expect(try bytes(.null) == docBytes(document)) + #expect(try Array(text(.null).utf8) == Array(docText(document).utf8)) + #expect(try byteReport(.null).statistics == docByteReport(document).statistics) + #expect(try textReport(.null).statistics == docTextReport(document).statistics) +} + +@Test func annotationHTMLCountCapsAreInclusiveAndZeroIsMeaningful() throws { + let annotations = [issue([]), issue([])] + for count: UInt64 in [1,2,3] { + var limits = JSONHTMLLimits(); limits.maxAnnotations = count + if count < 2 { + #expect(throws: JSONHTMLFailure.limitExceeded(.annotations, limit: 1, attempted: 2)) { try annotated(annotations, limits: limits).render(.null) } + } else { #expect(try annotated(annotations, limits: limits).renderWithReport(.null).statistics.annotations == 2) } + } + var limits = JSONHTMLLimits(); limits.maxAnnotations = 0 + #expect(try annotated([], limits: limits).renderWithReport(.null).statistics.annotations == 0) + #expect(throws: JSONHTMLFailure.self) { try annotated([issue([])], limits: limits).render(.null) } + limits.maxAnnotations = UInt64.max + #expect(throws: JSONHTMLFailure.invalidConfiguration) { try annotated([], limits: limits).render(.null) } + #expect(throws: JSONHTMLFailure.counterOverflow(.annotations)) { try JSONHTMLDocumentBuffer.adding(.annotations, UInt64.max, 1, UInt64.max) } +} + +@Test func annotationHTMLBoundsMessagesAndFailedPathSuffixesCumulatively() throws { + let annotations = [issue([.key("~/"), .index(123)], "message"), issue([], "other")] + let report = try annotated(annotations).renderWithReport(.null) + #expect(report.statistics.operation[.diagnosticBytes] == 17) // 7+2+3+5, both annotations and full failed path. + for cap: UInt64 in [16,17,18] { + var limits = JSONHTMLLimits(); limits.core[.diagnosticBytes] = cap + if cap < 17 { #expect(throws: JSONFailure.self) { try annotated(annotations, limits: limits).render(.null) } } + else { #expect(try annotated(annotations, limits: limits).render(.null) == report.result) } + } + for (metric, cap) in [(JSONResourceMetric.pointerTokens, UInt64(1)), (.pointerBytes, 8), (.keyBytes, 1), (.stringBytes, 6)] { + var limits = JSONHTMLLimits(); limits.core[metric] = cap + #expect(throws: JSONFailure.self) { try annotated(annotations, limits: limits).render(.null) } + } + // Invalid suffixes cannot hide behind the first missing component. + #expect(throws: JSONHTMLFailure.invalidConfiguration) { try annotated([issue([.key("absent"), .index(-1)])]).render(.null) } +} + +@Test func annotationHTMLSharesAdmissionAndFinalMaterializationBudgets() throws { + let root = try JSONParser().parse(#"{"a":[1,2]}"#) + for mode in [JSONHTMLPresentation.formatted, .interactive] { + let annotations = [issue([.key("a"), .index(1)])] + let renderer = annotated(annotations, mode: mode) + let report = try renderer.renderWithReport(root), text = try renderer.renderStringWithReport(root) + #expect(report.result == Data(text.result.utf8) && report.statistics == text.statistics) + #expect(report.statistics.operation[.nodes] == 4 && report.statistics.renderedJSONNodes == 4) + #expect(report.statistics.operation[.retainedBytes] == report.statistics.documentBytes) + for metric in [JSONResourceMetric.workUnits, .outputBytes, .retainedBytes] { + let boundary = metric == .retainedBytes ? report.statistics.operation.peakLogicalRetainedBytes : report.statistics.operation[metric] + for cap in [boundary-1,boundary,boundary+1] { + var limits = JSONHTMLLimits(); limits.core[metric] = cap + if cap < boundary { #expect(throws: JSONFailure.self) { try annotated(annotations, mode: mode, limits: limits).render(root) } } + else { #expect(try annotated(annotations, mode: mode, limits: limits).render(root) == report.result) } + } + } + } +} + +@Test func annotationHTMLBindingFailureCleansScratchAndLatchesMeter() throws { + var meter = try JSONWorkMeter(limits: .balanced, domain: .writing); try meter.retain(17) + let renderer = annotated([issue([]), issue([.key("missing"), .index(-1)])]) + #expect(throws: JSONHTMLFailure.invalidConfiguration) { try renderer.render(.null, meter: &meter) } + #expect(meter.used(.retainedBytes) == 17) + let work = meter.used(.workUnits) + #expect(throws: JSONHTMLFailure.invalidConfiguration) { try renderer.render(.null, meter: &meter) } + #expect(meter.used(.workUnits) == work) + try meter.release(17) +} + +@Test func annotationHTMLCancellationInBindingAndSourceScansIsTerminal() async throws { + let root = try JSONParser().parse(#"{"é":[1,2,3],"é":false}"#) + let document = try JSONParser().parseDocument(JSONWriter().write(root)) + for threshold: UInt64 in [64,256] { + try await Task { + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + var meter = try JSONWorkMeter(limits: limits, domain: .writing); try meter.retain(17) + meter.checkpointObserver = { work in if work >= threshold { withUnsafeCurrentTask { $0?.cancel() } } } + let renderer = annotated(Array(repeating: issue([.key("é")], String(repeating: "x", count: 100)), count: 8)) + #expect(throws: CancellationError.self) { try renderer.render(root, document: document, meter: &meter) } + #expect(meter.used(.retainedBytes) == 17 && meter.used(.outputBytes) == 0) + let work = meter.used(.workUnits) + #expect(throws: CancellationError.self) { try renderer.render(root, meter: &meter) } + #expect(meter.used(.workUnits) == work) + try meter.release(17) + }.value + } + // Rendezvous inside a real source-map helper after earlier binding has consumed work. + let wide = JSON.array(Array(repeating: .null, count: 500)) + let mapped = try JSONParser().parseDocument(JSONWriter().write(wide)) + try await Task { + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + var meter = try JSONWorkMeter(limits: limits, domain: .writing); try meter.retain(17) + try JSONAdmission.value(wide, meter: &meter) + let renderer = annotated([issue([.index(499)])]) + let records = try renderer.bindAnnotations(wide, document: nil, meter: &meter) + try meter.release(UInt64(records.count) * 64) + let earlierWork = meter.used(.workUnits) + meter.checkpointObserver = { work in if work >= earlierWork + 4 { withUnsafeCurrentTask { $0?.cancel() } } } + let map = try #require(mapped.sourceMap) + #expect(throws: CancellationError.self) { try map.span(at: [.index(499)], meter: &meter) } + #expect(meter.used(.workUnits) >= earlierWork + 4) + #expect(meter.used(.retainedBytes) == 17) + #expect(throws: CancellationError.self) { try renderer.render(wide, document: mapped, meter: &meter) } + #expect(meter.used(.retainedBytes) == 17) + try meter.release(17) + }.value +} + +@Test func sharedAnnotatedDocumentRendererHasIndependentConcurrentInvocations() async throws { + let document = try JSONParser().parseDocument(Data(#"{"é":1,"é":[2,3]}"#.utf8)) + let renderer = annotated([issue([.key("é")]), issue([.key("é"), .index(1)]), issue([.key("absent")])]) + let expected = try renderer.renderWithReport(document) + try await withThrowingTaskGroup(of: JSONHTMLRenderReport.self) { group in + for _ in 0..<16 { group.addTask { try renderer.renderWithReport(document) } } + var count = 0 + for try await actual in group { #expect(actual.result == expected.result && actual.statistics == expected.statistics); count += 1 } + #expect(count == 16) + } +} + +@Test func annotationHTMLDeepWideAndDuplicateBindingsKeepStableOrdinals() throws { + var root: JSON = .null, path: [JSONLocationComponent] = [] + for _ in 0..<60 { root = .array([root]); path.append(.index(0)) } + let annotations = [issue(path), issue(path, "second")] + let html = try annotated(annotations).renderString(root) + #expect(html.contains("id=\"jh-n-60\"") && html.contains("aria-describedby=\"jh-a-0 jh-a-1\"")) + #expect(try diagnostic(1, in: html).contains("data-target=\"jh-n-60\"")) + let wide = JSON.array(Array(repeating: .null, count: 500)) + #expect(try diagnostic(0, in: annotated([issue([.index(499)])]).renderString(wide)).contains("data-target=\"jh-n-500\"")) +} + +@Test func annotationHTMLSourceAdmissionHonorsConfiguredMappingAndInputCaps() throws { + let document = try JSONParser().parseDocument(Data(#"{"a":[1,2]}"#.utf8)) + for metric in [JSONResourceMetric.sourceMapEntries, .inputBytes] { + let boundary = metric == .sourceMapEntries ? UInt64(document.sourceMap!.entries.count) : UInt64(document.sourceBytes!.count) + for cap in [boundary-1,boundary,boundary+1] { + var limits = JSONHTMLLimits(); limits.core[metric] = cap + if cap < boundary { #expect(throws: JSONFailure.self) { try annotated([issue([.key("a")])], limits: limits).render(document) } } + else { #expect(try annotated([issue([.key("a")])], limits: limits).renderString(document).contains("Value source span:")) } + } + } +} diff --git a/Tests/JSONHTMLTests/FormattedRendererTests.swift b/Tests/JSONHTMLTests/FormattedRendererTests.swift index 3beabc4..a8cfdea 100644 --- a/Tests/JSONHTMLTests/FormattedRendererTests.swift +++ b/Tests/JSONHTMLTests/FormattedRendererTests.swift @@ -98,6 +98,7 @@ private func body(_ html: String) throws -> [UInt8] { case .domNodes: report.statistics.domNodeUpperBound case .outlineEntries: report.statistics.outlineEntries case .metadataBytes: report.statistics.metadataBytes + case .annotations: report.statistics.annotations } for delta in [-1, 0, 1] { var limits = JSONHTMLLimits() @@ -108,6 +109,7 @@ private func body(_ html: String) throws -> [UInt8] { case .domNodes: limits.maxDOMNodes = cap case .outlineEntries: limits.maxOutlineEntries = cap case .metadataBytes: limits.maxMetadataBytes = cap + case .annotations: limits.maxAnnotations = cap } let renderer = JSONHTMLRenderer(options: .init(limits: limits)) if delta < 0 { diff --git a/Tests/JSONHTMLTests/InteractiveRendererTests.swift b/Tests/JSONHTMLTests/InteractiveRendererTests.swift index f4ae226..c3e4225 100644 --- a/Tests/JSONHTMLTests/InteractiveRendererTests.swift +++ b/Tests/JSONHTMLTests/InteractiveRendererTests.swift @@ -81,6 +81,7 @@ private func exact(_ root: JSON) throws -> [UInt8] { switch metric { case .outlineEntries: boundary = report.statistics.outlineEntries case .metadataBytes: boundary = report.statistics.metadataBytes + case .annotations: boundary = report.statistics.annotations case .documentBytes: boundary = report.statistics.documentBytes case .domNodes: boundary = report.statistics.domNodeUpperBound case .renderedJSONNodes: boundary = report.statistics.renderedJSONNodes @@ -90,6 +91,7 @@ private func exact(_ root: JSON) throws -> [UInt8] { switch metric { case .outlineEntries: limits.maxOutlineEntries = cap case .metadataBytes: limits.maxMetadataBytes = cap + case .annotations: limits.maxAnnotations = cap case .documentBytes: limits.maxDocumentBytes = cap case .domNodes: limits.maxDOMNodes = cap case .renderedJSONNodes: limits.maxRenderedJSONNodes = cap diff --git a/Tools/JSONHTMLBrowser/README.md b/Tools/JSONHTMLBrowser/README.md index 0670de3..5d1cb36 100644 --- a/Tools/JSONHTMLBrowser/README.md +++ b/Tools/JSONHTMLBrowser/README.md @@ -1,7 +1,7 @@ # Browser qualification The public fixture generator imports JSON and JSONHTML only and writes eight -complete HTML documents, exact native JSON bytes and cumulative resource counts. +10 complete HTML documents (including mapped typed diagnostics), exact native JSON bytes and cumulative resource counts. The root package still has no external dependencies. Playwright 1.62.1 is a pinned qualification tool; install it in this directory and install its Chromium, Firefox and WebKit browsers. Alternatively set `JSONHTML_NODE` to an available @@ -9,7 +9,7 @@ Node executable and `PLAYWRIGHT_MODULE` to the installed pinned package. Run `Scripts/ci.py --lane browser` with the supported native toolchain selected. The lane verifies fixed asset hashes, generates release-native fixtures and runs -37 explicitly discovered browser cases. Missing browsers fail rather than becoming +46 explicitly discovered browser cases. Missing browsers fail rather than becoming skipped passes. Receipts include engine versions, fixture hashes, DOM observations, console/CSP/network errors, screenshots and a Chromium print PDF. diff --git a/Tools/JSONHTMLBrowser/Sources/BrowserFixtures/main.swift b/Tools/JSONHTMLBrowser/Sources/BrowserFixtures/main.swift index bbf7a69..c9a64ae 100644 --- a/Tools/JSONHTMLBrowser/Sources/BrowserFixtures/main.swift +++ b/Tools/JSONHTMLBrowser/Sources/BrowserFixtures/main.swift @@ -11,6 +11,15 @@ let hostile: JSON = .string("&\u{2028}\u{2029 var deep: JSON = .string("last-sentinel"); for _ in 0..<40 { deep = .array([deep]) } let wide: JSON = .array((0..<150).map { .string("literal [needle] \($0)") }) var zero = JSONHTMLRenderingOptions(presentation: .interactive); zero.limits.maxSearchQueryBytes = 0 +var annotations = JSONHTMLRenderingOptions(presentation: .interactive, initiallyExpandedDepth: 0) +annotations.annotations = [ + .init(path: [.key("é")], severity: .error, message: "
\u{0}\r\n"), + .init(path: [.key("é")], severity: .warning, message: "Scalar-distinct target"), + .init(path: [.key("needle"), .index(99), .key("0")], severity: .error, message: "Missing target"), + .init(path: [.index(0)], severity: .information, message: "Numeric root mismatch"), + .init(path: [.key("~1/a\u{0}\r\n"), .index(0), .key("0")], severity: .error, message: "Exact control path")] +var formattedAnnotations = annotations; formattedAnnotations.presentation = .formatted +let document = try JSONParser().parseDocument(JSONWriter(options: .init(indentation: 2, numberStyle: .preserved)).write(exact), source: "private-label-not-emitted") let fixtures: [(String, JSON, JSONHTMLRenderingOptions)] = [ ("exact", exact, .init(title: "Exact offline JSON", presentation: .interactive)), ("hostile", hostile, .init(title: "", presentation: .interactive)), @@ -19,15 +28,20 @@ let fixtures: [(String, JSON, JSONHTMLRenderingOptions)] = [ ("null", .null, .init(presentation: .interactive)), ("zero-query", exact, zero), ("no-outline", exact, .init(presentation: .interactive, outlineDepth: nil)), - ("formatted", exact, .init())] + ("formatted", exact, .init()), + ("annotated", exact, annotations), + ("annotated-formatted", exact, formattedAnnotations)] var records: [JSON] = [] for (name, root, options) in fixtures { - let report = try JSONHTMLRenderer(options: options).renderWithReport(root) + let renderer = JSONHTMLRenderer(options: options) + let report = try name.hasPrefix("annotated") ? renderer.renderWithReport(document) : renderer.renderWithReport(root) try report.result.write(to: output.appendingPathComponent(name + ".html")) let bytes = try JSONWriter(options: .init(indentation: options.indentation, numberStyle: .preserved, indentationUnit: options.indentationUnit)).write(root) try bytes.write(to: output.appendingPathComponent(name + ".json")) records.append(try object([ JSONMember("name", value: .string(name)), + JSONMember("presentation", value: .string(options.presentation.rawValue)), + JSONMember("annotations", value: .number(JSONNumber(integer: report.statistics.annotations))), JSONMember("nodes", value: .number(JSONNumber(integer: report.statistics.renderedJSONNodes))), JSONMember("domUpperBound", value: .number(JSONNumber(integer: report.statistics.domNodeUpperBound))), JSONMember("documentBytes", value: .number(JSONNumber(integer: report.statistics.documentBytes))), @@ -36,4 +50,4 @@ for (name, root, options) in fixtures { JSONMember("logicalPeakBytes", value: .number(JSONNumber(integer: report.statistics.operation.peakLogicalRetainedBytes))) ])) } try JSONWriter(options: .init(indentation: 2)).write(.array(records)).write(to: output.appendingPathComponent("fixtures.json")) -print("JSONHTML_BROWSER_FIXTURES passed count=8") +print("JSONHTML_BROWSER_FIXTURES passed count=10") diff --git a/Tools/JSONHTMLBrowser/browser.cjs b/Tools/JSONHTMLBrowser/browser.cjs index 3a8fc4b..598a8e3 100644 --- a/Tools/JSONHTMLBrowser/browser.cjs +++ b/Tools/JSONHTMLBrowser/browser.cjs @@ -13,7 +13,7 @@ assert.equal(version, '1.62.1', 'Qualification requires the pinned Playwright ve const fixtures = path.resolve(process.argv[2]), out = path.resolve(process.argv[3]); fs.mkdirSync(out, {recursive: true}); const records = JSON.parse(fs.readFileSync(path.join(fixtures, 'fixtures.json'))); -assert.equal(records.length, 8); +assert.equal(records.length, 10); const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); const receipt = {status: 'running', playwright: version, fixtures: [], engines: [], cases: [], limitations: ['Screenshots are captured in Chromium; WebKit screenshots would inject a CSP-blocked synchronization stylesheet. WebKit DOM/layout/interaction checks still enforce CSP.', 'Playwright WebKit is not native Safari qualification.', 'Viewport tests do not qualify physical mobile browsers or VoiceOver.', 'Clipboard API acceptance is separate from platform clipboard transformations; exact downloads are independently compared.', 'Search chunking is not an instruction, CPU, heap or process RSS guarantee.']}; for (const name of fs.readdirSync(fixtures).sort()) { @@ -43,7 +43,7 @@ async function main() { } try { browser = await playwright[engine].launch(launch); } catch (error) { - receipt.engines.push({name: engine, status: 'blocked', error: error.message, executedCases: 0, unexecutedCases: engine === 'chromium' ? 13 : 12}); + receipt.engines.push({name: engine, status: 'blocked', error: error.message, executedCases: 0, unexecutedCases: engine === 'chromium' ? 16 : 15}); save(); continue; } activeBrowsers.push(browser); @@ -85,7 +85,7 @@ async function main() { assert.ok(dom <= record.domUpperBound, 'DOM bound undercounts actual nodes'); entry.browserPolicyEvents = browserPolicyEvents; entry.actualDOMNodes = dom; entry.domUpperBound = record.domUpperBound; - if (fixture !== 'formatted') { + if (record.presentation === 'interactive') { assert.equal(await page.locator('#jh-tree .node').count(), record.nodes); assert.equal(await page.locator('#jh-toolbar').isVisible(), javaScriptEnabled); assert.ok(await page.locator('.pointer-actions').count() > 0); @@ -221,6 +221,40 @@ async function main() { await page.evaluate(() => {const q = document.getElementById('jh-query'); q.value = '\ud800'; q.dispatchEvent(new Event('input'));}); await status(page, 'Unicode scalar text'); }); + for (const protocol of ['http', 'file']) await run('typed-annotations-' + protocol, 'annotated', protocol, true, async page => { + assert.equal(await page.locator('[id^="jh-a-"]').count(), 5); + assert.equal(await page.locator('img').count(), 0); + assert.equal(await page.locator('script').count(), 1); + assert.equal(await page.locator('#jh-a-0 a').getAttribute('data-target'), 'jh-n-4'); + assert.equal(await page.locator('#jh-a-1 a').getAttribute('data-target'), 'jh-n-5'); + assert.equal(await page.locator('#jh-a-2').getAttribute('data-missing'), 'true'); + assert.equal(await page.locator('#jh-a-2 a').getAttribute('data-target'), 'jh-n-6'); + assert.ok((await page.locator('#jh-a-2').textContent()).includes('Parent source span:')); + assert.ok((await page.locator('#jh-a-2').textContent()).includes('index [99] / key "0"')); + assert.ok((await page.locator('#jh-a-0').textContent()).includes('\\u0000\\u000d\\u000a')); + assert.equal(await page.locator('#jh-a-4 a').getAttribute('data-target'), 'jh-n-3'); + const expected = await page.locator('#jh-a-4').textContent(); + assert.ok(expected.includes('~1/a\\u0000\\u000d\\u000a')); + await page.locator('#jh-a-4 a').click(); + assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-n-3'); + assert.equal(await page.locator('#jh-n-0').getAttribute('open') !== null, true); + assert.equal(await page.locator('#jh-n-1').getAttribute('open') !== null, true); + assert.equal(await page.locator('#jh-n-3').getAttribute('aria-describedby'), 'jh-a-4'); + const download = await downloaded(page, '[data-action="download-json"]'); + assert.deepEqual(download.bytes, fs.readFileSync(path.join(fixtures, 'annotated.json'))); + assert.equal(await page.evaluate(() => document.body.textContent.includes('private-label-not-emitted')), false); + }); + await run('formatted-annotations-no-script', 'annotated-formatted', 'file', false, async page => { + assert.equal(await page.locator('script').count(), 0); + assert.equal(await page.locator('pre code').textContent(), fs.readFileSync(path.join(fixtures, 'annotated-formatted.json'), 'utf8')); + assert.equal(await page.locator('#jh-n-4').getAttribute('aria-describedby'), 'jh-a-0'); + assert.equal(await page.locator('#jh-a-3').getAttribute('data-missing'), 'true'); + assert.equal(await page.locator('#jh-a-3 a').getAttribute('href'), '#jh-n-0'); + await page.locator('#jh-a-4 a').click(); + assert.ok(page.url().endsWith('#jh-n-3')); + await page.emulateMedia({media: 'print'}); + assert.ok(await page.locator('#jh-a-4').isVisible()); + }); if (engine === 'chromium') await run('native-browser-clipboard', 'exact', 'http', true, async (page, context) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']); // Keep previous clipboard text in browser memory only; never put it in receipts. @@ -239,12 +273,12 @@ async function main() { }); await browser.close(); } - receipt.expectedCases = 37; receipt.passedCases = receipt.cases.filter(c => c.status === 'passed').length; + receipt.expectedCases = 46; receipt.passedCases = receipt.cases.filter(c => c.status === 'passed').length; receipt.unexecutedCases = receipt.engines.reduce((sum, e) => sum + (e.unexecutedCases || 0), 0); if (receipt.unexecutedCases) {receipt.status = 'blocked'; save(); console.error('Browser startup blocked; unexecuted cases=' + receipt.unexecutedCases); process.exitCode = 2; return;} - assert.equal(receipt.cases.length, 37); - assert.equal(receipt.cases.filter(c => c.status === 'passed').length, 37); - receipt.status = 'passed'; receipt.expectedCases = 37; receipt.passedCases = 37; receipt.unexpectedSkips = 0; - save(); console.log('JSONHTML_BROWSER passed engines=3 cases=37 skips=0'); + assert.equal(receipt.cases.length, 46); + assert.equal(receipt.cases.filter(c => c.status === 'passed').length, 46); + receipt.status = 'passed'; receipt.expectedCases = 46; receipt.passedCases = 46; receipt.unexpectedSkips = 0; + save(); console.log('JSONHTML_BROWSER passed engines=3 cases=46 skips=0'); } main().catch(error => {receipt.status = 'failed'; receipt.error = error.stack; save(); console.error(error); process.exitCode = 1;}).finally(async () => {await Promise.all(activeBrowsers.map(b => b.close().catch(() => {}))); for (const data of isolatedDataDirectories) fs.rmSync(data, {recursive: true, force: true}); server.close();}); From 58b07f7e79e31badce6c765fd269d98f3f8786fa Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:47:44 +0200 Subject: [PATCH 31/59] docs(jsonhtml): correct browser fixture inventory --- Tools/JSONHTMLBrowser/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tools/JSONHTMLBrowser/README.md b/Tools/JSONHTMLBrowser/README.md index 5d1cb36..37451a6 100644 --- a/Tools/JSONHTMLBrowser/README.md +++ b/Tools/JSONHTMLBrowser/README.md @@ -1,7 +1,7 @@ # Browser qualification -The public fixture generator imports JSON and JSONHTML only and writes eight -10 complete HTML documents (including mapped typed diagnostics), exact native JSON bytes and cumulative resource counts. +The public fixture generator imports JSON and JSONHTML only and writes ten +complete HTML documents (including mapped typed diagnostics), exact native JSON bytes and cumulative resource counts. The root package still has no external dependencies. Playwright 1.62.1 is a pinned qualification tool; install it in this directory and install its Chromium, Firefox and WebKit browsers. Alternatively set `JSONHTML_NODE` to an available From a0a73b06c6c681876c40ecf60ec0ef67fd28a8fc Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:35:35 +0200 Subject: [PATCH 32/59] docs: reconcile JSONHTML qualification and API guidance --- Documentation/Architecture/JSONHTMLExport.md | 13 +- .../Architecture/Release1.3/Acceptance.json | 1663 ++++++++++++++--- Documentation/Architecture/Release1.3/Plan.md | 6 +- .../Architecture/Release1.3/Progress.json | 110 +- .../Release1.3/ReceivingQualification.md | 6 + .../Release1.3/ReviewAndQualification.md | 121 ++ Documentation/CI/README.md | 9 +- README.md | 6 +- Sources/JSONHTML/JSONExportOptions.swift | 2 +- 9 files changed, 1681 insertions(+), 255 deletions(-) create mode 100644 Documentation/Architecture/Release1.3/ReviewAndQualification.md diff --git a/Documentation/Architecture/JSONHTMLExport.md b/Documentation/Architecture/JSONHTMLExport.md index 967a3d9..9045d71 100644 --- a/Documentation/Architecture/JSONHTMLExport.md +++ b/Documentation/Architecture/JSONHTMLExport.md @@ -110,7 +110,8 @@ There is no unlimited or device-specific widening preset. independent five-product consumer, verifies all six fixture hashes, compiles WebPage declarations on macOS/iPhoneOS/iPhoneSimulator and builds the public mobile consumers. It records actual commands, exits, source manifest and toolchain. -The consumer produces five files, including explicit opt-in RTF, and tests native import, exact selected exports, +The consumer records 65 outcomes and eight artifacts, including explicit opt-in RTF +and A4, US Letter and continuous PDFs. It tests native import, exact selected exports, discovery, suggested filenames, immutable selection and cumulative reports. SDK builds do not establish simulator/device/share-sheet/drag/drop qualification. @@ -122,10 +123,12 @@ A4/Letter output or a qualified cancellation/timeout backend. The current pagina nor paper policy is inferred from the historical T0 result. An external probe watchdog is not a production timeout guarantee. -Remaining milestones: RTF/PDF real receiving-app UI and physical-device evidence; -final five-product release qualification. Actual SDK and simulator evidence must be -reported separately from those gates. The existing physical iPad -deferral is preserved. These incomplete gates must remain explicit in returns. +The initial receiving/device milestones above are historical. The +[qualification closure](Release1.3/ReviewAndQualification.md) records their actual +source-bound completion, including corrected PDF filenames, delivered drag events +and the approved physical-iPad deferral. SDK builds, automated device outcomes and +manual receiving observations remain separately attributed. Final PR-head checks +are required before a protected merge; tagging and release publication remain separate. The synthetic fixture bytes originate in the JSONHTML Transfer/Export briefing ZIP SHA-256 `5394fccbcd48244004e146189f4c764857cb83c05fd8a2a91380ca4f9d752105`. diff --git a/Documentation/Architecture/Release1.3/Acceptance.json b/Documentation/Architecture/Release1.3/Acceptance.json index 2b694ab..b6fcbe6 100644 --- a/Documentation/Architecture/Release1.3/Acceptance.json +++ b/Documentation/Architecture/Release1.3/Acceptance.json @@ -8,8 +8,22 @@ "requirement": "The product/module is JSONHTML in the same package; its target depends on JSON and the exact public set contains five products.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH02", @@ -17,18 +31,46 @@ "requirement": "Forward product policy, manifest/CI assertions, consumers and test-count expectations are revised coherently; historical released records remain accurate.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH03", "gate": "H0", "requirement": "Data/String/report APIs, checked Sendable configuration, finite presets and asset strategy compile through a public consumer.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH04", @@ -36,18 +78,46 @@ "requirement": "All six root kinds, empty containers and mixed deep/wide structures produce a complete strict UTF-8 HTML document.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH05", "gate": "H1", "requirement": "Large integers, exact decimals, negative zero and admitted exponent/number spellings survive display and exported JSON without JS numeric conversion.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH06", @@ -55,28 +125,70 @@ "requirement": "Scalar-distinct composed/decomposed keys/strings and insertion/array order remain distinct and ordered.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH07", "gate": "H1", "requirement": "One noncopyable invocation meter covers admission, traversal, token helpers, paths, assets, Base64, annotations and result copies.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH08", "gate": "H1", "requirement": "Inclusive boundaries and checked overflow cover core work/output/retention and renderer node/DOM/document/outline/annotation capacities.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH09", @@ -84,18 +196,46 @@ "requirement": "Cancellation inside owned loops and valid restrictive/zero ceilings produce honest failures without a successful partial result/report.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH10", "gate": "H1", "requirement": "Repeated and concurrent calls have exclusive state; deterministic output uses stable IDs/assets and no clock/random mutation.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH11", @@ -103,100 +243,238 @@ "requirement": "Data and String materialization charges are complete; final-document byte count is distinguished from cumulative core helper output.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH12", "gate": "H2", "requirement": "Formatted mode has no script; interactive native disclosure remains usable with scripting disabled and allows multiple open branches.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH13", "gate": "H2", "requirement": "Default expansion, bounded outline, ancestor opening, focus, root/empty/numeric/special-key paths and generated IDs behave coherently.", - "status": "partial", + "status": "passed", "evidence": [ "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH14", "gate": "H2", "requirement": "Copy/download bytes decode to the owned formatted JSON; RFC Pointer metadata preserves controls, CR/LF, NUL and exact scalar sequences.", - "status": "partial", + "status": "passed", "evidence": [ "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH15", "gate": "H2", "requirement": "Raw and escaped Pointer copy modes have explicit labels; unavailable clipboard behavior is reported and manual/UTF-8 alternatives work.", - "status": "partial", + "status": "passed", "evidence": [ "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH16", "gate": "H2", "requirement": "Literal search is bounded/chunked, preserves scalar distinction, opens matched ancestors and reports its rendered-text scope honestly.", - "status": "partial", + "status": "passed", "evidence": [ "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH17", "gate": "H2", "requirement": "Script/style closing strings, ampersands, quotes, event-handler payloads and keys such as __proto__/constructor remain inert data.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH18", "gate": "H2", "requirement": "No payload-driven innerHTML, executable interpolation, URLs/selectors or script-element text assignment appears in the output/control path.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH19", "gate": "H2", "requirement": "Emitted CSS/JS asset hashes match CSP block bytes; controls work under policy and external resource requests remain absent.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", - "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open.", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "BrowserQualification.md: 37 automated cases and scoped native Safari/maintainer VoiceOver observations; final-platform/annotations clauses remain open." + ], + "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Automated browser matrix passed, with explicitly recorded denied favicon events. Native Safari/maintainer VoiceOver scope is recorded separately; annotations, final mobile/platform and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH20", @@ -204,9 +482,23 @@ "requirement": "Annotations attach by exact typed path or identify missing targets at their parent; no empty list asserts schema validity.", "status": "passed", "evidence": [ - "DiagnosticAnnotations.md; AnnotationTests.swift (12 native behavior functions); Examples/Consumer public document API; 46-case browser receipt in successor return" + "DiagnosticAnnotations.md; AnnotationTests.swift (12 native behavior functions); Examples/Consumer public document API; 46-case browser receipt in successor return", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Targeted semantic/budget/cancellation and file/served three-engine behavior passed. Final source native/safety/DocC/platform refresh and receiver readiness remain separate gates." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "DiagnosticAnnotations.md; AnnotationTests.swift (12 native behavior functions); Examples/Consumer public document API; 46-case browser receipt in successor return" + ], + "limitation": "Targeted semantic/budget/cancellation and file/served three-engine behavior passed. Final source native/safety/DocC/platform refresh and receiver readiness remain separate gates." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH21", @@ -214,83 +506,200 @@ "requirement": "JSONDocument locations remain coherent, unmapped/replaced roots invent none, and annotation messages/paths are bounded and escaped.", "status": "passed", "evidence": [ - "DiagnosticAnnotations.md; AnnotationTests.swift (12 native behavior functions); Examples/Consumer public document API; 46-case browser receipt in successor return" + "DiagnosticAnnotations.md; AnnotationTests.swift (12 native behavior functions); Examples/Consumer public document API; 46-case browser receipt in successor return", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "DiagnosticAnnotations.md; AnnotationTests.swift (12 native behavior functions); Examples/Consumer public document API; 46-case browser receipt in successor return" + ], + "limitation": "Targeted semantic/budget/cancellation and file/served three-engine behavior passed. Final source native/safety/DocC/platform refresh and receiver readiness remain separate gates." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Targeted semantic/budget/cancellation and file/served three-engine behavior passed. Final source native/safety/DocC/platform refresh and receiver readiness remain separate gates." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH22", "gate": "H3", "requirement": "Safari, Firefox and Chromium execute file/served, JS on/off, outline/search/copy/download and policy cases with recorded versions.", - "status": "partial", + "status": "passed", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + ], + "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH23", "gate": "H3", "requirement": "Keyboard/focus, text/contrast/themes and relevant VoiceOver behavior are reviewed; accessibility claims match the executed evidence.", - "status": "partial", + "status": "passed", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + ], + "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative recorded receiver/accessibility matrix; physical iPad uses the approved simulator policy. TextEdit rich-save normalization and human-observed speech are explicit; no exhaustive receiver or accessibility certification is claimed." }, { "id": "JH24", "gate": "H3", "requirement": "Print output includes every value regardless of collapse/script state, excludes UI clutter, and preserves applicable UI state.", - "status": "partial", + "status": "passed", "evidence": [ - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + ], + "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH25", "gate": "H3", "requirement": "Five-product/platform/resource qualification; pure rendering does not execute UI frameworks, while JSONHTML may link narrowly isolated Apple PDF/printing frameworks.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "FormattedRendererTests.swift; InteractiveRendererTests.swift; AnnotationTests.swift; Examples/Consumer; 46-case browser receipt; ReviewAndQualification.md" ], "amendment": "Maintainer-approved transfer/export framework amendment; original JH25 remains historical.", - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH26", "gate": "H3", "requirement": "Representative generation/output-expansion/retention and browser DOM/interactivity measurements justify finite preset values.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "ReviewAndQualification.md; 18 release-native and 54 three-engine browser observations; public consumer export/PDF measurements; retained performance disposition" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + ], + "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative busy-host calibration supports finite initial policy; not idle A/B, maximum-workload throughput, hard RSS or a browser/platform instruction guarantee. Existing 10% review trigger and dispositions unchanged." }, { "id": "JH27", "gate": "H4", "requirement": "Existing JSONWriter behavior, native/safety/docs lanes, public examples and baseline semantic/corpus cases remain qualified.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + ], + "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "JH28", "gate": "H4", "requirement": "Final feature branch/PR head/base, review disposition, source/asset hashes, actual receipts and release placement are recorded.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" ], - "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "Interactive increment: InteractiveIncrement.md; 25 HTML Swift Testing functions and pinned 37-case browser harness, actual executed/blocked outcomes in materialized receipts" + ], + "limitation": "Implemented native behavior is tested; Firefox startup, native Safari/VoiceOver, final platform integration and final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX01", @@ -298,8 +707,22 @@ "requirement": "Reconcile live source/worktree/renderer branches and preserve unrelated changes before tracked edits.", "status": "passed", "evidence": [ - "Live main/origin, tag, worktree and ruleset reads; isolated codex/1.3-jsonhtml checkout at 532f725; primary checkout unchanged" - ] + "Live main/origin, tag, worktree and ruleset reads; isolated codex/1.3-jsonhtml checkout at 532f725; primary checkout unchanged", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "Live main/origin, tag, worktree and ruleset reads; isolated codex/1.3-jsonhtml checkout at 532f725; primary checkout unchanged" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX02", @@ -307,44 +730,112 @@ "requirement": "Exact five-product manifest, forward policy and CI expectations; no sixth product or external root package dependency.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX03", "gate": "T0", "requirement": "Final public APIs, reports, errors, finite presets and representation builders compile with actual Swift 6.4 / SDK 27.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open.", - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable.", + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX04", "gate": "T0", "requirement": "Explicit framework amendment; pure generation executes without WebKit; core JSON imports no WebKit/AppKit/UIKit.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX05", "gate": "T0", "requirement": "One JSON conformance in JSONHTML, qualified same-package compiler behavior and independent public consumers.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX06", "gate": "T0", "requirement": "Actual UTType and exported/imported-content-type probes, conditional availability and correct extensions.", - "status": "not_run", - "evidence": [], - "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [], + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX07", @@ -352,8 +843,22 @@ "requirement": "Legacy writer default/output/mutation/initializer behavior retained under spaces, compact and zero indentation.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX08", @@ -361,222 +866,575 @@ "requirement": "Tabs emitted directly by the cursor; checked 0...8 unit count and separate tab display width.", "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" - ] + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX09", "gate": "T1", "requirement": "Shared helpers admit the input once and keep one noncopyable native preparation meter across selected formats.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX10", "gate": "T2", "requirement": "Exact large numbers, negative zero, exponent spellings and constructed-number fallback; insertion order preserved.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX11", "gate": "T2", "requirement": "Composed/decomposed keys and strings, empty/numeric/control/special keys preserve decoded scalar identity.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX12", "gate": "T2", "requirement": "All six root kinds, empty containers and mixed/deep/wide values are handled without recursive untrusted traversal.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX13", "gate": "T2", "requirement": "Native .json import uses strict parser and finite defaults; invalid UTF-8, duplicate keys and trailing syntax fail.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX14", "gate": "T2", "requirement": "JSON/native JSON equality does not replace separate scalar/order/lexeme checks; no Foundation JSON authority.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX15", "gate": "T2", "requirement": "Plain text contains only the pretty JSON body with quoted scalar-string roots and no UI/fence/labels.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX16", "gate": "T2", "requirement": "Markdown uses json info string, safe fence, actual tabs, LF policy and unaltered JSON body bytes.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX17", "gate": "T2", "requirement": "No mutable global export defaults, shared render cache or heavy ProxyRepresentation getter.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX18", "gate": "T2", "requirement": "Immutable prepared artifacts match advertised/available sets; conditional exports omit missing or unselected data.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX19", "gate": "T2", "requirement": "Selecting unavailable/empty format sets fails; explicit JSON/Markdown/PDF selection advertises exactly that format.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX20", "gate": "T2", "requirement": "Exact data(for:) lookup does no rendering/subtype negotiation; unavailable data produces bounded error.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX21", "gate": "T2", "requirement": "Bounded filename/title validation prevents control/path/separator traversal and selects correct extensions.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX22", "gate": "T2", "requirement": "Wrapper presentation formats are export-only; direct import advertises native JSON only.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX23", "gate": "T2", "requirement": "Direct exports use documented bounded defaults and independently owned state per transport request.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX24", "gate": "T2", "requirement": "One native preparation call is all-or-nothing; failures install no partial artifact or successful report.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX25", "gate": "T3", "requirement": "Static transfer HTML is complete, expanded, highlighted and script-free with deterministic default theme.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX26", "gate": "T3", "requirement": "Static export omits controls/hidden disclosure/line numbers/outline/annotations by default; no JSON truncation.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX27", "gate": "T3", "requirement": "Escaping/CSP/assets hold for script/style delimiters, quotes, ampersands, Unicode/control and prototype-like keys.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX28", "gate": "T3", "requirement": "No external requests/fonts or browser numeric reconstruction; emitted CSS/CSP hashes match actual block bytes.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX29", "gate": "T3", "requirement": "Standalone HTML opening and native rich-paste compatibility are qualified separately.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative recorded receiver/accessibility matrix; physical iPad uses the approved simulator policy. TextEdit rich-save normalization and human-observed speech are explicit; no exhaustive receiver or accessibility certification is claimed." }, { "id": "TX30", "gate": "T3", "requirement": "Per-format and aggregate prepared byte limits include hidden artifacts and overlapping owned buffers.", - "status": "not_run", - "evidence": [] + "status": "passed", + "evidence": [ + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "not_run", + "evidence": [] + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX31", "gate": "T3", "requirement": "Inclusive minus-one/boundary/plus-one and overflow probes for work/retention/output/metadata/style/HTML caps.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX32", "gate": "T3", "requirement": "Cancellation inside owned loops, result-copy failure and terminal latch behavior preserve accounting.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX33", "gate": "T3", "requirement": "Repeated and concurrent native calls are deterministic and invocation-owned; all state is checked Sendable.", - "status": "partial", + "status": "passed", "evidence": [ - "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX34", @@ -584,9 +1442,23 @@ "requirement": "RTF is valid styled RTF with token colors/monospace/tab stops; no renamed HTML/Markdown/attachments.", "status": "passed", "evidence": [ - "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + ], + "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX35", @@ -594,9 +1466,23 @@ "requirement": "Independent RTF decode matches pretty body with Unicode/surrogate/combining/tab/backslash/brace cases.", "status": "passed", "evidence": [ - "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + ], + "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX36", @@ -604,9 +1490,23 @@ "requirement": "RTF strategy has bounded growth/run/UTF-16 range accounting and qualified platform/isolation behavior.", "status": "passed", "evidence": [ - "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + ], + "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX37", @@ -614,9 +1514,24 @@ "requirement": "RTF styles remain present in Mac/mobile decoder and real rich-editor observations; recipient limits recorded.", "status": "passed", "evidence": [ - "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "RTFExportTests.swift; 63-case Mac/iPhone native consumer; actual TextEdit rich reception and saved-file decoding in materialized receiving return" + ], + "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Native emitted RTF text/style qualified. TextEdit rich save normalizes a decomposed key; recipient exact scalar text fidelity is not promised and is explicitly reported." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative recorded receiver/accessibility matrix; physical iPad uses the approved simulator policy. TextEdit rich-save normalization and human-observed speech are explicit; no exhaustive receiver or accessibility certification is claimed." }, { "id": "TX38", @@ -626,9 +1541,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX39", @@ -638,10 +1569,26 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees.", + "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "preliminary_observation": "SDK declarations typecheck on macOS/iPhoneOS/iPhoneSimulator 27.2; complete public API/conformance/backend/runtime requirement remains open.", - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX40", @@ -651,9 +1598,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX41", @@ -663,9 +1626,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX42", @@ -675,9 +1654,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX43", @@ -687,9 +1682,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX44", @@ -699,9 +1710,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX45", @@ -711,9 +1738,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX46", @@ -723,9 +1766,25 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX47", @@ -735,108 +1794,274 @@ "evidence": [ "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", - "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return", + "ExportTests.swift; RTFExportTests.swift; PDFExportTests.swift; Tools/JSONHTMLExport 65-outcome consumer; ReviewAndQualification.md" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "passed", + "evidence": [ + "PDFIncrement.md; PDFExportTests.swift; Tools/JSONHTMLExport; materialized final-source receipts", + "Mac native/ASAN/TSAN/export/DocC receipts; SDK 27.2 compiler checks; iPhone/iPad simulator 63 outcomes each; final source binding in materialized return", + "Qualified code snapshot 1b4c96186d00ef97f9c6ce4eaeb281a0978493ca: passed native/safety/docs/export lanes and separately bound unchanged-source simulator/PDF inspection receipts in materialized return" + ], + "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Qualified Mac software, supported Apple SDKs and iPhone/iPad simulator native API/file transport. Receiving-app UI and physical-device PDF integration remain T6 gates; native instructions/RSS/deadline latency are not hard guarantees." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX48", "gate": "T6", "requirement": "Five-product public consumers/resources and Apple framework boundary qualify Mac/iOS/iPadOS SDK/simulator/device cases.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Actual representative physical UI and Mac/iPad receiving checks executed; drag gestures and fresh device filename qualification remain open. See ReceivingQualification.md and materialized receipts." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Actual representative physical UI and Mac/iPad receiving checks executed; drag gestures and fresh device filename qualification remain open. See ReceivingQualification.md and materialized receipts." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative recorded receiver/accessibility matrix; physical iPad uses the approved simulator policy. TextEdit rich-save normalization and human-observed speech are explicit; no exhaustive receiver or accessibility certification is claimed." }, { "id": "TX49", "gate": "T6", "requirement": "Actual Mac/iOS/iPad receiving-app matrix records share/drag/paste/open behavior and selected content types.", - "status": "partial", + "status": "passed", "evidence": [ - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Actual representative physical UI and Mac/iPad receiving checks executed; drag gestures and fresh device filename qualification remain open. See ReceivingQualification.md and materialized receipts." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Actual representative physical UI and Mac/iPad receiving checks executed; drag gestures and fresh device filename qualification remain open. See ReceivingQualification.md and materialized receipts." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative recorded receiver/accessibility matrix; physical iPad uses the approved simulator policy. TextEdit rich-save normalization and human-observed speech are explicit; no exhaustive receiver or accessibility certification is claimed." }, { "id": "TX50", "gate": "T6", "requirement": "Plain-text fallback versus Markdown subtype tested; explicit Copy JSON/Copy Markdown/Share PDF paths qualified.", - "status": "partial", + "status": "passed", "evidence": [ - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Actual representative physical UI and Mac/iPad receiving checks executed; drag gestures and fresh device filename qualification remain open. See ReceivingQualification.md and materialized receipts." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Actual representative physical UI and Mac/iPad receiving checks executed; drag gestures and fresh device filename qualification remain open. See ReceivingQualification.md and materialized receipts." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative recorded receiver/accessibility matrix; physical iPad uses the approved simulator policy. TextEdit rich-save normalization and human-observed speech are explicit; no exhaustive receiver or accessibility certification is claimed." }, { "id": "TX51", "gate": "T6", "requirement": "Existing renderer JH requirements qualified with explicit JH25 dependency amendment; no hidden weakened baseline.", - "status": "partial", + "status": "passed", "evidence": [ - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" ], - "limitation": "JH20/JH21 are now implemented and targeted-qualified; full final-source renderer/platform/receiver integration remains open." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "JH20/JH21 are now implemented and targeted-qualified; full final-source renderer/platform/receiver integration remains open." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX52", "gate": "T6", "requirement": "Native/safety/docs lanes pass on final exact candidate with deliberate nonzero suite/case counts and current base.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX53", "gate": "T6", "requirement": "Generation, combined preparation, RTF/PDF expansion and responsiveness measurements justify frozen finite profiles.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; 18 release-native and 54 three-engine browser observations; public consumer export/PDF measurements; retained performance disposition" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate.", + "limitation": "Representative busy-host calibration supports finite initial policy; not idle A/B, maximum-workload throughput, hard RSS or a browser/platform instruction guarantee. Existing 10% review trigger and dispositions unchanged." }, { "id": "TX54", "gate": "T6", "requirement": "Public docs/examples describe exactness, preparation, format selection, recipient and system-resource boundaries.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX55", "gate": "T6", "requirement": "Separate review pass findings resolved; final API/framework/limits choices and source/log/artifact identity returned.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" + ], + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." }, { "id": "TX56", "gate": "T6", "requirement": "Release slot reconciled; no main push/tag/release/messages under this brief alone; all incomplete rows explicit.", - "status": "partial", + "status": "passed", "evidence": [ "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", - "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return", + "ReviewAndQualification.md; final required Native/native,safety,docs and Linux receipts; preserved source/evidence manifests" ], - "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + "history": [ + { + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "record": { + "status": "partial", + "evidence": [ + "First formatted increment: Increment.md; actual local native/safety/docs receipts and authored tests", + "ReceivingQualification.md; final integrated lane and receiver receipts in materialized return" + ], + "limitation": "Only the implemented formatted renderer is covered; interactive, transfer/export, browser, final-platform or final-release clauses remain open as applicable." + }, + "policy": "Historical recorded scope retained; later coverage is not attributed to this earlier milestone." + } + ], + "qualification_scope": "Reviewed implementation and source-bound evidence; final PR-head required checks are a separate mandatory merge predicate." } ], "gate_status": { + "H0": "passed", + "H1": "passed", + "H2": "passed", + "H3": "passed", + "H4": "passed", + "T0": "passed", + "T1": "passed", + "T2": "passed", + "T3": "passed", + "T4": "passed", + "T5": "passed", + "T6": "passed" + }, + "historical_gate_status": { "H0": "partial", "H1": "partial", "H2": "not_run", @@ -849,5 +2074,13 @@ "T4": "passed", "T5": "passed", "T6": "partial" + }, + "qualification_basis": { + "reviewed_implementation_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "diagnostic_envelope_sha256": "f587fbeab66ef5f43eaa9d20057dc80fcc08bfa1c3dd0266f28345fd610b21fd", + "receiver_closure_sha256": "cc9720209057ed6e8d6aa5cf979f97b5853d541f37f8377f942df6d699b9d586", + "closure_guide": "ReviewAndQualification.md", + "history_preserved": true, + "scope": "84 implementation acceptance rows are backed by actual tests/consumers/browser/device/receiver receipts. Any changed final source must pass applicable fresh checks before merge. This ledger does not tag or publish a release." } } diff --git a/Documentation/Architecture/Release1.3/Plan.md b/Documentation/Architecture/Release1.3/Plan.md index 6b97fc8..516ed50 100644 --- a/Documentation/Architecture/Release1.3/Plan.md +++ b/Documentation/Architecture/Release1.3/Plan.md @@ -1,6 +1,10 @@ # Swift-JSON 1.3.0 — JSONHTML and exact export -Status: first formatted-rendering increment implemented and locally checked; complete 1.3 release qualification remains pending. +Status: all renderer/export implementation slices and representative receiving +qualification are complete on the reviewed source. See +[ReviewAndQualification.md](ReviewAndQualification.md) for source-bound receipts, +retained scope limits and required final PR-head checks. Tagging and publication +remain separate. The initial sequence and milestone history below are retained. 1.3.0 introduces **JSONHTML**, a fifth library product in the existing Swift-JSON package. It depends on JSON; JSON does not acquire a reverse dependency. The five-product policy becomes effective with the working target, consumers and matching CI checks. Historical four-product releases remain unchanged. JSONRegexRuntime remains private and root external dependencies remain empty. diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index d56e0d2..7c0fed7 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -1,12 +1,12 @@ { "schema_version": 1, "target_release": "1.3.0", - "status": "Formatted and interactive increments implemented; browser/platform/final 1.3 gates remain partial", + "status": "Implementation and representative qualification complete; protected PR-stack integration requires passing final-head checks", "actual_base": "532f725740a4d61c32bc9870986d050b71f9db78", "captured_briefing_base": "e9c6693c7cb29df5125e1699fef70f98229ccfdf", - "feature_branch": "codex/1.3-jsonhtml", - "pr": null, - "release_train_dependency": "WASI main 25fb04f integrated; final five-product portable release qualification remains a later gate", + "feature_branch": "codex/1.3-jsonhtml-transfer", + "pr": 22, + "release_train_dependency": "Qualified 1.2/WASI main25fb04f integrated; renderer PR21 merged as d463d0418e0fc201229ae38af6464e56311f6e11; portable five-product Node/Linux scope qualified", "expected_final_products": [ "JSON", "JSONSchema", @@ -16,30 +16,25 @@ ], "current_manifest_products": 5, "qualified_scope": [ - "Briefing integrity only", - "Source/worktree/ruleset orientation", - "Three supported Apple SDK declaration probes only", - "Formatted exact Data/String rendering and cumulative reports", - "16 new behavior functions; complete local native/safety/docs lanes", - "Five-product public consumer and 628 documented declarations", - "JSON/JSONHTML iPhoneOS and iPhoneSimulator module/public-interface typechecks only", - "12 interactive behavior functions; initial native debug/release 217 functions and 173 parameter cases passed", - "Pinned browser harness: 37 required cases, exact byte downloads and explicit unexecuted engine counts", - "All 37 browser cases executed; native Safari exact downloads/focus and four maintainer-observed VoiceOver checks" - ], - "implementation_qualified": false, - "next_increment": "Complete blocked browser/accessibility/final-platform qualification; continue separately scoped annotation and transfer/export work", - "open_decisions": [ - "Measured finite renderer/export/PDF presets", - "Executed PDF backend/pagination/defaults", - "Qualified RTF strategy", - "Exact final declaration shapes" + "Native 270 functions/173 parameters; ASAN270/173; TSAN10; strict memory warnings-as-errors", + "Portable Linux/Node 256 functions/173 parameters; WasmKit14 public consumer functions/probes, not full root", + "All49 baseline cap probes and populated pinned independent suites with exclusions separate", + "38 focused export functions;65 public consumer outcomes/eight artifacts; three Apple SDKs", + "65 actual consumer outcomes on Mac, physical iPhone and iPhone/iPad simulators", + "46 browser cases/zero skips; native Safari exact/diagnostic navigation and human VoiceOver scope", + "Three delivered Mac exact JSON/Markdown/generic String drop callbacks; physical and simulated A4.pdf Files save/reopen", + "Five compiled DocC catalogues,771 declarations and13 executed README fences", + "18 release-native representative measurements;54 browser observations across six frozen workloads/three engines", + "Immutable source/evidence archives, clean reproduction and reviewed public framework/dependency boundary" ], + "implementation_qualified": true, + "next_increment": "Complete protected PR22 integration after current required checks and source review; post-merge verification, tagging and publication remain separate", + "open_decisions": [], "notes": [ - "No raw private briefing packs or source-clone records enter Git", - "No empty product or passing placeholder tests", - "Existing core defaults and 49 metric meanings retained", - "No messages, merge, release or protection changes" + "Private briefs, device records, logs and build outputs remain outside public Git/source archives", + "Original milestone objects and failed-probe history retained", + "All49 metric values/meanings and10% performance review trigger unchanged", + "Explicit maintainer authorization2026-10-14 permits protected PR21/22 review and merge only; no tag/release publication, direct main push or protection bypass" ], "input_packets": [ { @@ -55,7 +50,7 @@ "entries": 8 } ], - "updated_utc": "2026-10-14T13:48:05.774840+00:00", + "updated_utc": "2026-10-14T20:31:44.666625+00:00", "evidence_locations": [ "Scripts/ci.py --lane native --output .build/ci/jsonhtml-native-compatible", "Scripts/ci.py --lane safety --output .build/ci/jsonhtml-safety-compatible", @@ -67,7 +62,7 @@ "Scripts/ci.py --lane docs --output .build/ci/jsonhtml-interactive-final-docs", "Scripts/ci.py --lane browser --output .build/ci/jsonhtml-interactive-final-browser" ], - "final_source_refresh": "The final-source lane receipts are materialized privately after committing this source; initial full lanes already passed. Any final failure keeps that lane open.", + "final_source_refresh": "Current required Native/native,safety,docs and supporting Linux checks must pass on the final PR head before merge; preserve byte-identity reuse for unchanged implementation, assets, fixtures and tests.", "interactive_increment": { "parent_commit": "41fdf1c5045ef171059981ffcbc59a5aef8f998a", "expected_root_functions": 217, @@ -252,5 +247,64 @@ "public_api": "JSONHTMLAnnotation; options.annotations; limits.maxAnnotations; statistics.annotations; four JSONDocument renderer overloads", "targeted_results": "12 native functions and 46 browser cases passed; final source lane refresh required after integration metadata edits", "remaining": "Receiver drag/drop and fresh physical filename/UI checks; full final candidate review and qualification. Original receiving/renderer history retained." + }, + "historical_progress_header": { + "status": "Formatted and interactive increments implemented; browser/platform/final 1.3 gates remain partial", + "feature_branch": "codex/1.3-jsonhtml", + "pr": null, + "release_train_dependency": "WASI main 25fb04f integrated; final five-product portable release qualification remains a later gate", + "qualified_scope": [ + "Briefing integrity only", + "Source/worktree/ruleset orientation", + "Three supported Apple SDK declaration probes only", + "Formatted exact Data/String rendering and cumulative reports", + "16 new behavior functions; complete local native/safety/docs lanes", + "Five-product public consumer and 628 documented declarations", + "JSON/JSONHTML iPhoneOS and iPhoneSimulator module/public-interface typechecks only", + "12 interactive behavior functions; initial native debug/release 217 functions and 173 parameter cases passed", + "Pinned browser harness: 37 required cases, exact byte downloads and explicit unexecuted engine counts", + "All 37 browser cases executed; native Safari exact downloads/focus and four maintainer-observed VoiceOver checks" + ], + "implementation_qualified": false, + "next_increment": "Complete blocked browser/accessibility/final-platform qualification; continue separately scoped annotation and transfer/export work", + "open_decisions": [ + "Measured finite renderer/export/PDF presets", + "Executed PDF backend/pagination/defaults", + "Qualified RTF strategy", + "Exact final declaration shapes" + ], + "notes": [ + "No raw private briefing packs or source-clone records enter Git", + "No empty product or passing placeholder tests", + "Existing core defaults and 49 metric meanings retained", + "No messages, merge, release or protection changes" + ], + "updated_utc": "2026-10-14T13:48:05.774840+00:00", + "final_source_refresh": "The final-source lane receipts are materialized privately after committing this source; initial full lanes already passed. Any final failure keeps that lane open." + }, + "current_qualification": { + "guide": "ReviewAndQualification.md", + "reviewed_implementation_head": "2872afee29ba65905a16653eee8ea4f3807e14e5", + "receiver_closure_sha256": "cc9720209057ed6e8d6aa5cf979f97b5853d541f37f8377f942df6d699b9d586", + "browser_measurements": { + "observations": 54, + "engines": 3, + "workloads": 6, + "source_head": "2872afee29ba65905a16653eee8ea4f3807e14e5" + }, + "required_checks": [ + "Native / native", + "Native / safety", + "Native / docs" + ], + "github_approvals_required": 0, + "unresolved_review_threads": 0, + "no_independent_reviewer_claim": true, + "retained_limits": [ + "Physical iPad simulator policy", + "TextEdit rich-save Unicode normalization", + "Representative calibration only; no maximum-workload/RSS/opaque SDK instruction guarantees", + "Formatted audible speech not part of the new interactive annotation check" + ] } } diff --git a/Documentation/Architecture/Release1.3/ReceivingQualification.md b/Documentation/Architecture/Release1.3/ReceivingQualification.md index 152580f..6d5379d 100644 --- a/Documentation/Architecture/Release1.3/ReceivingQualification.md +++ b/Documentation/Architecture/Release1.3/ReceivingQualification.md @@ -1,5 +1,11 @@ # Receiving-app and physical export qualification +The observations below retain the earlier receiving candidates and failures. The +[final qualification closure](ReviewAndQualification.md) records the subsequent +65-case source, successful delivered drag events, corrected PDF save/reopen and +annotation Safari/VoiceOver checks. Historical open states below do not describe +the current reviewed candidate. + This pass continues export implementation 1e0b86d2580bb9678039597523aa3b99d2905abf on codex/1.3-jsonhtml-transfer, stacked on renderer PR #21. It changes qualification tools and documentation, not the JSONHTML public API or library implementation. diff --git a/Documentation/Architecture/Release1.3/ReviewAndQualification.md b/Documentation/Architecture/Release1.3/ReviewAndQualification.md new file mode 100644 index 0000000..7b7dbbc --- /dev/null +++ b/Documentation/Architecture/Release1.3/ReviewAndQualification.md @@ -0,0 +1,121 @@ +# 1.3 implementation review and qualification + +The renderer, native preparation, styled RTF, explicit Apple PDF and typed diagnostic +slices are implemented and qualified in the representative scope below. The +reviewed implementation is `2872afee29ba65905a16653eee8ea4f3807e14e5`. Renderer +[PR #21](https://github.com/ThorFuchs/Swift-JSON/pull/21) merged as +`d463d0418e0fc201229ae38af6464e56311f6e11`; the child-only commits in +[PR #22](https://github.com/ThorFuchs/Swift-JSON/pull/22) were transplanted onto +that identical renderer tree without changing any source bytes. Documentation +corrections are a separately checked successor. Required checks and review-thread +resolution must pass on its actual final head before merge. The maintainer's +2026-10-14 authorization permits the protected stack merge; tagging and release +publication remain separate. + +## Review findings + +A separate source-and-contract review covered the shared writer cursor and original +initializer function type; exact typed/scalar identity; HTML/RTF escaping, fixed CSP +and offline navigation; cumulative admission/output/retention; cancellation and +late PDF results; immutable selection/transfer filenames; conditional framework +placement; consumers, independent fixtures and exact test discovery. No unresolved +code defect was found. This is an agent source review, not a claim of an independent +second reviewer or a GitHub approving review. Effective main rules require zero +approvals, resolved threads and strict Native / native, Native / safety and +Native / docs checks; bypass actors are empty. No protection was changed. + +Three documentation findings were corrected: README incorrectly called annotations +deferred; current CI/export inventory retained old counts; and the acceptance/progress +headers retained earlier not_run/partial states after the corresponding source-bound +passes. Historical records and their original limitations are preserved. The 84-row +acceptance ledger records implementation qualification; it does not substitute for +live required checks, certify every receiving app or publish a release. + +## Source-bound evidence + +The materialized source archive SHA-256 is +`36b21a3805d0b187d77bbe4bd173443e963ced4fd00c700f7c681ee2790cd44a`: +886 source files plus its manifest. Clean extraction/build/test, two byte-identical +normalized archives and exact final-source bindings were verified. The diagnostic +qualification envelope SHA-256 is +`f587fbeab66ef5f43eaa9d20057dc80fcc08bfa1c3dd0266f28345fd610b21fd`. +The receiving/accessibility closure envelope is 57,927,484 bytes, SHA-256 +`cc9720209057ed6e8d6aa5cf979f97b5853d541f37f8377f942df6d699b9d586`. +It includes the actual source and parent envelopes, new receipts/logs and artifacts. +A separate recursive audit verified all three envelope manifests, 844 scoped log +references, 1,552 historical aliases and three unique source-archive manifests. +Private device records and build products remain outside public Git/source archives. + +| Area | Executed evidence | +| --- | --- | +| Mac behavior | Native debug/release 270 functions and 173 parameter invocations; all 49 cap probes and the explicitly rooted 20-outcome public consumer | +| Safety | ASAN 270/173; ten shared/concurrent functions under TSAN; strict-memory-safety build with warnings as errors | +| Independent inputs | Syntax 318 observed = 287 evaluated successes + 31 policy exclusions; schema 2,337 = 2,300 successes + 37 exclusions; authored semantics 82, budget boundaries 9, malformed replay 4,396 | +| Export | 38 focused debug/release functions; 65 public consumer outcomes and eight artifacts; macOS, iPhoneOS and iPhoneSimulator SDK 27.2 compiler/consumer builds | +| Actual runtimes | The same 65 outcomes/eight artifacts executed on Mac, physical iPhone 14 Pro Max iOS 27.2, iPhone simulator and iPad simulator iOS/iPadOS 27.0; these are not 260 distinct behavior cases | +| Browsers | 46 cases across pinned Chromium/Firefox/WebKit, zero skips; native Safari 27.0 navigation/file/print checks remain separately recorded | +| Portable | Linux ARM64/x86-64 native/safety passed; Node debug/release 256 functions/173 parameters; 71 WASI commands. WasmKit remains limited to its recorded 14-function public consumer/probes/command parity scope | +| Documentation | Five compiled DocC catalogues, 771/771 authored declarations and 13 executable README fences; affected documentation is refreshed for the correction | + +Commands, exits, exact input/source/asset hashes and environment are in the receipts, +not inferred from summaries. Native tools were Apple Swift 6.4, language mode 6, +Xcode/SDK 27.2 on macOS 27.0 ARM64. Required Native and supporting Linux checks +passed on the reviewed source; final-head checks must be read again after integration. +Policy exclusions are not evaluated successes; ordinary schema invalidity remains +distinct from operational failure. Historical W/G/RC/F/J and failed probes remain +intact in their original scope. + +## Actual receiving and accessibility closure + +Three Mac drags delivered callbacks with exact comparisons: native JSON to the typed +JSON reader, Markdown to its subtype reader and Markdown to a generic String reader. +Earlier automation gestures delivered no event and were not counted as successes. +Mac plain TextEdit paste/save retained all 105 UTF-8 bytes. Rich RTF paste/save retained +font/colors, but TextEdit's rich save canonicalized a decomposed key; emitted/native- +decoded RTF is separately exact. Native JSON is the interchange authority. HTML rich +paste and standalone opening were observed independently. + +Fresh physical iPhone Share PDF offered `A4.pdf` without editing; the maintainer +confirmed local Files save/reopen, seven pages and the final LAST-CONTENT-SENTINEL. +The iPad simulator's actual Files-saved `A4.pdf` matched prepared bytes exactly and +independently decoded as seven pages with the sentinel. After Device Hub automation +stalled, the maintainer confirmed actual Files reopening. These manual observations +are separate from generated-file decode and automated API outcomes. Physical iPad +retains the approved simulator-only qualification policy. + +Native Safari's new annotated fixture opened nested target ancestors and focused the +exact value; missing targets focused the existing parent with the full missing path. +The script-free formatted fixture retained complete data and explicit value/parent +anchors. The maintainer reported all four interactive diagnostic VoiceOver checks +passed: labeled controls/disclosure states, nested value/diagnostic announcement, +missing-parent/path announcement, status/no keyboard trap. VoiceOver's runtime was +absent afterward. Audible speech is human-reported, not inferred from AX or Playwright; +formatted speech and exhaustive accessibility certification are outside this check. +Earlier no-script/print/manual navigation results remain source-bound history. + +## Representative calibration and finite policy + +Eighteen release-native observations cover six deterministic workloads. Median +rendering was 44.693 ms for 5,000-member formatted output and 71.459 ms for a +1,000-member interactive object with 128 coherent mapped diagnostics. The latter +emitted 980,689 bytes, charged 4,202,193 owned work units and peaked at 1,961,378 +logical retained bytes. Default 128 diagnostics passed; 129 refused. + +The review added 54 browser observations: the same six frozen HTML workloads, +three fresh contexts each in Chromium 151.0.7922.34, WebKit 26.5 and Firefox 153.0. +The Browser plugin was unavailable; the existing pinned Playwright 1.62.1 runtime +was reused. All loads, actual DOM bounds, expansion/collapse and literal-search +completion/focus checks passed with no unexpected request or script error. +5,000-member formatted output had 50,020 actual DOM nodes against a 120,032 bound; +the 128-mapped-diagnostic object had 29,128 against 64,512. Median load/DOM times +for that annotated object were 114.160, 223.408 and 232.024 ms respectively; median +search completion was 44.130, 110.151 and 66.949 ms. Public consumer receipts separately +record native preparation/RTF/PDF expansion and elapsed platform work, including the +physical representative seven-page export and historical background soft deadlines. + +These observations support the retained finite initial profiles in their measured +representative scope. They do not guarantee maximum-workload throughput, idle A/B, +hard RSS, physical responsiveness or browser/SDK/caller instruction quotas. Logical +retention and owned work remain separate. No profile, deadline, 49-metric meaning, +private optimization or 10% review trigger changed. Historical streaming and schema +performance dispositions remain retained, not relabeled as new idle-host passes. diff --git a/Documentation/CI/README.md b/Documentation/CI/README.md index a6882ed..15091c0 100644 --- a/Documentation/CI/README.md +++ b/Documentation/CI/README.md @@ -63,15 +63,16 @@ distribution, architecture or libc claims. `python3 Scripts/ci.py --lane wasm` requires the matching Swift.org 6.4.0 compiler, SDK and WasmKit plus Node. It records cross-build and runtime outcomes separately. -It preserves all 192/173 root discoveries, independent corpus policies and 49 cap -outcomes; both hosts also execute the frozen public consumer. The +Node executes the complete 256-function/173-parameter portable root suites, +independent corpus policies and all 49 cap outcomes. Both hosts execute the frozen +public consumer; WasmKit retains its separately recorded 14-function consumer scope. The [WASI contract](../Architecture/WebAssembly.md) defines host/safety scopes. This local lane neither activates hosted CI nor substitutes for Apple/Linux gates. ## JSONHTML export development lane `python3 Scripts/ci.py --lane export` requires 38 focused Swift Testing functions -in both configurations, 63 public consumer outcomes, six pinned fixtures and three +in both configurations, 65 public consumer outcomes, eight artifacts, six pinned fixtures and three Apple SDK declaration probes. It builds all five products in iPhoneOS and simulator consumers. A bounded offscreen WebPage region experiment remains outside the production API. The lane establishes native API/file transport and SDK builds, @@ -102,7 +103,7 @@ from those outcomes and from physical-device/receiver UI qualification. ## Integrated 1.3 portable qualification The five-product WASI lane requires six exact bundles: JSON 107, schema 51, private -regex 12, streaming 14, patch 11 and portable HTML 49. Its public consumer requires +regex 12, streaming 14, patch 11 and portable HTML 61. Its public consumer requires 14 functions and six explicit HTML/export outcomes in both Node and WasmKit. All 173 parameter cases, 49 cap outcomes, pinned independent suites and rooted vectors remain required. The first failed inherited expectations are retained; no blanket diff --git a/README.md b/README.md index 67c851f..7ab8d4e 100644 --- a/README.md +++ b/README.md @@ -405,7 +405,11 @@ bytes. Defaults are paginated A4, 36-point margins and a light Menlo profile. US Letter and an explicit continuous page are available. Finite limits and cancellation cover owned operations; opaque SDK work and allocation are separate. See the [PDF guide](Sources/JSONHTML/Documentation.docc/PDFPreparation.md). -Annotations remain deferred. Published 1.2 versions do not include JSONHTML. +Caller-supplied `JSONHTMLAnnotation` values attach diagnostics by exact typed path. +Rendering a coherent `JSONDocument` can show original value or parent UTF-8 byte +spans; unmapped or replaced roots invent none. Rendering does not execute schema +validation. See the [diagnostic guide](Sources/JSONHTML/Documentation.docc/DiagnosticAnnotations.md). +Published 1.2 versions do not include JSONHTML. ## Documentation and examples diff --git a/Sources/JSONHTML/JSONExportOptions.swift b/Sources/JSONHTML/JSONExportOptions.swift index ad02465..03041ce 100644 --- a/Sources/JSONHTML/JSONExportOptions.swift +++ b/Sources/JSONHTML/JSONExportOptions.swift @@ -13,7 +13,7 @@ public enum JSONExportFormat: String, CaseIterable, Hashable, Sendable { case html /// Styled monospaced rich text with the unchanged pretty JSON body; explicit opt-in. case rtf - /// Explicitly prepared PDF; unavailable until the Apple backend is qualified. + /// Explicitly prepared PDF; added only after successful Apple PDF preparation. case pdf /// The filename extension associated with this exact artifact format. From d23525bdc314b1e8f7967d26a9581f9a41a063cd Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:07:43 +0200 Subject: [PATCH 33/59] fix(cli): return checked errors for missing schema resources --- .../Architecture/ExtractionManifest.json | 3 +- .../Architecture/Release1.4/Acceptance.json | 2 +- .../Architecture/Release1.4/Integration.md | 36 ++++++++++++++++ .../Architecture/Release1.4/Progress.json | 21 ++++++--- Scripts/ci.py | 8 ++-- Scripts/qualify_cli_integration.py | 10 ++++- Scripts/wasm_support.py | 2 +- .../Document/JSONSchemaBuiltins.swift | 43 ++++++++++++++++++- .../JSONSchemaTests/ResourceLookupTests.swift | 29 +++++++++++++ 9 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 Tests/JSONSchemaTests/ResourceLookupTests.swift diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 7c81ed7..b7e3875 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -204,6 +204,7 @@ "1.4 C3 decimal magnitude, rational/quantization kernel, pull range and type-directed overloads are original Apache-2.0 code. The 738-case deterministic oracle uses Python integer/Fraction arithmetic; no jq/Rhoe arithmetic implementation was copied.", "1.4 inherited CLI qualification fixtures are copied byte-for-byte: RhoePlatform/RhoeJSON pin 51d37acc137422065b07ebeb7e4ecdb2d02b99ee, 119 golden and 49 subset rows. Apache-2.0 license retained in Licenses/RhoeJSON-CLI-Fixtures-Apache-2.0.txt. PlanningDispositions labels are provisional, not evaluated successes. New explicit-stack CLI code is original; old eager JSONValue evaluator and arithmetic were not ported.", "1.4 C4 immutable IR/function/pattern metadata, invocation closures/thunks, continuations, scalar string and format helpers, root-provenance paths and transactional mutation are original Apache-2.0 code. No eager upstream evaluator, Foundation regex or approximate arithmetic was copied.", - "1.4 C5 stream/offline-schema adapters, checked sink handoff, command help and consumers are original Apache-2.0 code. Five NewInputs fixtures are copied byte-for-byte from the maintainer CLI briefing pack 43f8819c57e3d93bc6f7cf561d2d111442e089f2973cb7c62e16e3bd3fd73d0d; these authored data fixtures introduce no runtime dependency. Missing provider helper prerequisites remain blocked rather than adapted or silently rewritten." + "1.4 C5 stream/offline-schema adapters, checked sink handoff, command help and consumers are original Apache-2.0 code. Five NewInputs fixtures are copied byte-for-byte from the maintainer CLI briefing pack 43f8819c57e3d93bc6f7cf561d2d111442e089f2973cb7c62e16e3bd3fd73d0d; these authored data fixtures introduce no runtime dependency. Missing provider helper prerequisites remain blocked rather than adapted or silently rewritten.", + "1.4 checked schema resource lookup and its missing/quota/cancellation/install regressions are original Apache-2.0 code. Supported app/framework/command layouts retain all nine official resource pins; no unbounded directory scan, fallback engine or external dependency is introduced." ] } diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json index 253e9ac..9539e52 100644 --- a/Documentation/Architecture/Release1.4/Acceptance.json +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -1,7 +1,7 @@ { "schema": "swift-json-cli-acceptance/1", "prepared": "2026-10-14", - "implementation_status": "C0-C4 behavior and portable C5 milestone tested; provider prerequisites blocked; final source/platform/reproduction closure tracked", + "implementation_status": "Native resource-fix successor qualified; final immutable return records close artifact/head rows. Mandatory providers remain blocked.", "requirements": [ { "id": "CL001", diff --git a/Documentation/Architecture/Release1.4/Integration.md b/Documentation/Architecture/Release1.4/Integration.md index 0c0bb89..506fa57 100644 --- a/Documentation/Architecture/Release1.4/Integration.md +++ b/Documentation/Architecture/Release1.4/Integration.md @@ -94,3 +94,39 @@ for2000 and3.5305s for20000 records on a heavily loaded host; logical retention stayed about66.5KB and process RSS about7.6MB. These distinct observations support bounded logical pipeline state, not quiet-host throughput or hard RSS promises. Final-head hosted checks/reproduction and the return manifests bind the successor. + + +## Checked resource successor + +Clean installation of predecessor `9a773f3` reproduced a SIGTRAP when the sibling +schema bundle and SwiftPM fallback were both absent. Its archive `cf72bb6c…` +and failed receipt remain immutable and are not release qualification evidence. +The successor replaces the generated trapping Bundle.module accessor with finite, +checked app/framework/command lookup. Apple `.bundle` and Linux `.resources` +layouts retain all nine size/SHA pins; a corrupt present bundle is not bypassed. +Missing resources throw a schema metaSchema error. The actual installed consumer +now requires exit5/no output for missing and tampered resources, with a positive +sibling-resource case. One new Swift Testing function exercises missing lookup, +shared work/retention, quota failure and in-loop cancellation. + +`Scripts/ci.py --lane native` passed25 commands with298 root functions/199 parameter +cases; safety passed ASan298/199, TSan8 and strict-memory/warnings-as-errors. The +CLI adapter boundary is now53 foundation +37 integration subprocesses,77 CLI +functions/26 parameter cases,33 checks across16 CLI metrics, and105 C1-C4 vectors. +The resource-fix runtime/source manifest is +`c81e1eb3526a2f67546af9a1d906638ef1b506721390876d4bab8da5343619f8`. +Native, resumed safety and resumed WASI receipts record unchanged source inputs. +WASI executes221 core/HTML functions in debug/release with173 parameter cases, +all49 cap outcomes, public/rooted consumers and the unchanged pinned independent +suite counts. Missing-resource and strict-memory probes pass. CLI WASI runtime +support remains the separate limited disposition above. Both selected mobile +SDK consumers passed on27.2 without CLI contamination. + +Disk-full safety/WASI attempts and a SwiftPM lock-banner failure remain failed or +incomplete records. Only completed caches owned by this CLI worktree were cleared; +source and materialized receipts were preserved, and separate complete reruns passed. +Resource-fix slow-sink release medians are0.0803s/2.7205s for2000/20000 records; +peak logical retention is about66.5KB and measured RSS about7.5MB on a loaded host. +There is no quiet-host throughput or hard RSS claim. Final-head hosted checks, +source archive, clean extraction and acceptance closure are bound by the returned +freeze/evidence records. Mandatory provider CL078/CL079 remain blocked. diff --git a/Documentation/Architecture/Release1.4/Progress.json b/Documentation/Architecture/Release1.4/Progress.json index cd8d952..fd2b1e2 100644 --- a/Documentation/Architecture/Release1.4/Progress.json +++ b/Documentation/Architecture/Release1.4/Progress.json @@ -4,7 +4,7 @@ "renderer_base": "3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7", "main_at_orientation": "25fb04f747e583209b4c4511abf39e11ef5b4f80", "branch": "codex/1.4-json-cli-integration", - "status": "Portable C5 implemented/tested; final head/distribution checks pending and provider prerequisites blocked", + "status": "Resource-fix successor: native/safety/WASI/mobile passed; final archive/hosted closure belongs to immutable return records", "gates": { "C0": "partial", "C1": "passed", @@ -21,13 +21,13 @@ "core_metrics": 49, "cli_metrics": 16, "publication_performed": false, - "root_test_functions": 297, + "root_test_functions": 298, "root_parameter_cases": 199, - "native_source_manifest_sha256": "68463696a494417a6509e7330cec0343a9199cb10f806cadfe4ff4d7506c9964", + "native_source_manifest_sha256": "c81e1eb3526a2f67546af9a1d906638ef1b506721390876d4bab8da5343619f8", "qualified_lanes": [ "native", "safety", - "docs" + "wasm" ], "remaining_foundation_requirements": [ "final-head hosted Linux/native checks", @@ -89,6 +89,14 @@ "root_parameter_cases": 199, "head": "8d00757022a753adc57a359be6bb4fd5307b0e8d", "pr": 26 + }, + { + "branch": "codex/1.4-json-cli-integration", + "head": "9a773f34fd35aa686ddf9d28bb0546621f3e9000", + "status": "Hosted seven checks passed, then clean installed missing-resource probe trapped; candidate superseded", + "root_test_functions": 297, + "integration_subprocess_cases": 36, + "source_archive_sha256": "cf72bb6c31976fc2c6020f66264d57f53f54adf7b873e048e23cabbaded97411" } ], "query_functions": 24, @@ -109,9 +117,10 @@ "inherited_passed": 154, "inherited_policy": 14, "integration_functions": 6, - "integration_subprocess_cases": 36, + "integration_subprocess_cases": 37, "new_cases_observed": 111, "new_cases_passed": 109, "new_cases_blocked": 2, - "release_ready": false + "release_ready": false, + "freeze_closure": "Final source/archive and hosted receipts are recorded in the materialized return pack without rewriting this frozen source." } diff --git a/Scripts/ci.py b/Scripts/ci.py index 155f520..474d773 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -115,11 +115,11 @@ def check_result(command, code, log, events=None): passed = code == 0 if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [77] if 'cli|sharedFilterPlan' in command else [13] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 1, 4] if 'thread' in command else [11, 12, 14, 25, 51, 77, 107] + expected = [77] if 'cli|sharedFilterPlan' in command else [13] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 1, 4] if 'thread' in command else [11, 12, 14, 25, 52, 77, 107] passed = passed and sorted(counts) == expected if '--filter' 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'] == {'JSONTests':107,'JSONSchemaTests':51,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONRegexRuntimeTests':12,'JSONHTMLTests':25,'JSONCLITests':77} + passed = passed and events['module_function_counts'] == {'JSONTests':107,'JSONSchemaTests':52,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONRegexRuntimeTests':12,'JSONHTMLTests':25,'JSONCLITests':77} 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)) == 199) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) @@ -131,7 +131,7 @@ def check_result(command, code, log, events=None): passed = data['observed'] == data['passed'] == 53 and data['failed'] == 0 if command[:3] == ['python3', '-B', 'Scripts/qualify_cli_integration.py'] and passed: d = json.loads(log.strip().splitlines()[-1]) - passed = d['status'] == 'passed' and (d['observed'], d['passed'], d['failed']) == (36, 36, 0) + passed = d['status'] == 'passed' and (d['observed'], d['passed'], d['failed']) == (37, 37, 0) if command[:3] == ['python3', '-B', 'Scripts/qualify_cli_limits.py'] and passed: data = json.loads(log.strip().splitlines()[-1]) passed = data['observed'] == data['passed'] == 33 and data['failed'] == 0 and data['metrics'] == 16 @@ -221,7 +221,7 @@ def save(): if receipt['status'] == 'passed' and args.lane == 'wasm': try: wasi = json.loads((output / 'probes/wasi.json').read_text()) - if wasi['status'] != 'passed' or wasi['root_counts'] != {'JSONTests':107,'JSONSchemaTests':51,'JSONRegexRuntimeTests':12,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONHTMLTests':25}: + if wasi['status'] != 'passed' or wasi['root_counts'] != {'JSONTests':107,'JSONSchemaTests':52,'JSONRegexRuntimeTests':12,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONHTMLTests':25}: raise ValueError('Missing WASI qualification boundary') receipt['materialized_wasi'] = {'path':'probes/wasi.json','sha256':hashlib.sha256((output / 'probes/wasi.json').read_bytes()).hexdigest()} except (OSError,KeyError,ValueError,TypeError) as error: diff --git a/Scripts/qualify_cli_integration.py b/Scripts/qualify_cli_integration.py index 6c5ab4f..1625906 100644 --- a/Scripts/qualify_cli_integration.py +++ b/Scripts/qualify_cli_integration.py @@ -44,7 +44,7 @@ def file(name, text): source=file('late-tail.jsons',b'{"x":"exact"}\n'*3000+b'{') run('long-prefix-atomic-failure',['query','--compact','.x',source,'--output',str(target),'--overwrite'],exit=5,expected=b'',check=lambda p:target.read_bytes()==b'old' and not list(directory.glob('.swift-json-*.tmp'))) installed=directory/'installed with spaces';installed.mkdir();executable=installed/'swift-json';shutil.copy2(binary,executable) - bundle=next((p for p in binary.parent.iterdir() if p.name=='Swift-JSON_JSONSchema.bundle'),None) + bundle=next((p for p in binary.parent.iterdir() if p.name in ('Swift-JSON_JSONSchema.bundle','Swift-JSON_JSONSchema.resources')),None) if bundle is None:raise ValueError('Missing actual sibling schema bundle') installed_bundle=installed/bundle.name;shutil.copytree(bundle,installed_bundle) run('installed-resource-schema',['validate','--schema',resource],b'9007199254740993',check=lambda p:json.loads(p.stdout)['valid'],executable=executable,cwd=directory) @@ -54,6 +54,14 @@ def file(name, text): # even on hosts with SwiftPM's build-time fallback directory still present. pin=next(installed_bundle.rglob('schema.json'));pin.write_bytes(b'{}') run('installed-tampered-resource',['validate','--schema',resource],b'1',exit=5,expected=b'',executable=executable,cwd=directory) + shutil.rmtree(installed_bundle) + held=bundle.with_name(bundle.name+'.qualification-held') + bundle.rename(held) + try: + run('installed-missing-resources',['validate','--schema',resource],b'1',exit=5,expected=b'',executable=executable,cwd=directory, + check=lambda p:b'schemaCompilation.metaSchema' in p.stderr) + finally:held.rename(bundle) + p=subprocess.Popen([str(binary),'query','-n','--diagnostics=json','range(10000)'],stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.PIPE) p.stdout.close();stderr=p.stderr.read();code=p.wait(timeout=10) good=code==7 and json.loads(stderr)['category']=='io' diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index 2df8ef3..6fe1e0c 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -6,7 +6,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SDK = 'swift-6.4.0-RELEASE_wasm' -COUNTS = {'JSONTests': 107, 'JSONSchemaTests': 51, 'JSONRegexRuntimeTests': 12, +COUNTS = {'JSONTests': 107, 'JSONSchemaTests': 52, 'JSONRegexRuntimeTests': 12, 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 25} GROUPS = {'core': 6, 'resources': 1, 'schema': 2, 'streaming': 3} diff --git a/Sources/JSONSchema/Document/JSONSchemaBuiltins.swift b/Sources/JSONSchema/Document/JSONSchemaBuiltins.swift index e15d763..4e1f80c 100644 --- a/Sources/JSONSchema/Document/JSONSchemaBuiltins.swift +++ b/Sources/JSONSchema/Document/JSONSchemaBuiltins.swift @@ -37,7 +37,24 @@ extension JSONSchemaRegistry { let root = URL(fileURLWithPath: command).deletingLastPathComponent() .appendingPathComponent("Swift-JSON_JSONSchema.bundle", isDirectory: true) #else - guard let root = Bundle.module.resourceURL else { throw JSONSchemaCompilationFailure(.metaSchema) } + // The generated Bundle.module accessor traps if packaging is incomplete. + // Resolve the same app/framework/command layouts through checked lookup. + var candidates: [URL] = [] + #if DEBUG + if let override = ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_PATH"] + ?? ProcessInfo.processInfo.environment["PACKAGE_RESOURCE_BUNDLE_URL"] { + candidates.append(URL(fileURLWithPath: override)) + } + #endif + if let directory = Bundle.main.resourceURL { candidates.append(directory) } + #if canImport(ObjectiveC) + if let directory = Bundle(for: JSONSchemaBundleAnchor.self).resourceURL { candidates.append(directory) } + #endif + candidates.append(Bundle.main.bundleURL) + if let command = CommandLine.arguments.first { + candidates.append(URL(fileURLWithPath: command).deletingLastPathComponent()) + } + let root = try builtinResourceRoot(in: candidates, meter: &meter) #endif for (path, identifier, size, expectedHash) in files { try meter.checkpoint(); try meter.charge(.schemaDocuments); try meter.resetValueBounds() @@ -77,6 +94,26 @@ extension JSONSchemaRegistry { } return documents } + + /// Fixed-layout lookup is checked; a present corrupt bundle is selected and + /// rejected by the existing pins rather than bypassed via another candidate. + package static func builtinResourceRoot(in candidates: [URL], meter: inout JSONWorkMeter) throws -> URL { + for candidate in candidates { + try meter.checkpoint() + let pathBytes = UInt64(candidate.path.utf8.count) + try meter.require(.stringBytes, pathBytes) + try meter.work(pathBytes + 1) + try meter.retain(pathBytes + 128) + defer { try? meter.release(pathBytes + 128) } + // SwiftPM's Apple/Swift Build and Linux resource directory spellings. + for name in ["Swift-JSON_JSONSchema.bundle", "Swift-JSON_JSONSchema.resources"] { + try meter.work(UInt64(name.utf8.count) + 1) + let url = candidate.appendingPathComponent(name, isDirectory: true) + if let bundle = Bundle(url: url), let root = bundle.resourceURL { return root } + } + } + throw JSONSchemaCompilationFailure(.metaSchema) + } package static func verifyBuiltin(_ data: Data, size: UInt64, expectedHash: String, meter: inout JSONWorkMeter) throws { try meter.work() @@ -87,3 +124,7 @@ extension JSONSchemaRegistry { guard hash.utf8.elementsEqual(expectedHash.utf8) else { throw JSONSchemaCompilationFailure(.metaSchema) } } } + +#if canImport(ObjectiveC) +private final class JSONSchemaBundleAnchor {} +#endif diff --git a/Tests/JSONSchemaTests/ResourceLookupTests.swift b/Tests/JSONSchemaTests/ResourceLookupTests.swift new file mode 100644 index 0000000..b972e8d --- /dev/null +++ b/Tests/JSONSchemaTests/ResourceLookupTests.swift @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +@testable import JSONSchema + +@Test func missingPackagedResourcesAreCheckedAndLookupSharesTheMeter() async throws { + var meter = try JSONWorkMeter(limits: .balanced, domain: .schemaCompilation) + let directory = URL(fileURLWithPath: "/nonexistent-swift-json-resource-qualification") + do { + _ = try JSONSchemaRegistry.builtinResourceRoot(in: [directory], meter: &meter) + Issue.record("Expected checked missing-resource error") + } catch let error as JSONSchemaCompilationFailure { #expect(error.category == .metaSchema) } + #expect(meter.used(.workUnits) > 0 && meter.used(.retainedBytes) == 0) + var limits = JSONLimits.balanced; limits[.stringBytes] = 1 + var bounded = try JSONWorkMeter(limits: limits, domain: .schemaCompilation) + do { + _ = try JSONSchemaRegistry.builtinResourceRoot(in: [directory], meter: &bounded) + Issue.record("Expected lookup quota") + } catch let error as JSONFailure { #expect(error.category == .resource && error.resource?.metric == .stringBytes) } + let task = Task { + var limits = JSONLimits.balanced; limits[.cancellationQuantum] = 1 + var meter = try JSONWorkMeter(limits: limits, domain: .schemaCompilation) + meter.checkpointObserver = { work in if work > 0 { withUnsafeCurrentTask { $0?.cancel() } } } + #expect(throws: CancellationError.self) { _ = try JSONSchemaRegistry.builtinResourceRoot(in: [directory], meter: &meter) } + #expect(meter.used(.workUnits) > 0 && meter.used(.retainedBytes) == 0) + } + try await task.value +} From 7a1334378493a72c08d428d475c26ccb7120c607 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:10:59 +0200 Subject: [PATCH 34/59] docs(cli): distinguish Node pipe and regular-file WASI outcomes --- Documentation/Architecture/Release1.4/Integration.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Documentation/Architecture/Release1.4/Integration.md b/Documentation/Architecture/Release1.4/Integration.md index 506fa57..13775e3 100644 --- a/Documentation/Architecture/Release1.4/Integration.md +++ b/Documentation/Architecture/Release1.4/Integration.md @@ -75,8 +75,13 @@ archive/file hashes and isolated Git discovery. WASI now has narrow WASILibc/entry guards, with native signal behavior unchanged. The CLI module compiles with matched Swift6.4.0 SDK. WasmKit0.3.1 executes exact format, Cartesian query, completed root-array stream, offline schema and static -HTML commands. Node26.5.0 version execution fails IO exit7: actual poll_oneoff -returns WASI errno28; tracing is qualification-only. No blocking fallback is used. +HTML commands. The first Node26.5.0 version execution with file-backed output failed IO exit7: +actual poll_oneoff returned WASI errno28; tracing is qualification-only. The +corrected module passes version/format/query/root-array/static HTML with pipe +stdio. Explicit schema-file reading and regular-file stdout still fail poll +with errno28 and exit7. Descriptor layout is recorded in the return matrix; +these partial successes do not qualify all file/stdin/stdout adapters. No +blocking fallback is used. CLI POSIX SIGINT bridging is absent on WASI, so these five command outcomes are a limited disposition, not full CLI qualification. From 526e47dfd3552e81177664adcc28f067654b5131 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:01:48 +0200 Subject: [PATCH 35/59] feat(cli): add bounded strict JSON command foundation --- AGENTS.md | 5 +- .../Architecture/ExtractionManifest.json | 3 +- .../Architecture/Release1.4/Acceptance.json | 1186 +++++++++++++++++ .../Architecture/Release1.4/Foundation.md | 103 ++ Documentation/Architecture/Release1.4/Plan.md | 40 + .../Architecture/Release1.4/Progress.json | 41 + .../Architecture/Release1.4/Qualification.md | 50 + Documentation/README.md | 2 + Package.swift | 7 +- README.md | 16 + Scripts/ci.py | 20 +- Scripts/qualify_cli.py | 109 ++ Scripts/qualify_cli_ownership.py | 21 + Scripts/testing_events.py | 1 + .../JSON/Documentation.docc/CommandLine.md | 36 + Sources/JSON/Limits/JSONWorkMeter.swift | 19 +- Sources/JSON/Pointer/JSONPointer.swift | 29 +- Sources/JSONCLI/Main.swift | 26 + Sources/JSONCLIEngine/CLIArguments.swift | 89 ++ Sources/JSONCLIEngine/CLIDiagnostics.swift | 115 ++ Sources/JSONCLIEngine/CLIEngine.swift | 225 ++++ Sources/JSONCLIEngine/CLIFileIO.swift | 154 +++ .../JSONCLIEngine/CLIProcessArguments.swift | 46 + Sources/JSONFilterRuntime/CLIJobContext.swift | 153 +++ Sources/JSONPatch/JSONPatch.swift | 14 +- .../Compilation/JSONSchemaCompiler.swift | 7 + .../Evaluation/JSONSchemaValidator.swift | 6 +- Tests/JSONCLITests/FoundationTests.swift | 135 ++ 28 files changed, 2640 insertions(+), 18 deletions(-) create mode 100644 Documentation/Architecture/Release1.4/Acceptance.json create mode 100644 Documentation/Architecture/Release1.4/Foundation.md create mode 100644 Documentation/Architecture/Release1.4/Plan.md create mode 100644 Documentation/Architecture/Release1.4/Progress.json create mode 100644 Documentation/Architecture/Release1.4/Qualification.md create mode 100644 Scripts/qualify_cli.py create mode 100644 Scripts/qualify_cli_ownership.py create mode 100644 Sources/JSON/Documentation.docc/CommandLine.md create mode 100644 Sources/JSONCLI/Main.swift create mode 100644 Sources/JSONCLIEngine/CLIArguments.swift create mode 100644 Sources/JSONCLIEngine/CLIDiagnostics.swift create mode 100644 Sources/JSONCLIEngine/CLIEngine.swift create mode 100644 Sources/JSONCLIEngine/CLIFileIO.swift create mode 100644 Sources/JSONCLIEngine/CLIProcessArguments.swift create mode 100644 Sources/JSONFilterRuntime/CLIJobContext.swift create mode 100644 Tests/JSONCLITests/FoundationTests.swift diff --git a/AGENTS.md b/AGENTS.md index 443ee58..b8aa949 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,9 @@ Swift-JSON requires Swift tools/compiler 6.4+, Swift language mode 6 and macOS/SDK 27+. Read README.md, CONTRIBUTING.md and the relevant contracts in Documentation/Architecture before changing behavior. -- Keep five public products: JSON, JSONSchema, JSONStreaming, JSONPatch and JSONHTML. - JSONHTML depends on JSON only; the maintainer-approved 1.3 addition preserves historical four-product releases. +- Keep six public products: JSON, JSONSchema, JSONStreaming, JSONPatch, JSONHTML + and the swift-json executable. JSONFilterRuntime and JSONCLIEngine are package-private. + JSONHTML depends on JSON only. The approved 1.4 CLI addition preserves historical product contracts. JSONRegexRuntime is package-private. Root package dependencies remain empty. - Preserve exact bounded decimals, decoded Unicode scalar identity, ordered objects with order-independent equality, one strict UTF-8 grammar, native diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 735d569..66bf9f6 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -196,7 +196,8 @@ "Schema graph, explicit evaluator, exact sets/assertions and scalar format recognizers are original implementations from Draft 2020-12, RFC3986/3987/3339/3492/5321/5891/5892/5893/6531/6570/6901 and fixed Unicode13 data.", "Private FIPS 180-4 SHA-256 is an original Swift implementation with checked cumulative production accounting, bounded scratch and inner cancellation. The separate trusted fixture tool mirrors its primitive core byte-for-byte; public_check verifies parity. No CryptoKit fallback or runtime dependency is introduced.", "JSONHTML formatted rendering, original fixed CSS and shared strict-writer token classification are original Apache-2.0 code. No third-party renderer, browser numeric authority or runtime dependency is adapted.", - "JSONHTML typed annotations, iterative exact-path binding, static/interactive attachment and coherent JSONDocument span presentation are original Apache-2.0 code. No schema engine, third-party renderer, source archive or new dependency is adapted." + "JSONHTML typed annotations, iterative exact-path binding, static/interactive attachment and coherent JSONDocument span presentation are original Apache-2.0 code. No schema engine, third-party renderer, source archive or new dependency is adapted.", + "CLI foundation, argv parser, POSIX adapters, prepaid bounded diagnostics and shared-meter package adapters are original Apache-2.0 Swift code. No legacy filter engine or jq source is copied. See Release1.4/Foundation.md for native test-harness reuse provenance." ], "policy": "Record actual adaptations and retain applicable upstream copyrights and licenses. Reference inputs and comparison experiments are not distributed runtime dependencies.", "jsonhtml_export_original_contributions": { diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json new file mode 100644 index 0000000..5b17493 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -0,0 +1,1186 @@ +{ + "schema": "swift-json-cli-acceptance/1", + "prepared": "2026-10-14", + "implementation_status": "foundation boundary only; integration unqualified", + "requirements": [ + { + "id": "CL001", + "gate": "C0", + "title": "Live source/prerequisite reconciliation", + "verification": "Record actual main/base/HTML/provider heads and compare pins; preserve active work and dirty state.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL002", + "gate": "C0", + "title": "Six public products with correct kinds", + "verification": "Package dump contains five libraries plus swift-json executable; private targets have no products.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL003", + "gate": "C0", + "title": "Empty root dependency set", + "verification": "Dump/package resolution and clean-source build prove no external dependency was introduced.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL004", + "gate": "C0", + "title": "Dependency direction and mobile consumers", + "verification": "Selected library consumers have no CLI/filter dependencies; core has no HTML/provider/UI imports.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL005", + "gate": "C0", + "title": "Forward policy and historical records", + "verification": "Update AGENTS/README/CI prospective inventory; historical releases remain accurate.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL006", + "gate": "C0", + "title": "Private compiler/IR ownership", + "verification": "Compiler probes prove immutable checked-Sendable plans and invocation-owned noncopyable context.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL007", + "gate": "C0", + "title": "Attribution and extraction manifest", + "verification": "Every actual adaptation has pinned origin/status/notices; inherited files retain identity.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL008", + "gate": "C0", + "title": "Prior brief conflict resolution", + "verification": "Record reused HTML/tab/provider APIs and authoritative amendments; no duplicate conformance or restarted portability.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL009", + "gate": "C1", + "title": "Argv parsing and limits", + "verification": "Exercise long equals, grouped Boolean flags, leading-dash filenames, --, duplicates, unknown/conflicting options and bounded args.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [ + "unknown-option" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL010", + "gate": "C1", + "title": "Truthful help/version", + "verification": "Only available qualified commands appear; missing command/config/capability is 2; help/version is 0.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL011", + "gate": "C1", + "title": "Strict native format", + "verification": "Single root, full EOF, native UTF-8/duplicate-key grammar, exact lexemes and default two-space formatting.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "format-preserved-lexemes", + "reject-duplicate-key", + "reject-trailing-root", + "reject-trailing-comma" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL012", + "gate": "C1", + "title": "Explicit presentation options", + "verification": "Compact/indent/tab/sort/number-style options preserve library defaults and scalar key order.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "conflicting-style-options" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL013", + "gate": "C1", + "title": "Syntax validation outcomes", + "verification": "Completed valid/invalid result and issues; malformed input 5, resource 6, IO 7 are separate.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL014", + "gate": "C1", + "title": "Offline schema validation", + "verification": "Compile once per job; explicit immutable resources; instance invalid 1; no fetch/default insertion.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [ + "validate-instance-invalid" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL015", + "gate": "C1", + "title": "Pointer presence", + "verification": "Present null versus missing, escaped tokens, root pointer, allow-missing exit4 and type/domain errors.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "pointer-present-null", + "pointer-missing-allowed", + "pointer-escaped-key" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL016", + "gate": "C1", + "title": "Patch transactional candidate", + "verification": "Successful operations emit full result; failed test/path never emits staged mutation or changes source.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [ + "patch-success", + "patch-failed-test" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL017", + "gate": "C1", + "title": "Incremental local IO", + "verification": "Owned chunk reads, explicit closure, split UTF-8 and partial/no-progress writes; no read-to-end job arrays.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL018", + "gate": "C1", + "title": "Stdout/stderr separation", + "verification": "All data is clean; JSON diagnostics/reports are tagged stderr lines with no ANSI/banner.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL019", + "gate": "C1", + "title": "Shared compound accounting", + "verification": "Nested parse/validate/patch/write and multiple records cannot reset work or retained overlap; failure latches.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL020", + "gate": "C1", + "title": "Every CLI cap boundary", + "verification": "Inclusive-cap and cap+1 sweeps for all 16 CLI caps; checked counters and conversion overflow.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL021", + "gate": "C1", + "title": "Existing native limits preserved", + "verification": "49 metric inventory/meaning and existing independent boundary tests remain intact.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL022", + "gate": "C1", + "title": "Actual in-loop cancellation", + "verification": "Checkpoint rendezvous inside long owned work and source/sink suspend; no further output after terminal failure.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL023", + "gate": "C1", + "title": "Atomic file success", + "verification": "Owned same-directory stage installed only after EOF/flush/close; report committed completion.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL024", + "gate": "C1", + "title": "Atomic file failure", + "verification": "Late invalid tail/resource/cancel/IO preserves destination and removes only owned stage.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL025", + "gate": "C1", + "title": "Overwrite/path policy", + "verification": "Existing file requires --overwrite; input/output alias and output symlink reject; no silent cross-FS fallback.", + "obligation": "mandatory", + "status": "passed", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL026", + "gate": "C1", + "title": "Exit precedence", + "verification": "Codes0/1/2/3/4/5/6/7/130 and completed-result precedence verified in actual subprocesses.", + "obligation": "mandatory", + "status": "partial", + "evidence_required": true, + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Qualification.md" + ] + }, + { + "id": "CL027", + "gate": "C2", + "title": "Native query lexer/literals", + "verification": "Exact numbers and scalar text/keys with source byte ranges; malformed source compiles to exit3.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL028", + "gate": "C2", + "title": "Grammar precedence", + "verification": "Pipeline/comma/default/assignment/operator precedence and delimiters tested beyond old parser goldens.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "pipeline-comma-precedence" + ] + }, + { + "id": "CL029", + "gate": "C2", + "title": "Pull VM cardinality", + "verification": "Empty/one/many outputs without whole-job array; next() demand limits observable reads/retention.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL030", + "gate": "C2", + "title": "Pipeline and comma order", + "verification": "Every upstream result is evaluated and ordered; late errors do not resume a failed branch.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL031", + "gate": "C2", + "title": "Cartesian operators/constructors", + "verification": "No .first collapse; object versus arithmetic nesting orders and shared input effects are tested.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "object-product-order" + ] + }, + { + "id": "CL032", + "gate": "C2", + "title": "Navigation and wrong-type", + "verification": "Missing/null convention, has, wrong-type error, optional catch and checked indices/slices.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "missing-field-null", + "wrong-type-field", + "optional-wrong-type-empty", + "has-present-null", + "negative-index" + ] + }, + { + "id": "CL033", + "gate": "C2", + "title": "Exact Unicode identity", + "verification": "Scalar-distinct strings/keys, scalar length/slicing/order and non-BMP behavior.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "scalar-key-lookup", + "scalar-string-inequality", + "scalar-length", + "utf8-length", + "scalar-slice" + ] + }, + { + "id": "CL034", + "gate": "C2", + "title": "Truth/default/Boolean generators", + "verification": "Only false/null falsey; // handles full streams; left-driven short-circuit and if generators.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "truthiness", + "fallback-whole-generator", + "fallback-empty", + "Boolean-short-circuit", + "if-condition-stream" + ] + }, + { + "id": "CL035", + "gate": "C2", + "title": "Constructed duplicate keys", + "verification": "Explicit replace-preserve-position policy; parsed exact duplicate keys still reject.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "constructed-key-replacement" + ] + }, + { + "id": "CL036", + "gate": "C2", + "title": "Exact comparison/order", + "verification": "Metered decimal/string/array/object equality and total order with stable ties.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL037", + "gate": "C2", + "title": "Common builtin inventory", + "verification": "All C2 builtins and type/domain/empty cases; map collects and map_values updates/deletes correctly.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "empty-array-collector", + "map-all-results", + "map-values-first", + "map-values-delete", + "normals-nonzero-native" + ] + }, + { + "id": "CL038", + "gate": "C2", + "title": "Lexical bindings/arguments", + "verification": "as, --arg/argjson/rawfile/slurpfile, $ARGS, duplicate/reserved names, strict auxiliary-file admission.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "binding-original-input", + "argjson-exact" + ] + }, + { + "id": "CL039", + "gate": "C2", + "title": "Shared input cursor", + "verification": "Driver/input/inputs share consumption; EOF catchable only for input; malformed tails/IO remain fatal.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "null-input-once", + "shared-inputs" + ] + }, + { + "id": "CL040", + "gate": "C2", + "title": "JSON/raw framing", + "verification": "values/document/NDJSON and strict raw UTF-8 lines/slurp; files have independent boundaries.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "values-framing", + "adjacent-roots-rejected", + "ndjson-blank-line-rejected", + "raw-line-crlf" + ] + }, + { + "id": "CL041", + "gate": "C2", + "title": "Raw output/NUL/join", + "verification": "Correct string bytes and nonstring JSON; embedded NUL rejected; delimiters/errors/counts tested.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "raw-lf", + "raw-nul-delimiter", + "raw-nul-value-rejected", + "join-no-delimiter" + ] + }, + { + "id": "CL042", + "gate": "C2", + "title": "ASCII output and sort keys", + "verification": "Owned escaping, surrogate pairs and exact scalar order; option conflicts tested.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "ascii-surrogate-pair" + ] + }, + { + "id": "CL043", + "gate": "C2", + "title": "Query exit-status", + "verification": "Final semantic value only after completion; truthy0/empty string/collections; false/null1 and noresult4.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "exit-status-truthy-zero", + "exit-status-false", + "exit-status-empty" + ] + }, + { + "id": "CL044", + "gate": "C2", + "title": "Plan reuse and isolation", + "verification": "Concurrent independent invocations share immutable plan only; mutable iterator not concurrently accessed.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL045", + "gate": "C3", + "title": "Exact add/subtract/multiply", + "verification": "Carry/borrow/sign/zero/large integer cases against independent exact expectations.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "Cartesian-add-order", + "exact-large-integer", + "exact-decimal-add", + "exact-borrow", + "exact-multiply" + ] + }, + { + "id": "CL046", + "gate": "C3", + "title": "Numeric admission and lexemes", + "verification": "Computed values normalized with no false originalLexeme; untouched inputs preserve spelling.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL047", + "gate": "C3", + "title": "Terminating exact division", + "verification": "Reduced denominators containing only2/5, large common factors and mixed signs.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "terminating-division" + ] + }, + { + "id": "CL048", + "gate": "C3", + "title": "Repeating division default", + "verification": "1/3 and repeating values give language inexactDivision; no floating fallback.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "repeating-division-error" + ] + }, + { + "id": "CL049", + "gate": "C3", + "title": "Explicit scale/half-even", + "verification": "Required option pair, decimal places, finite caps, signed ties and report rounding counts.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "rounded-third", + "half-even-ties", + "scale-pair-required" + ] + }, + { + "id": "CL050", + "gate": "C3", + "title": "Zero division and integral remainder", + "verification": "Typed catchable domain errors; signed truncated remainder; fractional remainder rejected.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "negative-remainder", + "fractional-remainder-rejected" + ] + }, + { + "id": "CL051", + "gate": "C3", + "title": "Exact numeric builtins", + "verification": "abs/floor/ceil/trunc/round/range/add with negative and empty/domain cases; round ties away from zero.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "round-away-from-zero", + "range-negative-step", + "range-zero-step" + ] + }, + { + "id": "CL052", + "gate": "C3", + "title": "Exponent/growth preflight", + "verification": "Large sparse exponent operations fail finite caps before dense allocation; exact zero shortcut allowed.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "exponent-gap-cap" + ] + }, + { + "id": "CL053", + "gate": "C3", + "title": "Numeric work and cancellation", + "verification": "Charged GCD/division/carry inner loops; boundaries and checkpoint cancellation; terminal errors not catchable.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL054", + "gate": "C3", + "title": "32-bit arithmetic safety", + "verification": "No Int/UInt64 negation/narrowing/product overflow traps; wasm32 kernel tests or explicit gate blocker.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL055", + "gate": "C3", + "title": "No Decimal/Double authority", + "verification": "Source audit plus precision cases prove no foreign arithmetic/fallback authority.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL056", + "gate": "C4", + "title": "Reduce/foreach streams", + "verification": "Last-update state, every foreach extraction, multiple inits, empty updates and actual evaluation order.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "reduce-last-update", + "reduce-empty-update", + "foreach-every-update", + "reduce-multiple-inits" + ] + }, + { + "id": "CL057", + "gate": "C4", + "title": "Lexical filter functions", + "verification": "Filter/value parameters, arity/scopes, self-recursion, no forward refs and finite explicit frames.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "filter-parameter-code", + "forward-definition-rejected", + "recursive-definition" + ] + }, + { + "id": "CL058", + "gate": "C4", + "title": "Loops/recurse/walk", + "verification": "Correct order/null handling/parent-after-child behavior with bounded frames/work and no fixed hidden10k cap.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "walk-child-before-parent", + "recurse-null-leaves" + ] + }, + { + "id": "CL059", + "gate": "C4", + "title": "Typed try/catch/optional", + "verification": "Catch only language errors; prior yields/handler streams correct; syntax/resource/cancel/IO never suppressed.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "input-eof-catch", + "malformed-input-cannot-catch", + "repeating-division-caught", + "division-zero-caught", + "try-prior-yield" + ] + }, + { + "id": "CL060", + "gate": "C4", + "title": "Collection builtin inventory", + "verification": "sort/group/unique/min/max/by forms, stable ties, multi-key generator lists, finite materialization.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "sort-exact-integers", + "total-order", + "group-scalar-distinct" + ] + }, + { + "id": "CL061", + "gate": "C4", + "title": "Generator first/last/nth", + "verification": "Correct pull/empty behavior and completion of remaining input; no hidden job early success.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "first-generator" + ] + }, + { + "id": "CL062", + "gate": "C4", + "title": "Flatten/any/all", + "verification": "Depth bounds, empty identities, short circuit and explicit deep traversal.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "all-any-empty" + ] + }, + { + "id": "CL063", + "gate": "C4", + "title": "Literal string helpers", + "verification": "Scalar contains/search/split/join/ASCII trim/case; empty patterns/type errors and bounded scan.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "split-empty-scalars", + "index-empty-pattern" + ] + }, + { + "id": "CL064", + "gate": "C4", + "title": "Conversions/explode/implode", + "verification": "Strict tonumber, preserved tostring, non-BMP scalar ints, invalid surrogate/range/type rejection.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "explode-non-BMP", + "implode-surrogate-rejected", + "tonumber-exact", + "tonumber-whitespace-rejected" + ] + }, + { + "id": "CL065", + "gate": "C4", + "title": "Path enumeration/admission", + "verification": "Exact segments, missing paths, generated paths, shape/domain restrictions and no Pointer conflation.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "setpath-created-array" + ] + }, + { + "id": "CL066", + "gate": "C4", + "title": "Assignment = versus |=", + "verification": "Original versus current-value RHS input; generator output, first update/empty delete semantics.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "assignment-set-generator", + "assignment-update-first", + "assignment-update-delete" + ] + }, + { + "id": "CL067", + "gate": "C4", + "title": "Transactional mutation/deletion", + "verification": "Stable array removals, deduplicated targets, overlap rejection and input unchanged on failure.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "assignment-overlap-rejected" + ] + }, + { + "id": "CL068", + "gate": "C4", + "title": "Sparse path growth", + "verification": "setpath container creation/null fill bounded before allocation and overflow.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "sparse-growth-fails" + ] + }, + { + "id": "CL069", + "gate": "C4", + "title": "String interpolation streams", + "verification": "Cartesian holes/literal identity/empty hole; formatter holes only with no eager unbounded strings.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "interpolation-generators", + "interpolation-empty" + ] + }, + { + "id": "CL070", + "gate": "C4", + "title": "Bounded format encoders", + "verification": "All C4 formatters, strict base64/UTF8, flat CSV/TSV, percent/HTML/POSIX quoting; no shell execution.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "format-html", + "format-base64" + ] + }, + { + "id": "CL071", + "gate": "C4", + "title": "Regex deferral", + "verification": "Compile-time unavailable diagnostics; no ICU/ECMA substitutions or compatibility claim.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "regex-not-advertised" + ] + }, + { + "id": "CL072", + "gate": "C4", + "title": "Other exclusions", + "verification": "Modules/seq/jq-stream/dates/env/halt/remote/server/converters rejected or absent from truthful help.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "numeric-transcendental-deferred", + "env-not-available" + ] + }, + { + "id": "CL073", + "gate": "C4", + "title": "Every inherited fixture disposition", + "verification": "119+49 originals retained, actual per-row status/reason/gate/claim, exclusions separated from successes.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL074", + "gate": "C5", + "title": "Stream completion commands", + "verification": "Check/cat/root-array reader, strict line/tail behavior and completed summary exactly once.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "stream-root-array", + "stream-root-array-bad-tail" + ] + }, + { + "id": "CL075", + "gate": "C5", + "title": "Static HTML rendering", + "verification": "JSONHTML integration with no script, expanded nodes, exact values/keys, escaping and finite shared accounting.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "render-static-inert" + ] + }, + { + "id": "CL076", + "gate": "C5", + "title": "Interactive HTML rendering", + "verification": "Approved fixed CSS/JS self-contained document with inert payloads; no server or external assets.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL077", + "gate": "C5", + "title": "Text/Markdown output", + "verification": "Tab pretty body, safe JSON fence and exact tokens; shared qualified writer options/defaults unchanged.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "render-markdown" + ] + }, + { + "id": "CL078", + "gate": "C5", + "title": "Offline schema/provider commands", + "verification": "Native schema check plus qualified local OpenAI helpers; schema object not quoted string, no network/key.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "schema-provider-object-envelope" + ] + }, + { + "id": "CL079", + "gate": "C5", + "title": "Provider incompatibility and availability", + "verification": "No constraint loss/lowering; compatibility false exit1; Apple signatures guarded and advertised only when qualified.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [ + "schema-provider-optionality-reject" + ] + }, + { + "id": "CL080", + "gate": "C5", + "title": "Native macOS executable qualification", + "verification": "Actual SDK27/Swift6.4 build/run/subprocess tests at final head.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL081", + "gate": "C5", + "title": "Linux x86_64 executable qualification", + "verification": "Actual Ubuntu24.04 x86_64 runtime and Linux/linux-safety receipts; not crosscompile-only.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL082", + "gate": "C5", + "title": "Linux ARM64 executable qualification", + "verification": "Actual Ubuntu24.04 ARM64 runtime and Linux/linux-safety receipts.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL083", + "gate": "C5", + "title": "Mobile library compatibility", + "verification": "Selected-product iOS/iPadOS consumers and existing mobile qualification preserved; no CLI process support claim.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL084", + "gate": "C5", + "title": "WASI CLI disposition", + "verification": "Explicit final supported/blocked/limited evidence with resources/argv/preopen/counters; no inherited CLI pass.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL085", + "gate": "C5", + "title": "Baseline WASI/core preservation", + "verification": "Existing all-four-product resource/integrity/49metrics/platform lanes retain exact truthful evidence.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL086", + "gate": "C5", + "title": "Installed resources/clean extraction", + "verification": "Source archive rebuilt, executable run outside repo cwd, sibling schema bundle and missing/tampered resource tests.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL087", + "gate": "C5", + "title": "Fresh oracle and comparisons", + "verification": "Pinned jq1.8.2 build/hash; native exact comparison plus byte/exit modes; missing oracle blocks that lane.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL088", + "gate": "C5", + "title": "Independent exact expectations", + "verification": "Integer/rational/Unicode/cardinality expectations separate from native self-comparison and jq floats.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL089", + "gate": "C5", + "title": "Streaming retention/performance", + "verification": "Generated chunks/slow sink/large record counts and bounded collectors; release-mode identities/variance, no blanket speedup.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL090", + "gate": "C5", + "title": "Six-product CI/documentation", + "verification": "Existing native/safety/docs plus affected Linux lanes, exact product kinds and public CLI help/docs/examples.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL091", + "gate": "C5", + "title": "Final-head review/return evidence", + "verification": "Actual PR/base/head topology, nonzero counts, immutable IDs/log hashes/fixture results, blockers and release recommendation.", + "obligation": "mandatory", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL092", + "gate": "C5", + "title": "Optional parallel evaluator", + "verification": "Only independent record filters; reserved bounded tasks/reorder buffers, preserved order/errors and measured material gain.", + "obligation": "optional-extension", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL093", + "gate": "C5", + "title": "Optional Apple PDF/RTF CLI export", + "verification": "Only after existing export backend qualification; actual host, explicit async preparation/page capabilities and unsupported-host errors.", + "obligation": "optional-extension", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + }, + { + "id": "CL094", + "gate": "C5", + "title": "Optional native binary distribution", + "verification": "Explicit packaging policy, architecture/resource/signature receipts; no publication/tag performed by this task.", + "obligation": "optional-extension", + "status": "not_run", + "evidence_required": true, + "fixture_ids": [] + } + ], + "notes": [ + "Package integrity is separate from native qualification.", + "Optional extensions do not block the scoped candidate when not claimed.", + "WASI CLI supported/blocked/limited disposition is mandatory; full CLI support is separately earned." + ] +} diff --git a/Documentation/Architecture/Release1.4/Foundation.md b/Documentation/Architecture/Release1.4/Foundation.md new file mode 100644 index 0000000..9d5f7ac --- /dev/null +++ b/Documentation/Architecture/Release1.4/Foundation.md @@ -0,0 +1,103 @@ +# CLI foundation + +This feature boundary implements format, strict validation, RFC 6901 selection, +staged RFC 6902 application and HTML/text/Markdown rendering. It does not advertise +the query engine, stream/schema command adapters or provider helpers before their +own increments are qualified. The target release is 1.4.0; this is development source. + +## Composition and ownership + +`CLIJobContext: ~Copyable` lives in the private runtime target. Argv, native parsing, +number projection, registry/plan admission, validation, Patch, rendering and writing +borrow its one `JSONWorkMeter`. Package-only borrowed entry points introduce no new +public library signatures. Switching the operation domain changes subsystem work +accounting without replacing health or counters. Only per-value shape capacities +reset; consumed work never resets. Patch refunds its local staging reservation and +preserves ownership that preceded its invocation. + +CLI policy has 16 additional finite quotas. Core policy keeps exactly 49 metric +identifiers. Effective work and logical-retention ceilings are the intersection of +CLI and core ceilings. Configuration bootstraps under the finite offline profile, +then validates requested caps against already consumed work and admitted capacities. +Explicit finite overrides reuse the ledger. They cannot erase bootstrap input. + +Error/cancellation latches terminal health. Data paths cannot resume after failure. +Diagnostics reserve finite work and buffer ownership before command execution; +the bounded terminal serializer spends that reservation and cannot reopen the job. +Reports distinguish provisional results from fully accepted stdout results and +atomically installed file results. Logical retained bytes are not process RSS. + +## Native I/O + +The adapters poll nonblocking POSIX descriptors, check cancellation between polls, +and handle short reads/writes, EINTR, EAGAIN, EOF and broken pipes explicitly. +Filesystem/kernel work is foreign work; cancellation does not claim to interrupt +a filesystem operation already executing in the kernel. Borrowed stdio flags are +restored. The signal callback is explicitly `@Sendable`; a real subprocess test +caught and fixed an inferred-actor dispatch assertion on SIGINT. + +File output uses an exclusive owned temporary in the destination directory. +Installation happens after parsing, command evaluation, writing and close succeed. +No-overwrite installation uses link/unlink; explicit overwrite uses rename. +Existing destinations, final symlinks and input aliases/hard links are checked. +Failures remove only the invocation's temporary and preserve the destination. +Concurrent pathname replacement by another process is outside this adapter's +transactional guarantee. There is no cross-filesystem fallback, mkdir, fsync or +power-loss durability claim. Previously accepted stdout bytes cannot be recalled. + +## Checks + +Twelve meaningful Swift Testing functions include four parameter cases. The executable +subprocess harness has 53 cases covering exact lexemes/scalar keys, strict errors, +schema invalidity versus operational failure, bounded issues, Pointer null/absence, +root removal, failed Patch staging, renderer selection, limits/reports, filesystem +aliases, atomic cleanup and SIGINT while stdin is idle. The initial failure receipts +are retained; the corrected subprocess lane passes all 53. + +The event receipt reader is reused from the separately qualified 1.3 export work +at `1e0b86d2580bb9678039597523aa3b99d2905abf`. No export/PDF feature source is copied. +The six-product checks retain exact target/dependency/toolchain/platform assertions, +all baseline test bundle counts, 173 inherited parameter cases plus four CLI cases, +and all 49 cap probe outcomes. Native/safety/docs qualification remains required +for the committed source. + +## Compiler probes and review corrections + +The supported native compiler accepts an inout job borrow, rejects consuming the +same job twice with the expected ownership diagnostic, and rejects access from an +external package. The noncopyable initializer constructs its meter before the +other stored assignments; this follows actual Swift 6.4 initialization rules. +An initial default-CLT/cache attempt is unqualified and retained as blocked evidence; +its negative exits are not ownership passes. + +Strict memory-safety compilation marks individual synchronous POSIX pointer and +CString expressions `unsafe`. Byte pointers stay within `withUnsafeBytes` scopes; +counts are checked against the array slice, poll/stat pointers address live local +structures, and C strings are borrowed only for a synchronous syscall. No pointer +escapes, suspension crosses a borrow, unsafe compiler option or blanket annotation +is introduced. + +The SIGINT acceptance test waits for its exclusive staging file as a readiness +signal and holds stdin open until exit. Closing stdin through `communicate()` +immediately after SIGINT introduced a competing EOF and was corrected. The test +still requires exit 130, no output, and complete staging cleanup. + +The final argument adapter reads process-owned C argv under the finite bootstrap +ledger, rejects invalid UTF-8 instead of accepting replacement characters, and +checks each byte before copying. Structured diagnostic preferences survive usage +errors. Malformed/overflowing limit configuration is usage failure; exhaustion, +cancellation and I/O remain operational. Missing array Pointer targets use a +package-only presence adapter over the shared strict index decoder; malformed +indices and wrong kinds stay errors. Existing public Pointer resolution retains +its previous error contract. These review findings have dedicated regressions. + +Named inputs open with O_NONBLOCK from the initial syscall, so FIFO open does not +wait indefinitely before cancellation handling begins. Subprocess cancellation +covers both idle borrowed stdin and an owned named FIFO. Reports include actual +enforced native caps, remaining cumulative work and profile/override origin. + +The terminal diagnostic builder checks each append before growth and discards an +incomplete report before handoff. Zero forbids diagnostic output; small diagnostic +quotas cannot produce broken JSONL. HTML-specific quotas map to resource exit 6; +renderer configuration maps to usage exit 2. Typed schema compile/evaluation errors +stay operational exit 5, with stable subsystem codes. These have explicit regressions. diff --git a/Documentation/Architecture/Release1.4/Plan.md b/Documentation/Architecture/Release1.4/Plan.md new file mode 100644 index 0000000..9ae86d6 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Plan.md @@ -0,0 +1,40 @@ +# CLI release plan + +The maintainer-approved 1.4.0 train adds `swift-json` as an executable beside the +five libraries. JSONFilterRuntime and JSONCLIEngine are package-private targets; +JSONRegexRuntime remains private. Root external dependencies remain empty. +Historical four/five-product releases and their evidence are unchanged. + +The CLI stack starts at the qualified JSONHTML renderer commit +`3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7`, PR #21. Its native, safety, docs and +Linux x86_64/arm64 lanes passed. Main at orientation was +`25fb04f747e583209b4c4511abf39e11ef5b4f80`. This train uses dependent feature +branches and draft PRs; it does not change the parallel transfer/PDF or 1.2 work. + +| Gate | Cohesive implementation and acceptance | +| --- | --- | +| C0 | Source/protection/prerequisite audit, executable/private target shapes, one noncopyable job ledger | +| C1 | Bounded argv/stdio/files, diagnostics/exits, atomic output, format/validate/Pointer/Patch, six-product inventory | +| C2 | Original flat filter IR and pull VM, input framing, generators/paths/control/bindings/common builtins | +| C3 | Private exact decimal arithmetic; explicit scale plus half-even division policy | +| C4 | Qualified richer filters, lexical functions/reductions/path updates; explicit unsupported features | +| C5 | Qualified renderer/schema/provider adapters, consumers, independent corpus/oracle, platform/distribution and final candidate | + +The planned stack is foundation, query-core, arithmetic, richer filters and +integration. Each has a compiling boundary, exact acceptance receipts and a draft +PR against its actual dependency. No public query library, eager legacy evaluator, +remote resolver, shell executor, daemon or release action is introduced. + +The input briefing SHA-256 is +`43f8819c57e3d93bc6f7cf561d2d111442e089f2973cb7c62e16e3bd3fd73d0d`. +Its structural verifier counted 94 requirements, 111 new cases and 168 inherited +cases; those are inventory counts, not execution successes. The jq 1.7 observations +are historical references; differential claims require an actual pinned jq 1.8.2 +runner. Missing provider APIs/platform runners remain explicit blockers. + +Swift 6.4, language 6 and macOS/SDK 27 remain the native baseline. Mobile libraries +must continue to compile independently of the executable. Linux is a process +host; WASI executable feasibility is separate from the already qualified libraries. +Native and sanitizer receipts, subprocess outcomes, commands, toolchain, source and +binary identities accompany every qualified milestone. All 49 baseline probes and +independent-suite exclusions remain required and separate from CLI additions. diff --git a/Documentation/Architecture/Release1.4/Progress.json b/Documentation/Architecture/Release1.4/Progress.json new file mode 100644 index 0000000..176a9e8 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Progress.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "target_release": "1.4.0", + "renderer_base": "3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7", + "main_at_orientation": "25fb04f747e583209b4c4511abf39e11ef5b4f80", + "branch": "codex/1.4-json-cli-foundation", + "status": "Compiling foundation boundary qualified on native macOS; C0/C1 complete closure and C2-C5 remain pending", + "gates": { + "C0": "partial", + "C1": "partial", + "C2": "not_run", + "C3": "not_run", + "C4": "not_run", + "C5": "not_run" + }, + "foundation_functions": 12, + "foundation_parameter_cases": 4, + "subprocess_cases": 53, + "subprocess_passed": 53, + "baseline_parameter_cases": 173, + "core_metrics": 49, + "cli_metrics": 16, + "publication_performed": false, + "root_test_functions": 232, + "root_parameter_cases": 177, + "native_source_manifest_sha256": "864d9b3dd8e1d79a0a6cbec862476843ddca2ff8a1582471b80fba54b863459e", + "qualified_lanes": [ + "native", + "safety", + "docs" + ], + "remaining_foundation_requirements": [ + "clean-source reproduction", + "selected mobile SDK consumers", + "immutable filter-plan compiler probe and implemented IR", + "all 16 CLI cap boundaries", + "multi-record shared accounting", + "slow/partial/no-progress sink probes", + "query compile exit 3 and full exit precedence" + ] +} diff --git a/Documentation/Architecture/Release1.4/Qualification.md b/Documentation/Architecture/Release1.4/Qualification.md new file mode 100644 index 0000000..460214c --- /dev/null +++ b/Documentation/Architecture/Release1.4/Qualification.md @@ -0,0 +1,50 @@ +# Foundation qualification boundary + +The foundation code and harness were qualified together under source manifest +`864d9b3dd8e1d79a0a6cbec862476843ddca2ff8a1582471b80fba54b863459e`. +The receipts identify the renderer base HEAD plus the actual edited-file hashes; +this is pre-commit evidence, not a claim that the parent commit contains the CLI. +Only this ledger, progress and qualification prose were subsequently updated. +Production Swift, the package manifest and executed scripts match those receipts. + +Native environment: arm64 macOS 27.0; Swift 6.4 +(swiftlang-6.4.0.34.1), language mode 6, Xcode 27.2 (27B5019j), SDK 27.2. +The configured beta Xcode is required on this host; the default CLT attempt was +unqualified and is retained separately. No baseline or compiler check was lowered. + +| Command | Actual outcome | +| --- | --- | +| `python3 Scripts/ci.py --lane native --output .build/ci/cli-c1-reviewed-native` | Exit 0; all 18 commands passed; debug/release each 232 functions, 177 parameter invocations | +| `python3 Scripts/ci.py --lane safety --output .build/ci/cli-c1-reviewed-safety` | Exit 0; ASan 232 functions/177 parameter invocations; TSan seven shared-state functions; strict-memory-safety build with warnings as errors | +| `python3 Scripts/ci.py --lane docs --output .build/ci/cli-c1-reviewed-docs` | Exit 0; five DocC catalogues, coverage checks and compiled examples | +| `python3 Scripts/qualify_cli.py --binary .build/release/swift-json --output …` | Exit 0; 53 real subprocess cases, no exclusions | +| `python3 Scripts/qualify_cli_ownership.py --output …` | Exit 0; three compiler probes: borrow compiles; double consume and cross-package access fail with required diagnostics | + +The inherited 173 parameter cases remain unchanged; four CLI parameter cases are +additional. All 49 baseline cap probes keep their recorded outcomes. Independent +syntax qualification observes 318 cases: 287 evaluated successes and 31 policy +cases. Schema qualification observes 2,337: 2,300 evaluated successes and 37 policy +cases. Authored suites retain 82 public semantics, nine budget/counter cases and +4,396 deterministic malformed-input replays. The downstream consumer retains 20 +explicitly rooted outcomes: eight positive, twelve negative, with two operational +error outcomes distinguished from ordinary invalidity. These do not establish MCP +server conformance. + +The three final lane receipts report unchanged source inputs while running. Earlier +failed compiler, SIGINT actor-isolation, EOF-race, normalized-spelling expectation +and strict-pointer-annotation attempts are retained alongside successful corrections. +The expected normalized `1e-1` spelling follows the existing coefficient/exponent +contract; no existing test was weakened. + +This is a reviewable foundation milestone. All 94 acceptance IDs remain in +Acceptance.json. Partial/not_run rows are not successes: complete filter IR/VM, +arithmetic/richer filters, all 16 CLI cap boundaries, split/slow/no-progress I/O, +selected mobile/portable consumers and clean candidate reproduction remain pending. +The renderer prerequisite is PR #21 at `3c1e8a1`; transfer/PDF work and the parallel +1.2 train are unchanged. Provider bridge APIs are absent at this base; qualification +and integration must precede their advertisement. No tag, merge or publication is +performed by this boundary. + +Materialized receipts and their exact source-input records are in the local +foundation evidence bundle. Run the listed commands to regenerate evidence; local +build outputs and private input packets are excluded from public Git/source archives. diff --git a/Documentation/README.md b/Documentation/README.md index 44892ae..9e022d6 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -33,6 +33,8 @@ Compiled archives and public symbol graphs remain available in CI artifacts. - [Linux platform and portable schema integrity](Architecture/Linux.md) - [WebAssembly audit and unresolved qualification boundary](Architecture/WebAssemblyAudit.md) +- [Prospective 1.4 CLI plan](Architecture/Release1.4/Plan.md) and [foundation contract](Architecture/Release1.4/Foundation.md) + ## Consumers, qualification and maintenance - [Five-product compiled example](../Examples/Consumer/README.md) diff --git a/Package.swift b/Package.swift index 4fbde36..c68db49 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,8 @@ let package = Package( .library(name: "JSONSchema", targets: ["JSONSchema"]), .library(name: "JSONStreaming", targets: ["JSONStreaming"]), .library(name: "JSONPatch", targets: ["JSONPatch"]), - .library(name: "JSONHTML", targets: ["JSONHTML"]) + .library(name: "JSONHTML", targets: ["JSONHTML"]), + .executable(name: "swift-json", targets: ["JSONCLI"]) ], targets: [ .target(name: "JSON"), @@ -19,6 +20,10 @@ let package = Package( .target(name: "JSONStreaming", dependencies: ["JSON"]), .target(name: "JSONPatch", dependencies: ["JSON"]), .target(name: "JSONHTML", dependencies: ["JSON"], exclude: ["Assets"]), + .target(name: "JSONFilterRuntime", dependencies: ["JSON"]), + .target(name: "JSONCLIEngine", dependencies: ["JSON", "JSONSchema", "JSONStreaming", "JSONPatch", "JSONHTML", "JSONFilterRuntime"]), + .executableTarget(name: "JSONCLI", dependencies: ["JSONCLIEngine"]), + .testTarget(name: "JSONCLITests", dependencies: ["JSONCLIEngine", "JSONFilterRuntime", "JSON", "JSONPatch", "JSONSchema", "JSONHTML"]), .testTarget(name: "JSONHTMLTests", dependencies: ["JSONHTML", "JSON"]), .testTarget(name: "JSONTests", dependencies: ["JSON"], exclude: ["ImplementationPlan.md"]), .testTarget(name: "JSONSchemaTests", dependencies: ["JSONSchema"], exclude: ["ImplementationPlan.md"]), diff --git a/README.md b/README.md index 7ab8d4e..a6a0826 100644 --- a/README.md +++ b/README.md @@ -474,3 +474,19 @@ following [SECURITY](SECURITY.md); see [SUPPORT](SUPPORT.md) for maintenance and Original code is licensed under [Apache-2.0](LICENSE). Adapted code and qualification inputs retain their applicable [notices](NOTICE), [third-party notices](THIRD_PARTY_NOTICES.md), [licenses](Licenses) and [extraction provenance](Documentation/Architecture/ExtractionManifest.json). + +## Prospective 1.4 command-line tool + +This feature train adds `swift-json` beside the five libraries. Its compiling +foundation provides strict formatting, validation, Pointer selection, staged +Patch and qualified HTML/text/Markdown rendering. See the [CLI plan](Documentation/Architecture/Release1.4/Plan.md) +and [foundation contract](Documentation/Architecture/Release1.4/Foundation.md) +for command semantics, cumulative budgets and qualification boundaries. Query and +provider commands are advertised only after their separate gates pass. + +```sh +swift build -c release --product swift-json +.build/release/swift-json format --compact input.json +.build/release/swift-json validate --schema schema.json input.json +.build/release/swift-json render input.json --output inspection.html +``` diff --git a/Scripts/ci.py b/Scripts/ci.py index 6ed3220..5108e93 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -25,7 +25,7 @@ def commands(lane, output): # Root behavior/corpora remain required. SwiftData and Apple SDK/lifetime # probes are separate Apple acceptance; they never become Linux passes. return [c for c in commands('native', ROOT / output) - if not any(x in c for x in ['Tools/MobileQualification', '--mode'])] + [ + if not any(x in c for x in ['Tools/MobileQualification', '--mode', 'Scripts/qualify_cli_ownership.py'])] + [ ['python3', '-B', 'Scripts/qualify_linux_reproduction.py', '--output', str(output / 'reproduction')]] if lane == 'linux-safety': # Every behavior case uses Swift Testing. Do not start the generated @@ -53,6 +53,8 @@ def commands(lane, output): ['swift', 'run', '-c', 'release', '--package-path', 'Tools/JSONHTMLBrowser', 'BrowserFixtures', str(output / 'fixtures')], ['python3', '-B', 'Scripts/qualify_html_browser.py', '--fixtures', str(output / 'fixtures'), '--output', str(output / 'browser')], ] + if lane == 'cli': + return [['swift', 'build', '-c', 'release', '--product', 'swift-json'], ['swift', 'test', '--filter', 'cli'], ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')]] if lane == 'native': return [ ['python3', '-B', 'Scripts/public_check.py'], @@ -61,6 +63,8 @@ def commands(lane, output): ['swift', 'build', '-c', 'release'], ['python3', '-B', 'Scripts/verify_testing_events.py'], ['swift', 'test'], ['swift', 'test', '-c', 'release'], + ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')], + ['python3', '-B', 'Scripts/qualify_cli_ownership.py', '--output', str(output / 'cli-ownership')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'ownership', '--output', str(output / 'ownership.json')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'reader', '--output', str(output / 'reader.json')], ['swift', 'run', '-c', 'release', '--package-path', 'Examples/Consumer', 'Consumer'], @@ -112,10 +116,12 @@ def check_result(command, code, log, events=None): if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 3 if sys.platform == 'linux' else 4, 4] if 'thread' in command else sorted([11, 12, 14, 61 if sys.platform == 'linux' else 75, 51, 107]) + expected = [12] if 'cli' in command else [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 3 if sys.platform == 'linux' else 4, 4] if 'thread' in command else sorted([11, 12, 12, 14, 61 if sys.platform == 'linux' else 75, 51, 107]) passed = passed and sorted(counts) == expected if '--filter' not in command and not any(x.startswith('Tools/') for x in command): - 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)) == 173) + if events is not None: + passed = passed and events['module_function_counts'] == {'JSONTests':107,'JSONSchemaTests':51,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONRegexRuntimeTests':12,'JSONHTMLTests':61 if sys.platform == 'linux' else 75,'JSONCLITests':12} + 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)) == 177) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) matrix = json.loads((ROOT / 'Documentation/Qualification/LimitsMatrix.json').read_text()) @@ -123,10 +129,10 @@ def check_result(command, code, log, events=None): passed = passed and len(probes) == 49 and dict(probes) == expected_probes if command == ['swift', 'package', 'dump-package'] and passed: d = json.loads(log) - passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] - passed = passed and {p['name']: p['targets'] for p in d['products']} == {name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML']} + passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming', 'swift-json'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] + passed = passed and {p['name']: p['targets'] for p in d['products']} == dict({name: [name] for name in ['JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML']}, **{'swift-json': ['JSONCLI']}) targets = {t['name']: t for t in d['targets']} - passed = passed and set(targets) == {'JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] + passed = passed and set(targets) == {'JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' if command == ['swift', 'package', 'show-dependencies', '--format', 'json'] and passed: passed = json.loads(log)['dependencies'] == [] if command[:2] == ['swift', 'run'] and 'Tools/MobileQualification' in command: @@ -143,7 +149,7 @@ def check_result(command, code, log, events=None): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export'], required=True) + parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) diff --git a/Scripts/qualify_cli.py b/Scripts/qualify_cli.py new file mode 100644 index 0000000..8ce7f81 --- /dev/null +++ b/Scripts/qualify_cli.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Exercise the real CLI subprocess, exact bytes, exits, stderr and atomic files.""" +import argparse, hashlib, json, os, pathlib, signal, subprocess, tempfile, time + +ROOT = pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser = argparse.ArgumentParser(); parser.add_argument('--binary', required=True); parser.add_argument('--output', required=True) + args = parser.parse_args(); binary = (ROOT / args.binary).resolve(); rows = [] + def run(name, argv, data=b'', exit=0, expected=None, check=None): + command = [str(binary), *argv]; process = subprocess.run(command, input=data, capture_output=True, timeout=30) + ok = process.returncode == exit and (expected is None or process.stdout == expected) + if check: ok = ok and check(process) + rows.append({'id': name, 'command': ['swift-json', *[a if isinstance(a,str) else {'bytes_hex':a.hex()} for a in argv]], 'exit_code': process.returncode, 'expected_exit': exit, + 'stdout_sha256': hashlib.sha256(process.stdout).hexdigest(), 'stdout_bytes': len(process.stdout), + 'stderr_sha256': hashlib.sha256(process.stderr).hexdigest(), 'stderr_bytes': len(process.stderr), 'status': 'passed' if ok else 'failed'}) + if not ok: print(name, process.returncode, repr(process.stdout[:250]), repr(process.stderr[:500]), flush=True) + return process + with tempfile.TemporaryDirectory(prefix='swift-json-cli-') as temp: + temp = pathlib.Path(temp) + def file(name, data): + path = temp / name; path.write_bytes(data); return str(path) + def limit(**caps): return file('limits.json', json.dumps({'version':1,'cli':caps}).encode()) + run('help', ['--help'], check=lambda p: b'format' in p.stdout and not p.stderr) + run('invalid-argv-utf8', ['format', b'\xff'], exit=2, expected=b'', check=lambda p: b'invalidArgvUTF8' in p.stderr) + run('version', ['--version'], expected=b'swift-json 1.4.0-dev\n') + run('version-json', ['--version','--json'], check=lambda p: json.loads(p.stdout)['revision'] == 'unknown') + run('missing-command', [], exit=2, expected=b'') + run('unknown-option', ['format','--unknown'], exit=2, expected=b'') + run('usage-json-diagnostic', ['format','--diagnostics=json','--unknown'], exit=2, expected=b'', check=lambda p: json.loads(p.stderr)['kind'] == 'diagnostic' and json.loads(p.stderr)['category'] == 'usage') + run('duplicate-option', ['format','--compact','--compact'], exit=2, expected=b'') + run('indent-conflict', ['format','--compact','--tab'], exit=2, expected=b'') + run('preserved-numbers', ['format','--compact'], b'[9007199254740993,0.10,-0,1e1000]', expected=b'[9007199254740993,0.10,-0,1e1000]\n') + run('normalized-numbers', ['format','--compact','--number-style=normalized'], b'[0.10,-0]', expected=b'[1e-1,0]\n') + run('tab-indentation', ['format','--tab'], b'{"a":[1]}', expected=b'{\n\t"a": [\n\t\t1\n\t]\n}\n') + scalar = '{"é":1,"é":2}'.encode() + run('scalar-distinct-keys', ['format','-cS'], scalar, expected='{"é":2,"é":1}\n'.encode()) + run('duplicate-decoded-key', ['format','--compact'], b'{"a":1,"\\u0061":2}', exit=5, expected=b'') + run('malformed-utf8', ['format'], b'"\xff"', exit=5, expected=b'') + run('trailing-root', ['format'], b'{} []', exit=5, expected=b'') + run('validation-syntax', ['validate'], b'{}', check=lambda p: json.loads(p.stdout) == {'valid':True,'issues':[],'truncated':False}) + run('validation-malformed', ['validate'], b'{', exit=5, expected=b'') + schema = file('schema.json', b'{"type":"integer","minimum":3}') + run('schema-valid', ['validate','--schema',schema], b'4', check=lambda p: json.loads(p.stdout)['valid'] is True) + run('schema-invalid', ['validate','--schema',schema], b'2', exit=1, check=lambda p: json.loads(p.stdout)['valid'] is False and len(json.loads(p.stdout)['issues']) > 0) + run('schema-bounded-issues', ['validate','--schema',schema,'--max-issues','0'], b'2', exit=1, check=lambda p: json.loads(p.stdout) == {'valid':False,'issues':[],'truncated':True}) + run('pointer-null', ['pointer','/a'], b'{"a":null}', expected=b'null\n') + run('pointer-missing', ['pointer','/a'], b'{}', exit=5, expected=b'') + run('pointer-allow-missing', ['pointer','/a','--allow-missing'], b'{}', exit=4, expected=b'') + run('pointer-array-absence', ['pointer','/0','--allow-missing'], b'[]', exit=4, expected=b'') + run('pointer-malformed-index-stays-error', ['pointer','/01','--allow-missing'], b'[]', exit=5, expected=b'') + run('pointer-wrong-type', ['pointer','/a','--allow-missing'], b'[]', exit=5, expected=b'') + run('pointer-escaped-key', ['pointer','/a~1b~0c'], b'{"a/b~c":1}', expected=b'1\n') + patch = file('patch.json', b'[{"op":"remove","path":""}]') + run('patch-root-absence', ['patch','--patch',patch], b'null', expected=b'') + patch = file('patch.json', b'[{"op":"add","path":"/b","value":9007199254740993}]') + run('patch-exact-value', ['patch','--patch',patch], b'{"a":1}', check=lambda p: b'9007199254740993' in p.stdout) + patch = file('patch.json', b'[{"op":"add","path":"/b","value":2},{"op":"test","path":"/a","value":0}]') + run('patch-failure-no-candidate', ['patch','--patch',patch], b'{"a":1}', exit=5, expected=b'') + run('html-invalid-title-is-usage', ['render','--title','\x01'], b'{}', exit=2, expected=b'') + run('static-html', ['render'], b'{"x":"\",\"n\":9007199254740993}\n", + "expected_exit": 0, + "comparison": "process-contract", + "status": "not_run", + "probe": { + "assert_no_script_element": true, + "assert_exact_value": true, + "assert_no_external_assets": true + } + }, + { + "id": "render-markdown", + "gate": "C5", + "requirement_ids": [ + "CL077" + ], + "command": "render", + "options": [ + "--format", + "markdown" + ], + "operands": [], + "stdin_utf8": "{\"a\":[1.2300e+2]}\n", + "expected_exit": 0, + "comparison": "stdout-byte-exact", + "status": "not_run", + "expected_stdout_utf8": "```json\n{\n\t\"a\": [\n\t\t1.2300e+2\n\t]\n}\n```\n" + } + ] +} diff --git a/Tests/JSONCLITests/QueryBudgetTests.swift b/Tests/JSONCLITests/QueryBudgetTests.swift new file mode 100644 index 0000000..e7e6e2f --- /dev/null +++ b/Tests/JSONCLITests/QueryBudgetTests.swift @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +@testable import JSONFilterRuntime + +@Test(arguments: CLIResourceMetric.allCases) +func cliEveryCounterHasInclusiveCheckedBoundaryAndTerminalFailure(metric: CLIResourceMetric) throws { + var limits = CLILimits(); limits[metric] = 3 + var context = try CLIJobContext(limits: limits) + try context.charge(metric, 3) + #expect(context.used(metric) == 3) + do { try context.charge(metric); Issue.record("Expected cap+1 rejection") } + catch let error as CLIError { #expect(error.category == .resource && error.metric == metric.rawValue) } + #expect(throws: CLIError.self) { try context.checkpoint() } +} +@Test(arguments: [CLIResourceMetric.querySourceBytes, .queryTokens, .astNodes, .queryDepth]) +func cliCompilerEnforcesCapsAtActualSourceTokenAndIRGrowth(metric: CLIResourceMetric) throws { + var limits = CLILimits(); limits[metric] = 1 + var allowed = try CLIJobContext(limits: limits) + let program = try FilterCompiler.compile(".", context: &allowed) + #expect(program.nodes.count == 1) + limits[metric] = 0 + var blocked = try CLIJobContext(limits: limits) + do { _ = try FilterCompiler.compile(".", context: &blocked); Issue.record("Expected actual compiler admission failure") } + catch let failure as CLIError { #expect(failure.category == .resource && failure.metric == metric.rawValue) } +} +@Test func cliParserDepthUsesConfiguredWorkStackAndRejectsWithoutRecursion() throws { + var limits = CLILimits(); limits[.queryDepth] = 64 + var context = try CLIJobContext(limits: limits) + let source = String(repeating: "(", count: 65) + "." + String(repeating: ")", count: 65) + do { _ = try FilterCompiler.compile(source, context: &context); Issue.record("Expected depth cap") } + catch let failure as CLIError { #expect(failure.metric == "queryDepth") } +} +@Test func cliRuntimeFramesAndBindingsAreLiveOwnedCaps() throws { + var limits = CLILimits(); limits[.runtimeFrames] = 0 + var context = try CLIJobContext(limits: limits) + let program = try FilterCompiler.compile(".", context: &context) + do { _ = try FilterVM(program: program, input: .init(value: .null), context: &context); Issue.record("Expected frame cap before growth") } + catch let failure as CLIError { #expect(failure.metric == "runtimeFrames") } +} +@Test func cliQueryPlanIsImmutableAndCheckedSendable() throws { + func requireSendable(_ value: T) {} + var context = try CLIJobContext() + let program = try FilterCompiler.compile(".a, .b", context: &context) + requireSendable(program) + #expect(program.nodes.count > 1 && program.sourceRanges.count == program.nodes.count) +} + +@Test func cliQueryOperationalFailureBypassesTryAndCleansInvocationStorage() throws { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile("try [1,2,3] catch null", context: &compilation) + var limits = CLILimits(); limits[.collectionValues] = 2 + var context = try CLIJobContext(limits: limits) + let baseline = context.meter.used(.retainedBytes) + var vm = try FilterVM(program: program, input: .init(value: .null), context: &context) + struct NoInput: FilterInput { mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } } + var input = NoInput() + do { _ = try vm.next(input: &input, context: &context); Issue.record("Resource failure must not become catch null") } + catch let failure as CLIError { #expect(failure.category == .resource && failure.metric == "collectionValues") } + try vm.close(context: &context) + #expect(context.used(.runtimeFrames) == 0 && context.used(.collectionValues) == 0) + #expect(context.meter.used(.retainedBytes) == baseline) + #expect(throws: CLIError.self) { try context.checkpoint() } +} +@Test func cliQueryCancellationHappensInsideOwnedEvaluationAndCleansFrames() async throws { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile(".[]", context: &compilation) + let task = Task { + var context = try CLIJobContext() + let baseline = context.meter.used(.retainedBytes) + var value = try FilterValues.produced(.array((0..<100).map { .number(JSONNumber(integer: $0)) }), context: &context) + var vm = try FilterVM(program: program, input: value, context: &context) + try value.release(context: &context) + struct NoInput: FilterInput { mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } } + var input = NoInput() + var pending = try vm.next(input: &input, context: &context) + var first = try #require(pending); pending = nil + try first.release(context: &context) + context.meter.checkpointObserver = { _ in withUnsafeCurrentTask { $0?.cancel() } } + #expect(throws: CancellationError.self) { _ = try vm.next(input: &input, context: &context) } + try vm.close(context: &context) + #expect(context.used(.runtimeFrames) == 0 && context.used(.collectionValues) == 0) + #expect(context.meter.used(.retainedBytes) == baseline) + } + try await task.value +} + +@Test func cliQueryConstructorsAndSelectorsReleaseScratchAfterMidOperationFailure() throws { + let object = try JSONParser().parse(Array("{\"a\":1,\"b\":2,\"c\":3,\"d\":4,\"e\":5,\"f\":6}".utf8)) + let array = JSON.array((0..<20).map { .number(JSONNumber(integer: $0)) }) + struct NoInput: FilterInput { mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } } + for (source, value) in [("keys", object), (".[1:15]", array), ("map_values(.)", object), ("{a: .a,b: .b}", object), ("try error(.) catch .", object)] { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile(source, context: &compilation) + var rejected = 0 + for cap: UInt64 in [360, 720, 1440, 2880] { + var limits = CLILimits(); limits[.retainedBytes] = cap + var context = try CLIJobContext(limits: limits) + var vm = try FilterVM(program: program, input: .init(value: value), context: &context) + var input = NoInput() + do { while var result = try vm.next(input: &input, context: &context) { try result.release(context: &context) } } + catch { rejected += 1 } + try vm.close(context: &context) + #expect(context.used(.runtimeFrames) == 0 && context.used(.collectionValues) == 0) + #expect(context.meter.used(.retainedBytes) == 0, "\(source) cap=\(cap)") + } + #expect(rejected > 0) + } +} diff --git a/Tests/JSONCLITests/QueryCoreTests.swift b/Tests/JSONCLITests/QueryCoreTests.swift new file mode 100644 index 0000000..ef22c05 --- /dev/null +++ b/Tests/JSONCLITests/QueryCoreTests.swift @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +@testable import JSONFilterRuntime + +private struct EmptyFilterInput: FilterInput { + mutating func next(context: inout CLIJobContext) throws -> FilterYield? { nil } +} +private func evaluateFilter(_ source: String, input: String = "null") throws -> [JSON] { + var context = try CLIJobContext() + let program = try FilterCompiler.compile(source, context: &context) + let entry = context.meter.used(.retainedBytes) + let value = try JSONParser().parse(Array(input.utf8), meter: &context.meter) + let owned = context.meter.used(.retainedBytes) - entry + let lease = try CLIValueLease(ownedRetained: owned, context: &context) + var vm = try FilterVM(program: program, input: .init(value: value, lease: lease), context: &context) + defer { try? vm.close(context: &context) } + var input = EmptyFilterInput(), values: [JSON] = [] + while var result = try vm.next(input: &input, context: &context) { + values.append(result.value) + try result.release(context: &context) + } + return values +} +@Test func cliQueryCommaAndPipeAreOrderedPullStreams() throws { + #expect(try evaluateFilter("1,2 | .") == [.number(1), .number(2)]) + #expect(try evaluateFilter(".[] | .", input: "[1,2,3]") == [.number(1), .number(2), .number(3)]) + #expect(try evaluateFilter("[1,2,3]") == [.array([.number(1), .number(2), .number(3)])]) +} +@Test func cliQueryDecodedScalarKeysAndPresenceStayExact() throws { + let input = "{\"é\":1,\"é\":2,\"null\":null}" + #expect(try evaluateFilter(".[\"é\"], .[\"é\"]", input: input) == [.number(1), .number(2)]) + #expect(try evaluateFilter("has(\"null\"),has(\"missing\")", input: input) == [.boolean(true), .boolean(false)]) + #expect(try evaluateFilter(".missing?", input: input) == [.null]) +} +@Test func cliQueryWrongTypesAreCatchableButEmptyIsNotNull() throws { + #expect(try evaluateFilter(".[]?", input: "null").isEmpty) + #expect(try evaluateFilter("try .[] catch .", input: "null") == [.string("cannotIterate")]) + #expect(try evaluateFilter("empty // 9") == [.number(9)]) + #expect(try evaluateFilter("(false,null,0) // 9") == [.number(0)]) +} +@Test func cliQueryLogicalShortCircuitAndBranchesUseOriginalInput() throws { + #expect(try evaluateFilter("false and error(\"unreachable\")") == [.boolean(false)]) + #expect(try evaluateFilter("true or error(\"unreachable\")") == [.boolean(true)]) + #expect(try evaluateFilter("if (true,false) then . else null end", input: "7") == [.number(7), .null]) + #expect(try evaluateFilter("if . == 0 then 0 elif . == 1 then 2 else 3 end", input: "1") == [.number(2)]) +} +@Test func cliQueryMapsKeepAllResultsAndFirstUpdatePolicy() throws { + #expect(try evaluateFilter("map((.,.))", input: "[1,2]") == [.array([.number(1), .number(1), .number(2), .number(2)])]) + #expect(try evaluateFilter("map_values((.,9))", input: "[1,2]") == [.array([.number(1), .number(2)])]) + #expect(try evaluateFilter("map_values(empty)", input: "[1,2]") == [.array([])]) +} +@Test func cliQueryUnicodeLengthCountsScalarsAndRecursiveNullLeaves() throws { + #expect(try evaluateFilter("length,utf8bytelength", input: "\"é🦊\"") == [.number(3), .number(7)]) + #expect(try evaluateFilter("..", input: "[null,1]") == [.array([.null, .number(1)]), .null, .number(1)]) +} +@Test func cliQueryAliasesKeepSourceChargedUntilTheirLastRelease() throws { + var context = try CLIJobContext() + let baseline = context.meter.used(.retainedBytes) + var value = try FilterValues.produced(.array([.string(String(repeating: "x", count: 1000))]), context: &context) + var alias = value.selecting(value.value.arrayValue![0]) + try value.release(context: &context) + #expect(context.meter.used(.retainedBytes) > baseline) + try alias.release(context: &context) + #expect(context.meter.used(.retainedBytes) == baseline) +} +@Test func cliQueryTerminalQuotaStillReleasesValueStorage() throws { + var limits = CLILimits(); limits[.totalWorkUnits] = 2000 + var context = try CLIJobContext(limits: limits) + let baseline = context.meter.used(.retainedBytes) + var value = try FilterValues.produced(.string("owned"), context: &context) + do { try context.work(2000); Issue.record("Expected terminal work failure") } catch let failure as CLIError { #expect(failure.category == .resource) } + try value.release(context: &context) + #expect(context.meter.used(.retainedBytes) == baseline) + #expect(throws: CLIError.self) { try context.checkpoint() } +} +@Test func cliQueryObjectsUseCartesianOrderAndExactReplacement() throws { + let values = try evaluateFilter("{a:(1,2),b:(3,4)}") + let expected = try ["{\"a\":1,\"b\":3}","{\"a\":1,\"b\":4}","{\"a\":2,\"b\":3}","{\"a\":2,\"b\":4}"].map { try JSONParser().parse($0) } + #expect(values == expected) + let replacement = try evaluateFilter("{a:1,b:2,a:3}") + #expect(replacement[0].objectValue!.members.map(\.key.string) == ["a","b"]) + #expect(replacement[0].objectValue![JSONKey("a")] == .number(3)) + #expect(try evaluateFilter("{\"é\":1,\"é\":2}")[0].objectValue!.count == 2) +} +@Test func cliQuerySlicesUseScalarBoundariesAndCheckedNegativeIndices() throws { + #expect(try evaluateFilter(".[1:3]", input: "\"é🦊x\"") == [.string("́🦊")]) + #expect(try evaluateFilter(".[-2:]", input: "[1,2,3]") == [.array([.number(2),.number(3)])]) + #expect(try evaluateFilter(".[-1],.[1e1000]", input: "[1,2,3]") == [.number(3),.null]) +} +@Test func sharedFilterPlanHasIndependentConcurrentInvocations() async throws { + var compilation = try CLIJobContext() + let program = try FilterCompiler.compile(".a, .b", context: &compilation) + let outputs = try await withThrowingTaskGroup(of: [JSON].self, returning: [[JSON]].self) { group in + for _ in 0..<8 { + group.addTask { + var context = try CLIJobContext() + let input = try JSONParser().parse("{\"a\":1,\"b\":2}") + var value = try FilterValues.produced(input, context: &context) + var vm = try FilterVM(program: program, input: value, context: &context), source = EmptyFilterInput() + defer { try? vm.close(context: &context) } + try value.release(context: &context) + var results: [JSON] = [] + while var result = try vm.next(input: &source, context: &context) { results.append(result.value); try result.release(context: &context) } + #expect(context.used(.totalWorkUnits) > 0) + #expect(context.meter.used(.retainedBytes) == 0) + return results + } + } + var values: [[JSON]] = [] + for try await value in group { values.append(value) } + return values + } + #expect(outputs.count == 8 && outputs.allSatisfy { $0 == [.number(1), .number(2)] }) +} diff --git a/Tests/JSONCLITests/QueryInputTests.swift b/Tests/JSONCLITests/QueryInputTests.swift new file mode 100644 index 0000000..ab77db2 --- /dev/null +++ b/Tests/JSONCLITests/QueryInputTests.swift @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +@testable import JSONCLIEngine +@testable import JSONFilterRuntime + +private func withInputFile(_ bytes: [UInt8], _ body: (String) throws -> T) throws -> T { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("json-query-" + UUID().uuidString) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + defer { try? FileManager.default.removeItem(at: root) } + let path = root.appendingPathComponent("input.json") + try Data(bytes).write(to: path) + return try body(path.path) +} +@Test func cliQueryValueFramingRequiresWhitespaceAndIndependentFileEOF() throws { + try withInputFile(Array("1 2\n{}".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path,path]) + defer { try? cursor.close(context: &context) } + var values: [JSON] = [] + while var value = try cursor.next(context: &context) { values.append(value.value); try value.release(context: &context) } + #expect(values.count == 6 && values[0] == .number(1) && values[3] == .number(1)) + } + try withInputFile(Array("{}[]".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path]) + defer { try? cursor.close(context: &context) } + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + } +} +@Test func cliQueryDocumentWaitsForEOFAndTailErrorsAreTerminal() throws { + try withInputFile(Array("1 2".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .document) + defer { try? cursor.close(context: &context) } + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + #expect(throws: CLIError.self) { _ = try cursor.next(context: &context) } + } + try withInputFile(Array("1\n{\"bad\":]".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path]) + defer { try? cursor.close(context: &context) } + let maybeFirst = try cursor.next(context: &context); var first = try #require(maybeFirst); #expect(first.value == .number(1)); try first.release(context: &context) + #expect(throws: JSONFailure.self) { try cursor.drain(context: &context) } + } +} +@Test func cliQueryStrictNDJSONAndRawLinesPreserveTheirDifferentContracts() throws { + try withInputFile(Array("1\n\n2\n".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .ndjson) + defer { try? cursor.close(context: &context) } + let maybeFirst = try cursor.next(context: &context); var first = try #require(maybeFirst); #expect(first.value == .number(1)); try first.release(context: &context) + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + } + try withInputFile(Array("a\r\n\n🦊".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .rawLines) + defer { try? cursor.close(context: &context) } + var values: [JSON] = [] + while var value = try cursor.next(context: &context) { values.append(value.value); try value.release(context: &context) } + #expect(values == [.string("a"), .string(""), .string("🦊")]) + } +} +@Test func cliQueryRootArrayYieldsProvisionallyAndRequiresClosingEOF() throws { + try withInputFile(Array("[1,2".utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path], framing: .array) + defer { try? cursor.close(context: &context) } + let maybeFirst = try cursor.next(context: &context); var first = try #require(maybeFirst); #expect(first.value == .number(1)); try first.release(context: &context) + let maybeSecond = try cursor.next(context: &context); var second = try #require(maybeSecond); #expect(second.value == .number(2)); try second.release(context: &context) + #expect(throws: JSONFailure.self) { _ = try cursor.next(context: &context) } + } +} +@Test(arguments: [2000, 20000]) func cliQueryManyTinyRecordsDoNotRetainTheWholeInputJob(records: Int) throws { + try withInputFile(Array(String(repeating: "0\n", count: records).utf8)) { path in + var context = try CLIJobContext(), cursor = CLIInputCursor(files: [path]) + defer { try? cursor.close(context: &context) } + var count = 0 + while var value = try cursor.next(context: &context) { count += 1; try value.release(context: &context) } + #expect(count == records) + #expect(context.meter.used(.retainedBytes) == 0) + // One 16 KiB syscall buffer may overlap one accepted 16 KiB chunk; + // the completed scalar/lease has a small fixed reservation. This bound + // follows the actual adapter, rather than an arbitrary RSS threshold. + let chunk = min(UInt64(16384), context.limits.core[.chunkBytes]) + #expect(context.meter.statistics().peakLogicalRetainedBytes <= 2 * chunk + 256) + #expect(context.used(.totalRecords) == UInt64(records) && context.used(.totalWorkUnits) > UInt64(records)) + } +} diff --git a/Tools/CLIQualification/Package.swift b/Tools/CLIQualification/Package.swift new file mode 100644 index 0000000..7d084fa --- /dev/null +++ b/Tools/CLIQualification/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version: 6.4 +// SPDX-License-Identifier: Apache-2.0 +import PackageDescription +let package = Package(name: "CLIQualification", platforms: [.macOS("27.0")], + products: [.executable(name: "CLIExactCompare", targets: ["CLIExactCompare"])], + dependencies: [.package(name: "Swift-JSON", path: "../..")], + targets: [.executableTarget(name: "CLIExactCompare", dependencies: [ + .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONStreaming", package: "Swift-JSON")])], + swiftLanguageModes: [.v6]) diff --git a/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift new file mode 100644 index 0000000..8b656bb --- /dev/null +++ b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONStreaming +#if canImport(Darwin) +import Darwin +#else +import Glibc +#endif + +private struct Bytes: AsyncSequence { + typealias Element = JSONByteChunk + let bytes: [UInt8] + struct Iterator: AsyncIteratorProtocol { + let bytes: [UInt8] + var index = 0 + mutating func next() async -> JSONByteChunk? { + guard index < bytes.count else { return nil } + let end = Swift.min(index + 16384, bytes.count) + defer { index = end } + return .init(Array(bytes[index.. Iterator { .init(bytes: bytes) } +} +@main struct CLIExactCompare { + static func main() async { + do { + let arguments = CommandLine.arguments + guard arguments.count == 3 else { throw NSError(domain: "qualification", code: 2) } + let actual = try bounded(arguments[1]), expected = try bounded(arguments[2]) + let declaration = try JSONParser().parse(expected) + guard case .array(let texts) = declaration else { throw NSError(domain: "qualification", code: 2) } + // CLI result streams may be empty. The library's value sequence + // requires a record; verify the zero-result contract explicitly. + if texts.isEmpty { + guard actual.allSatisfy({ [9,10,13,32].contains($0) }) else { throw NSError(domain: "qualification", code: 1) } + print("NATIVE_EXACT_COMPARE passed values=0"); return + } + var iterator = JSONValueSequence(Bytes(bytes: actual), options: .init(framing: .whitespaceSeparatedValues)).makeAsyncIterator() + var count = 0 + for text in texts { + guard case .string(let raw) = text, let actual = try await iterator.next() else { throw NSError(domain: "qualification", code: 1) } + let expected = try JSONParser().parse(raw) + guard try JSONComparison.equals(actual, expected) else { throw NSError(domain: "qualification", code: 1) } + count += 1 + } + guard try await iterator.next() == nil else { throw NSError(domain: "qualification", code: 1) } + print("NATIVE_EXACT_COMPARE passed values=\(count)") + } catch { fputs("NATIVE_EXACT_COMPARE failed\n", stderr); exit(1) } + } + private static func bounded(_ path: String) throws -> [UInt8] { + let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path)) + defer { try? handle.close() } + var bytes: [UInt8] = [] + while let data = try handle.read(upToCount: 16384), !data.isEmpty { + guard bytes.count + data.count <= 33554432 else { throw NSError(domain: "qualification", code: 2) } + bytes.append(contentsOf: data) + } + return bytes + } +} From 1e82158de754888eda1b22566c1e7260470eeaed Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:09:05 +0200 Subject: [PATCH 37/59] fix(qualification): avoid mutable Glibc stderr in exact consumer --- Documentation/Architecture/Release1.4/Qualification.md | 10 ++++++++++ .../Sources/CLIExactCompare/Main.swift | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Documentation/Architecture/Release1.4/Qualification.md b/Documentation/Architecture/Release1.4/Qualification.md index ededc33..b3d4622 100644 --- a/Documentation/Architecture/Release1.4/Qualification.md +++ b/Documentation/Architecture/Release1.4/Qualification.md @@ -79,3 +79,13 @@ source remains identical; final C5 qualification will use the integration head. The inherited 168 corpus rows and auxiliary binding modes still need their complete mode-specific dispositions. Arithmetic and advanced filters have separate gates. No simulator/device, Linux or WASI CLI success is inferred from this local run. + + +Hosted C2 Linux runs on x86_64/ARM64 passed 256 root functions/199 parameter cases, +ASan/TSan safety and the 53 CLI subprocess cases, then the comparator failed to +compile: Glibc's mutable `stderr` global is not concurrency-safe in async main. +The qualification tool now writes bounded failure text through +FileHandle.standardError. No unsafe annotation/flag or library behavior changed. +Failed run 38593758299 and its actual compiler log are retained; a fresh hosted +run is required before Linux closure. Local comparator rebuild and all 38 C2 +vectors verify the diagnostic-only fix on the supported Mac. diff --git a/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift index 8b656bb..0a9ae2f 100644 --- a/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift +++ b/Tools/CLIQualification/Sources/CLIExactCompare/Main.swift @@ -47,7 +47,7 @@ private struct Bytes: AsyncSequence { } guard try await iterator.next() == nil else { throw NSError(domain: "qualification", code: 1) } print("NATIVE_EXACT_COMPARE passed values=\(count)") - } catch { fputs("NATIVE_EXACT_COMPARE failed\n", stderr); exit(1) } + } catch { FileHandle.standardError.write(Data("NATIVE_EXACT_COMPARE failed\n".utf8)); exit(1) } } private static func bounded(_ path: String) throws -> [UInt8] { let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path)) From bcb2a9735bb6de67c9096f44d7494d3d5b73fc8a Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:03:23 +0200 Subject: [PATCH 38/59] feat(cli): add bounded exact decimal arithmetic and pull range --- .../Architecture/ExtractionManifest.json | 5 +- .../Architecture/Release1.4/Acceptance.json | 76 +- .../Architecture/Release1.4/Arithmetic.md | 59 + .../Architecture/Release1.4/Progress.json | 34 +- .../Architecture/Release1.4/Qualification.md | 32 + README.md | 7 +- Scripts/ci.py | 20 +- Scripts/generate_arithmetic_oracle.py | 58 + Scripts/qualify_cli_cases.py | 12 +- Scripts/qualify_cli_limits.py | 42 + .../JSON/Documentation.docc/CommandLine.md | 25 +- Sources/JSONCLIEngine/CLIDiagnostics.swift | 24 +- Sources/JSONCLIEngine/CLIEngine.swift | 5 +- Sources/JSONCLIEngine/CLIQueryCommand.swift | 11 +- Sources/JSONFilterRuntime/CLIJobContext.swift | 19 +- .../JSONFilterRuntime/DecimalMagnitude.swift | 160 + Sources/JSONFilterRuntime/ExactDecimal.swift | 184 + .../JSONFilterRuntime/FilterCompiler.swift | 22 +- .../JSONFilterRuntime/FilterOperators.swift | 134 + Sources/JSONFilterRuntime/FilterVM.swift | 57 +- Sources/JSONFilterRuntime/FilterValues.swift | 6 + Sources/JSONFilterRuntime/RangeState.swift | 28 + Tests/Fixtures/CLI/ArithmeticOracle.json | 5858 +++++++++++++++++ Tests/JSONCLITests/ArithmeticTests.swift | 147 + 24 files changed, 6964 insertions(+), 61 deletions(-) create mode 100644 Documentation/Architecture/Release1.4/Arithmetic.md create mode 100644 Scripts/generate_arithmetic_oracle.py create mode 100644 Scripts/qualify_cli_limits.py create mode 100644 Sources/JSONFilterRuntime/DecimalMagnitude.swift create mode 100644 Sources/JSONFilterRuntime/ExactDecimal.swift create mode 100644 Sources/JSONFilterRuntime/FilterOperators.swift create mode 100644 Sources/JSONFilterRuntime/RangeState.swift create mode 100644 Tests/Fixtures/CLI/ArithmeticOracle.json create mode 100644 Tests/JSONCLITests/ArithmeticTests.swift diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index 4d9844a..8047a0b 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -209,5 +209,8 @@ "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_notes": [ + "1.4 C3 decimal magnitude, rational/quantization kernel, pull range and type-directed overloads are original Apache-2.0 code. The 738-case deterministic oracle uses Python integer/Fraction arithmetic; no jq/Rhoe arithmetic implementation was copied." + ] } diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json index d61ac68..e9dcb45 100644 --- a/Documentation/Architecture/Release1.4/Acceptance.json +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -1,7 +1,7 @@ { "schema": "swift-json-cli-acceptance/1", "prepared": "2026-10-14", - "implementation_status": "Foundation and pull query-core milestones; complete C0-C5 integration remains unqualified", + "implementation_status": "Foundation, query core and exact arithmetic milestones; C4/C5 integration remains unqualified", "requirements": [ { "id": "CL001", @@ -666,7 +666,7 @@ "title": "Exact add/subtract/multiply", "verification": "Carry/borrow/sign/zero/large integer cases against independent exact expectations.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "Cartesian-add-order", @@ -674,6 +674,10 @@ "exact-decimal-add", "exact-borrow", "exact-multiply" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -682,9 +686,13 @@ "title": "Numeric admission and lexemes", "verification": "Computed values normalized with no false originalLexeme; untouched inputs preserve spelling.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL047", @@ -692,10 +700,14 @@ "title": "Terminating exact division", "verification": "Reduced denominators containing only2/5, large common factors and mixed signs.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "terminating-division" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -704,10 +716,14 @@ "title": "Repeating division default", "verification": "1/3 and repeating values give language inexactDivision; no floating fallback.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "repeating-division-error" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -716,12 +732,16 @@ "title": "Explicit scale/half-even", "verification": "Required option pair, decimal places, finite caps, signed ties and report rounding counts.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "rounded-third", "half-even-ties", "scale-pair-required" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -730,11 +750,15 @@ "title": "Zero division and integral remainder", "verification": "Typed catchable domain errors; signed truncated remainder; fractional remainder rejected.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "negative-remainder", "fractional-remainder-rejected" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -743,12 +767,16 @@ "title": "Exact numeric builtins", "verification": "abs/floor/ceil/trunc/round/range/add with negative and empty/domain cases; round ties away from zero.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "round-away-from-zero", "range-negative-step", "range-zero-step" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -757,10 +785,14 @@ "title": "Exponent/growth preflight", "verification": "Large sparse exponent operations fail finite caps before dense allocation; exact zero shortcut allowed.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, "fixture_ids": [ "exponent-gap-cap" + ], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" ] }, { @@ -769,9 +801,13 @@ "title": "Numeric work and cancellation", "verification": "Charged GCD/division/carry inner loops; boundaries and checkpoint cancellation; terminal errors not catchable.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL054", @@ -779,9 +815,13 @@ "title": "32-bit arithmetic safety", "verification": "No Int/UInt64 negation/narrowing/product overflow traps; wasm32 kernel tests or explicit gate blocker.", "obligation": "mandatory", - "status": "not_run", + "status": "partial", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL055", @@ -789,9 +829,13 @@ "title": "No Decimal/Double authority", "verification": "Source audit plus precision cases prove no foreign arithmetic/fallback authority.", "obligation": "mandatory", - "status": "not_run", + "status": "passed", "evidence_required": true, - "fixture_ids": [] + "fixture_ids": [], + "evidence": [ + "Documentation/Architecture/Release1.4/Arithmetic.md", + "Documentation/Architecture/Release1.4/Qualification.md" + ] }, { "id": "CL056", diff --git a/Documentation/Architecture/Release1.4/Arithmetic.md b/Documentation/Architecture/Release1.4/Arithmetic.md new file mode 100644 index 0000000..2ec8f69 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Arithmetic.md @@ -0,0 +1,59 @@ +# Exact filter arithmetic + +C3 builds on query core 6e8f145 / draft PR #24. Original private code implements +base-ten unsigned working magnitudes plus native sign/coefficient/exponent values. +No Double, Float or Foundation Decimal is an arithmetic authority. Numeric binary +operators clear originalLexeme and normalize computed zero. Untouched selected +values preserve their admitted spelling; null's identity overload selects a value. + +Addition bounds exponent alignment before dense storage. Schoolbook multiplication +uses UInt16: digit product + stored digit + carry <=99; addition <=19. Subtraction +fits Int16. Long division maintains r
" : "", meter: &meter) - if popPath { path.removeLast(); try meter.release(JSONLogicalWeights.frame) } - case .node(let node, let component, let level): - if let component { try meter.retain(JSONLogicalWeights.frame); path.append(component) } - try output.addNode(meter: &meter) - let id = ordinal - ordinal = try JSONHTMLDocumentBuffer.adding(.renderedJSONNodes, ordinal, 1, options.limits.maxRenderedJSONNodes) - try output.addDOM(32, meter: &meter) - let container: Bool - switch node { case .array, .object: container = true; default: container = false } - if container, let depth = options.outlineDepth, level <= depth { - try output.addOutline(meter: &meter) - try meter.retain(40); outline.append(.init(ordinal: id, component: component, level: level)) - } - try output.append(container ? "
" : ">", meter: &meter) - try label(component, output: &output, meter: &meter, searchable: true) - switch node { - case .object(let members): - try output.append(" object (", meter: &meter) - try output.decimal(UInt64(members.count), meter: &meter) - try output.append(" members)", meter: &meter) - case .array(let values): - try output.append(" array (", meter: &meter) - try output.decimal(UInt64(values.count), meter: &meter) - try output.append(" items)", meter: &meter) - default: - try output.append(" ", meter: &meter) - try highlighted(node, options: writing, output: &output, meter: &meter) - try output.append("", meter: &meter) - } - try output.append(" ", meter: &meter) - for byte in quotedPointer { try output.escaped(byte, meter: &meter) } - try meter.release(UInt64(quotedPointer.count)); quotedPointer = [] - try output.append("", meter: &meter) - for (action, title) in [("copy-pointer", "Copy Pointer (RFC 6901)"), ("copy-pointer-escaped", "Copy Pointer as JSON string"), ("download-pointer", "Download Pointer (UTF-8)")] { - try meter.work() - try output.append("", meter: &meter) - } - try output.append("", meter: &meter) - try push(.close(container, component != nil), meter: &meter) - switch node { - case .object(let members): - try output.append("
", meter: &meter) - for member in members.reversed() { try meter.work(); try push(.node(member.value, .key(member.key), level + 1), meter: &meter) } - case .array(let values): - try output.append("", meter: &meter) - try annotationSection(annotations, output: &output, meter: &meter) - try output.append("
", meter: &meter)
-        try highlighted(value, options: writing, output: &output, meter: &meter)
-        try output.append("
\n", meter: &meter) - try meter.checkpoint() - return output - } - - private func nativeBytes(_ value: JSON, options: JSONWritingOptions, meter: inout JSONWorkMeter) throws -> [UInt8] { + package func nativeBytes(_ value: JSON, options: JSONWritingOptions, meter: inout JSONWorkMeter) throws -> [UInt8] { var cursor = JSONWritingCursor(value: value, options: options), bytes: [UInt8] = [] while let byte = try cursor.next(meter: &meter) { try meter.work(); try meter.charge(.outputBytes); try meter.retain(1); bytes.append(byte) } return bytes } - private func highlighted(_ value: JSON, options: JSONWritingOptions, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws { + package func highlighted(_ value: JSON, options: JSONWritingOptions, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws { var cursor = JSONWritingCursor(value: value, options: options) var active: JSONWritingTokenKind? while let token = try cursor.nextToken(meter: &meter) { @@ -174,7 +27,7 @@ extension JSONHTMLRenderer { } if let active, active != .whitespace { try output.append("", meter: &meter) } } - private func label(_ component: JSONLocationComponent?, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter, searchable: Bool) throws { + package func label(_ component: JSONLocationComponent?, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter, searchable: Bool) throws { switch component { case .key(let key): try output.append(searchable ? "" : "", meter: &meter) @@ -186,7 +39,7 @@ extension JSONHTMLRenderer { case nil: try output.append("Root", meter: &meter) } } - private func pointerBytes(_ path: [JSONLocationComponent], meter: inout JSONWorkMeter) throws -> [UInt8] { + package func pointerBytes(_ path: [JSONLocationComponent], meter: inout JSONWorkMeter) throws -> [UInt8] { try meter.require(.pointerTokens, UInt64(path.count)) var result: [UInt8] = [] func emit(_ byte: UInt8, meter: inout JSONWorkMeter) throws { @@ -211,16 +64,6 @@ extension JSONHTMLRenderer { } } -private enum InteractiveStep { - case node(JSON, JSONLocationComponent?, Int) - case close(Bool, Bool) -} -private struct OutlineEntry { - let ordinal: UInt64 - let component: JSONLocationComponent? - let level: Int -} - extension JSONHTMLDocumentBuffer { // Reserve the complete fixed decimal scratch before any owned formatting. package mutating func decimal(_ value: UInt64, meter: inout JSONWorkMeter) throws { diff --git a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift index 7b014ac..69ec520 100644 --- a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift +++ b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift @@ -5,6 +5,23 @@ package enum JSONHTMLInteractiveAssets { package static let css = #""" /* SPDX-License-Identifier: Apache-2.0; Swift-JSON interactive asset v1 */ .interactive .toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}.interactive button,.interactive input{font:inherit;padding:.35rem .55rem;border:1px solid currentColor;border-radius:.3rem;background:var(--bg);color:var(--fg)}.interactive :focus-visible{outline:3px solid var(--num);outline-offset:3px}.interactive .layout{display:grid;grid-template-columns:minmax(0,1fr) 18rem;gap:1.5rem}.interactive .layout:not(:has(.outline)){grid-template-columns:minmax(0,1fr)}.interactive .outline{max-height:70vh;overflow:auto;position:sticky;top:1rem;align-self:start}.interactive a{color:var(--num);overflow-wrap:anywhere}.interactive ul{padding-left:1.1rem}.interactive .node{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.7;overflow-wrap:anywhere}.interactive .children{padding-left:1.25rem;border-left:1px solid var(--fg);margin-left:.35rem}.interactive summary{cursor:pointer}.interactive .pointer-label{font-size:.8em;opacity:.85;margin-inline:.5rem}.interactive:not(.enhanced) .pointer-actions{display:none}.interactive .pointer-actions{display:inline-flex;gap:.3rem;flex-wrap:wrap}.interactive .pointer-actions button{font-size:.75rem;padding:.1rem .3rem}.interactive .search-hit{outline:2px solid var(--num);outline-offset:2px}.interactive #jh-status{min-height:1.7rem}.interactive .manual-copy textarea{display:block;width:100%;min-height:8rem;font-family:monospace}.interactive .print-json{display:none}.interactive [hidden]{display:none!important}.interactive code{white-space:pre-wrap}.interactive .search-results{max-height:16rem;overflow:auto}.interactive .kind{font-weight:600}@media(max-width:700px){body.interactive{padding:1rem}.interactive .layout{grid-template-columns:1fr}.interactive .outline{position:static;max-height:15rem}.interactive .pointer-actions{display:flex}.interactive .children{padding-left:.75rem}}@media print{.interactive .toolbar,.interactive .layout,.interactive .manual-copy,.interactive #jh-status,.interactive .search-results,.interactive noscript{display:none!important}.interactive .print-json{display:block!important}.interactive .print-json pre{white-space:pre-wrap;overflow:visible}} +/* Structured forms/cards and native previews. Fixed original asset v2. */ +.rich{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.5;max-width:88rem;margin:auto;padding:2rem;--card:#fff;--line:#d3dce8;--muted:#46566b;--chip:#edf4ff;--string:#174075;--number:#134c27;--bool:#683817;--null:#4b466e} +.rich.dark{--card:#182438;--line:#43526a;--muted:#bbc9dc;--chip:#233753;--string:#c2deff;--number:#b8e4c4;--bool:#ffd8b8;--null:#d9ceff} +.rich .document-header{border-bottom:1px solid var(--line);margin-bottom:1rem}.rich .document-header h1{font-size:1.6rem;margin:.2rem 0}.rich .document-header p{color:var(--muted);font-size:.9rem}.rich .view-tabs{display:flex;gap:.6rem;margin:1rem 0}.rich .view-tabs a{border:1px solid var(--line);border-radius:.45rem;padding:.45rem .8rem;text-decoration:none}.rich [aria-selected=true]{background:var(--chip);font-weight:650} +.rich .layout{grid-template-columns:minmax(0,1fr) 16rem}.rich .node{font-family:inherit;min-width:0;overflow-wrap:anywhere}.rich .container{background:var(--card);border:1px solid var(--line);border-radius:.65rem;padding:.7rem;margin:.2rem 0 .6rem}.rich summary{cursor:pointer;line-height:1.6}.rich summary .kind{color:var(--muted);font-size:.83rem}.rich .container.depth-0{padding:1rem}.rich .container.depth-1{border-inline-start:3px solid #789ac3}.rich .container.depth-2{border-inline-start:3px solid #809d8b}.rich .container.depth-3{border-inline-start:3px solid #9b8ead} +.rich .object-form{margin:.6rem 0 0}.rich .field-row{display:grid;grid-template-columns:minmax(7rem,23%) minmax(0,1fr);gap:.6rem;border-top:1px solid var(--line);padding:.6rem 0}.rich dt,.rich dd{margin:0;min-width:0}.rich dt{color:var(--muted)}.rich .key{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.85rem}.rich .scalar{padding:.2rem .45rem;border-radius:.35rem;background:var(--chip);white-space:normal}.rich .scalar.string{color:var(--string)}.rich .scalar.number{color:var(--number)}.rich .scalar.bool{color:var(--bool)}.rich .scalar.null{color:var(--null)}.rich .display-value{white-space:pre-wrap;overflow-wrap:anywhere}.rich code{font-size:.9em}.rich .pointer-actions{margin-inline:.4rem}.rich .pointer-actions button{font-size:.72rem} +.rich .indexed-list{list-style:none;padding:0;margin:.6rem 0 0;display:grid;gap:.65rem}.rich .record-card{border:1px solid var(--line);border-radius:.55rem;padding:.65rem;min-width:0}.rich .index-label{display:block;font-size:.8rem;color:var(--muted);font-weight:650;margin-bottom:.35rem}.rich .scalar-flow{display:flex;flex-wrap:wrap;list-style:none;padding:0;gap:.4rem}.rich .flow-item{min-width:0;max-width:100%}.rich .visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}.rich .original-value{margin:.4rem 0;color:var(--fg);border-top:1px dashed var(--line);padding-top:.4rem}.rich .original-value code{display:block;white-space:pre-wrap;overflow-wrap:anywhere}.rich .preview-label,.rich .preview-kind{display:block;font-size:.78rem;color:var(--muted);margin:.25rem 0}.rich .color-swatch{display:block;width:7.5rem;height:3rem;border:1px solid var(--line);border-radius:.4rem;margin:.3rem 0}.rich .image-preview{margin:.5rem 0}.rich .image-preview img{display:block;max-width:100%;height:auto;object-fit:contain}.rich figcaption{font-size:.78rem;color:var(--muted)}.rich math{font-size:1.12em;max-width:100%;overflow-x:auto;vertical-align:middle}.rich .inert-link,.rich .inert-image{overflow-wrap:anywhere}.rich .exact-source{margin:1rem 0}.rich .print-json{display:none}.rich dialog{width:min(90vw,44rem);background:var(--bg);color:var(--fg);border:1px solid var(--line);border-radius:.5rem}.rich dialog::backdrop{background:#0008}.rich .manual-copy textarea{box-sizing:border-box}.rich .search-hit{outline:2px solid var(--num);outline-offset:2px}.rich pre{white-space:pre-wrap;overflow-wrap:anywhere;overflow:visible}.rich a:focus-visible,.rich button:focus-visible,.rich input:focus-visible,.rich select:focus-visible{outline:3px solid var(--num);outline-offset:3px} +@media(prefers-color-scheme:dark){.rich.automatic{--card:#182438;--line:#43526a;--muted:#bbc9dc;--chip:#233753;--string:#c2deff;--number:#b8e4c4;--bool:#ffd8b8;--null:#d9ceff}} +@media(max-width:700px){.rich,.rich.interactive{padding:.8rem}.rich .layout{grid-template-columns:minmax(0,1fr)}.rich .field-row{grid-template-columns:minmax(0,1fr);gap:.3rem}.rich .container.depth-0{padding:.55rem}.rich .container,.rich .record-card{padding:.45rem}.rich .outline{position:static}} +@media(prefers-reduced-motion:reduce){.rich *{scroll-behavior:auto!important;animation:none!important;transition:none!important}} +@media(forced-colors:active){.rich .scalar,.rich .container,.rich .record-card{background:Canvas;color:CanvasText;border-color:CanvasText}.rich .color-swatch{forced-color-adjust:none}.rich .kind,.rich .preview-kind,.rich .preview-label,.rich .index-label{color:CanvasText}} +@media print{.rich .screen-view,.rich .document-header,.rich .exact-source,.rich .view-tabs,.rich .preview-issues,.rich .toolbar,.rich #jh-status,.rich .search-results,.rich dialog,.rich noscript{display:none!important}.rich .print-json{display:block!important}.rich .print-json .container{border:1px solid #aaa;break-inside:auto}.rich .print-json .record-card{break-inside:avoid}.rich{padding:0;max-width:none;color:#111;background:#fff;--card:#fff;--line:#bbb;--muted:#333;--chip:#f6f6f6;--string:#173353;--number:#154524;--bool:#4b2611;--null:#494158}.rich .original-value{display:block}.rich .image-preview img{max-height:22cm;width:auto;max-width:100%}.rich .print-json .object-form{display:block}.rich .print-json summary{list-style:none}} +.rich pre code{unicode-bidi:plaintext;direction:ltr}.rich .container.depth-3{padding:0;border:0;margin:.2rem 0;border-radius:0}.rich dialog[open]{display:block!important} +.rich .scalar-kind{font-size:.72rem;color:var(--muted);margin-inline-end:.4rem;font-weight:600}.rich .display-value,.rich .key,.rich .original-value code,.rich .inert-link,.rich .inert-image{unicode-bidi:isolate;direction:ltr}.rich .depth-3 .field-row{grid-template-columns:minmax(0,1fr)} +.rich .color-swatch{max-width:100%;height:auto;aspect-ratio:5/2}.rich .view-tabs{flex-wrap:wrap}.rich .pointer-actions{max-width:100%;min-width:0}.rich button,.rich input,.rich select{max-width:100%;min-width:0;box-sizing:border-box}.rich button{white-space:normal;overflow-wrap:anywhere} + +@media print{.rich .source-download{display:none!important}} """# package static let script = #""" // SPDX-License-Identifier: Apache-2.0 @@ -21,6 +38,32 @@ package enum JSONHTMLInteractiveAssets { const MAX_RESULTS = 100; let searchEpoch = 0; let bulkEpoch = 0; + let activeView = 'structured'; + let hitIDs = [], hitIndex = -1, copyFocus; + const tabs = document.getElementById('jh-tabs'); + const views = ['structured', 'syntax']; + const activate = (view, focus = false) => { + const changed = activeView !== view; + activeView = view; + if (!tabs) return; + for (const name of views) { + const tab = document.getElementById('jh-tab-' + name); + const pane = document.getElementById('jh-' + name); + const selected = name === view; + tab.setAttribute('aria-selected', String(selected)); tab.tabIndex = selected ? 0 : -1; + pane.hidden = !selected; + if (focus && selected) tab.focus(); + } + const label = document.querySelector('label[for="jh-query"]'); + label.textContent = view === 'syntax' ? 'Search exact JSON source' : 'Search previews'; + if (changed) search(); + }; + const moveHit = delta => { + if (!hitIDs.length) return; + hitIndex = hitIndex < 0 ? (delta < 0 ? hitIDs.length - 1 : 0) : (hitIndex + delta + hitIDs.length) % hitIDs.length; + reveal(hitIDs[hitIndex]); + say('Result ' + (hitIndex + 1) + ' of ' + hitIDs.length + ' listed matches (maximum 100).'); + }; const bytes = encoded => { const text = atob(encoded); const result = new Uint8Array(text.length); @@ -31,7 +74,8 @@ package enum JSONHTMLInteractiveAssets { const say = message => { status.textContent = message; }; const reveal = (id, focus = true) => { const node = document.getElementById(id); - if (!node || !tree.contains(node)) return; + if (!node || !(tree.contains(node) || node.id === 'jh-syntax')) return; + if (tabs) activate(node.id === 'jh-syntax' ? 'syntax' : 'structured'); for (let parent = node.parentElement; parent; parent = parent.parentElement) { if (parent.tagName === 'DETAILS') parent.open = true; } @@ -49,21 +93,25 @@ package enum JSONHTMLInteractiveAssets { setTimeout(() => URL.revokeObjectURL(url), 1000); say('Download requested: ' + filename + '.'); }; - const copy = async (encoded, label) => { + const copy = async (encoded, label, opener) => { const value = text(encoded); - manual.hidden = true; + copyFocus = opener || document.activeElement; + if (manual.open) manual.close(); + else manual.hidden = true; try { if (!navigator.clipboard || !navigator.clipboard.writeText) throw new Error('Clipboard unavailable'); await navigator.clipboard.writeText(value); say('Browser accepted ' + label + ' copy. Platform clipboard transformations may differ.'); } catch (_) { manual.hidden = false; manualText.value = value; + if (manual.showModal) manual.showModal(); manualText.focus(); manualText.select(); - say('Clipboard unavailable or rejected. Select the text and copy manually, or download exact UTF-8 bytes.'); + say(value.length === 0 ? 'Root Pointer is empty. Clipboard unavailable or rejected; download exact UTF-8 bytes.' : 'Clipboard unavailable or rejected. Select the text and copy manually, or download exact UTF-8 bytes.'); } }; const bulk = open => { const epoch = ++bulkEpoch; + if (tabs && activeView === 'syntax') { say('The exact JSON source view has no container disclosures.'); return; } const nodes = tree.querySelectorAll('details.node'); let i = 0; const step = () => { @@ -78,7 +126,7 @@ package enum JSONHTMLInteractiveAssets { // Full DOM remains present. Scanning is chunked, not a browser CPU/heap quota. const search = () => { const epoch = ++searchEpoch; - results.replaceChildren(); + results.replaceChildren(); hitIDs = []; hitIndex = -1; const needle = query.value; const byteLimit = Number(query.dataset.byteLimit); let issue = ''; @@ -91,17 +139,19 @@ package enum JSONHTMLInteractiveAssets { if (new TextEncoder().encode(needle).length > byteLimit) issue = 'Search query exceeds the UTF-8 byte limit.'; } const accepted = issue ? '' : needle; - const candidates = tree.querySelectorAll('.node'); + const candidates = activeView === 'syntax' && tabs ? [document.getElementById('jh-syntax')] : tree.querySelectorAll('.node'); let i = 0, matches = 0, listed = 0; const step = () => { if (epoch !== searchEpoch) return; const end = Math.min(i + CHUNK, candidates.length); for (; i < end; i++) { const node = candidates[i]; - const fields = node.tagName === 'DETAILS' ? node.querySelector('summary').querySelectorAll('[data-search]') : node.querySelectorAll(':scope > [data-search]'); + const fields = node.id === 'jh-syntax' ? node.querySelectorAll('[data-source-search]') : node.tagName === 'DETAILS' ? node.querySelector('summary').querySelectorAll('[data-search]') : node.querySelectorAll(':scope > [data-search]'); + const key = node.parentElement.tagName === 'DD' ? node.parentElement.previousElementSibling : null; let matched = false; for (const field of fields) { if (accepted.length > 0 && field.textContent.includes(accepted)) matched = true; } - node.classList.toggle('search-hit', matched); + if (key && accepted.length && key.textContent.includes(accepted)) matched = true; + node.classList.toggle('search-hit', matched && listed < MAX_RESULTS); if (!matched) continue; matches++; if (listed < MAX_RESULTS) { @@ -109,13 +159,14 @@ package enum JSONHTMLInteractiveAssets { const link = document.createElement('a'); link.href = '#' + node.id; link.dataset.target = node.id; - link.textContent = 'Match ' + matches + ': ' + text(node.dataset.pointerEscaped); + link.textContent = 'Match ' + matches + ': ' + (node.dataset.pointerEscaped ? text(node.dataset.pointerEscaped) : 'Exact JSON source'); + hitIDs.push(node.id); item.append(link); results.append(item); listed++; } } if (i < candidates.length) setTimeout(step, 0); else { - say(issue || matches + ' matching nodes in rendered JSON key/value text. ' + listed + ' results listed (maximum ' + MAX_RESULTS + ').'); + say(issue || matches + ' matching nodes in ' + (activeView === 'syntax' ? 'exact JSON source' : 'displayed key/value previews') + '. ' + listed + ' results listed (maximum ' + MAX_RESULTS + ').'); const first = results.querySelector('a'); if (first) reveal(first.dataset.target, false); } @@ -123,6 +174,31 @@ package enum JSONHTMLInteractiveAssets { step(); }; query.addEventListener('input', search); + query.addEventListener('keydown', event => { + if (event.key === 'Escape') { event.preventDefault(); query.value = ''; search(); } + if (event.key === 'Enter') { event.preventDefault(); moveHit(event.shiftKey ? -1 : 1); } + }); + if (tabs) { + tabs.setAttribute('role', 'tablist'); tabs.setAttribute('aria-label', 'JSON view'); + for (const view of views) { + const tab = document.getElementById('jh-tab-' + view), pane = document.getElementById('jh-' + view); + tab.setAttribute('role', 'tab'); tab.setAttribute('aria-controls', pane.id); + pane.setAttribute('role', 'tabpanel'); pane.setAttribute('aria-labelledby', tab.id); + tab.addEventListener('click', event => { event.preventDefault(); activate(view); search(); }); + tab.addEventListener('keydown', event => { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + event.preventDefault(); + activate(event.key === 'Home' ? 'structured' : event.key === 'End' ? 'syntax' : view === 'syntax' ? 'structured' : 'syntax', true); search(); + }); + } + activate(document.getElementById('jh-structured').dataset.initialView); + } + const theme = document.getElementById('jh-theme'); + if (theme) { + theme.value = document.body.classList.contains('dark') ? 'dark' : document.body.classList.contains('light') ? 'light' : 'automatic'; + theme.addEventListener('change', () => { for (const value of ['automatic', 'light', 'dark']) document.body.classList.toggle(value, value === theme.value); }); + } + if (manual.tagName === 'DIALOG') manual.addEventListener('close', () => { if (copyFocus && copyFocus.isConnected) copyFocus.focus(); }); document.addEventListener('click', event => { const link = event.target.closest('a[data-target]'); if (link) { event.preventDefault(); reveal(link.dataset.target); return; } @@ -130,27 +206,30 @@ package enum JSONHTMLInteractiveAssets { if (!button) return; event.preventDefault(); event.stopPropagation(); const action = button.dataset.action; + if (action === 'next') { moveHit(1); return; } + if (action === 'previous') { moveHit(-1); return; } + if (action === 'close-copy') { manual.close(); return; } if (action === 'expand') { bulk(true); return; } if (action === 'collapse') { bulk(false); return; } if (action === 'clear') { query.value = ''; search(); return; } if (action === 'select') { manualText.focus(); manualText.select(); return; } if (action === 'copy-json' || action === 'download-json') { const encoded = document.getElementById('jh-copy-json').dataset.bytes; - if (action === 'copy-json') void copy(encoded, 'JSON'); + if (action === 'copy-json') void copy(encoded, 'JSON', button); else download(encoded, 'document.json'); return; } const node = document.getElementById(button.dataset.node); if (!node) return; - if (action === 'copy-pointer') void copy(node.dataset.pointer, 'raw RFC 6901 Pointer'); - else if (action === 'copy-pointer-escaped') void copy(node.dataset.pointerEscaped, 'Pointer JSON string'); + if (action === 'copy-pointer') void copy(node.dataset.pointer, 'raw RFC 6901 Pointer', button); + else if (action === 'copy-pointer-escaped') void copy(node.dataset.pointerEscaped, 'Pointer JSON string', button); else if (action === 'download-pointer') download(node.dataset.pointer, 'pointer.txt'); }); document.body.classList.add('enhanced'); document.getElementById('jh-toolbar').hidden = false; - say('Interactive controls ready. Search scope: rendered JSON key/value text.'); + say('Interactive controls ready. Search scope: selected displayed previews or exact JSON source.'); })(); """# - package static let styleHash = "fi44l6KCKEBAprWu6rC4hvdhuRlXkKrLPRfZZl6TXrw=" - package static let scriptHash = "JPrAwRyMk+9Ugeh/SMuDKPXdJRom5+Hq7qc4Xo4GLPs=" + package static let styleHash = "f+TaCOj0W3wyCPnJkJlaJPTkXaxYdsglIBjynmeClbg=" + package static let scriptHash = "0E/zm4efMpR/ae2G76+pX+c/zABUnOf3Bmlbu8CFcDc=" } diff --git a/Sources/JSONHTML/JSONHTMLMiniMath.swift b/Sources/JSONHTML/JSONHTMLMiniMath.swift new file mode 100644 index 0000000..e674666 --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLMiniMath.swift @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +// Closed grammar rejection is deliberately distinct from operational failure. +package enum JSONHTMLPreviewRejection: Error { case syntax, eligibility } +package struct JSONHTMLMathNode { + package let tag: String + package let text: String + package let children: [Int] +} +package struct JSONHTMLMathTree { + package var nodes: [JSONHTMLMathNode] + package let root: Int + package let reservation: UInt64 + package func emit(output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws { + guard nodes.indices.contains(root) else { throw JSONHTMLFailure.invalidConfiguration } + for (i, node) in nodes.enumerated() { + try meter.work() + switch node.tag { + case "mi", "mn", "mo": + guard node.children.isEmpty, !node.text.isEmpty else { + throw JSONHTMLFailure.invalidConfiguration + } + case "mrow": + guard !node.children.isEmpty, node.text.isEmpty else { + throw JSONHTMLFailure.invalidConfiguration + } + case "msqrt": + guard node.children.count == 1, node.text.isEmpty else { + throw JSONHTMLFailure.invalidConfiguration + } + case "msup", "msub", "mfrac": + guard node.children.count == 2, node.text.isEmpty else { + throw JSONHTMLFailure.invalidConfiguration + } + case "msubsup": + guard node.children.count == 3, node.text.isEmpty else { + throw JSONHTMLFailure.invalidConfiguration + } + default: throw JSONHTMLFailure.invalidConfiguration + } + for child in node.children { + try meter.work() + guard child >= 0, child < i else { throw JSONHTMLFailure.invalidConfiguration } + } + } + try meter.retain(32) + try output.addDOM(UInt64(nodes.count) * 2 + 2, meter: &meter) + try output.append( + "", meter: &meter) + enum Step { + case node(Int) + case close(String) + } + var stack: [Step] = [] + func push(_ s: Step, _ m: inout JSONWorkMeter) throws { + try m.retain(32) + stack.append(s) + } + try push(.node(root), &meter) + while let step = stack.popLast() { + try meter.release(32) + try meter.work() + switch step { + case .close(let tag): try output.append("", meter: &meter) + case .node(let i): + let n = nodes[i] + try output.append("<" + n.tag + ">", meter: &meter) + if n.children.isEmpty { + try output.escaped(n.text, meter: &meter) + try output.append("", meter: &meter) + } else { + try push(.close(n.tag), &meter) + for child in n.children.reversed() { + try meter.work() + try push(.node(child), &meter) + } + } + } + } + try output.append("", meter: &meter) + try meter.release(32) + } +} +private struct MathFrame { + enum Kind { + case expression, group, fractionNumerator + case fractionDenominator(Int) + case radical + case script(Int, Bool, Int?, Int?) + } + let kind: Kind + var items: [Int] = [] +} +package enum JSONHTMLMiniMath { + // Fixed original mappings, not a general macro registry. + private static let greek: [String: String] = [ + "alpha": "α", "beta": "β", "gamma": "γ", "delta": "δ", "theta": "θ", "lambda": "λ", "mu": "μ", + "pi": "π", "sigma": "σ", "phi": "φ", "omega": "ω", "Delta": "Δ", "Omega": "Ω", + ] + private static let operators: [String: String] = [ + "times": "×", "cdot": "⋅", "pm": "±", "le": "≤", "ge": "≥", "ne": "≠", + ] + package static func parse( + _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter + ) throws -> JSONHTMLMathTree? { + let entry = meter.used(.retainedBytes) + do { + return try parseBody(source, policy: policy, meter: &meter, entry: entry) + } catch is JSONHTMLPreviewRejection { + try meter.release(meter.used(.retainedBytes) - entry) + return nil + } catch { throw error } + } + private static func parseBody( + _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter, entry: UInt64 + ) throws -> JSONHTMLMathTree { + var scalars: [Unicode.Scalar] = [] + var utf8: UInt64 = 0 + for scalar in source.unicodeScalars { + try meter.work() + utf8 += UInt64(scalar.utf8.count) + guard UInt64(scalars.count) < policy.maxMathSourceScalars, + utf8 <= policy.maxMathSourceUTF8Bytes + else { throw JSONHTMLPreviewRejection.eligibility } + try meter.retain(4) + scalars.append(scalar) + } + // One number-token construction plus fixed command/child-call scratch. + try meter.retain(utf8 + 128) + var nodes: [JSONHTMLMathNode] = [] + var frames: [MathFrame] = [] + var tokenCount: UInt64 = 0 + var i = 0 + func node(_ tag: String, _ text: String = "", _ children: [Int] = [], _ m: inout JSONWorkMeter) + throws -> Int + { + try m.work() + guard UInt64(nodes.count) < policy.maxMathNodes else { + throw JSONHTMLPreviewRejection.eligibility + } + try m.retain(64 + UInt64(children.count) * 8 + UInt64(text.utf8.count)) + nodes.append(.init(tag: tag, text: text, children: children)) + return nodes.count - 1 + } + func push(_ kind: MathFrame.Kind, _ m: inout JSONWorkMeter) throws { + try m.work() + guard UInt64(frames.count) < policy.maxMathSyntaxDepth else { + throw JSONHTMLPreviewRejection.eligibility + } + try m.retain(64) + frames.append(.init(kind: kind)) + } + func append(_ n: Int, _ m: inout JSONWorkMeter) throws { + try m.work() + try m.retain(8) + frames[frames.count - 1].items.append(n) + } + func space(_ m: inout JSONWorkMeter) throws { + while i < scalars.count && scalars[i] == " " { + try m.work() + i += 1 + } + } + func token(_ m: inout JSONWorkMeter) throws { + try m.work() + guard tokenCount < policy.maxMathTokens else { throw JSONHTMLPreviewRejection.eligibility } + tokenCount += 1 + } + func command(_ m: inout JSONWorkMeter) throws -> String { + try token(&m) + i += 1 + var text = "" + while i < scalars.count + && ((65...90).contains(scalars[i].value) || (97...122).contains(scalars[i].value)) + { + try m.work() + text.unicodeScalars.append(scalars[i]) + i += 1 + guard text.utf8.count <= 8 else { throw JSONHTMLPreviewRejection.syntax } + } + return text + } + func simple(_ m: inout JSONWorkMeter, single: Bool = false) throws -> Int { + guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax } + let c = scalars[i] + if c == "\\" { + let name = try command(&m) + guard let text = greek[name] else { throw JSONHTMLPreviewRejection.syntax } + return try node("mi", text, [], &m) + } + if (48...57).contains(c.value) { + try token(&m) + var text = "" + text.unicodeScalars.append(c) + i += 1 + if !single { + while i < scalars.count && (48...57).contains(scalars[i].value) { + try m.work() + text.unicodeScalars.append(scalars[i]) + i += 1 + } + if i < scalars.count && scalars[i] == "." { + text.append(".") + i += 1 + let start = i + while i < scalars.count && (48...57).contains(scalars[i].value) { + try m.work() + text.unicodeScalars.append(scalars[i]) + i += 1 + } + guard i > start else { throw JSONHTMLPreviewRejection.syntax } + } + } + return try node("mn", text, [], &m) + } + if (65...90).contains(c.value) || (97...122).contains(c.value) + || greek.values.contains(String(c)) + { + try token(&m) + i += 1 + return try node("mi", String(c), [], &m) + } + throw JSONHTMLPreviewRejection.syntax + } + func scripted(_ base: Int, _ sub: Int?, _ sup: Int?, _ m: inout JSONWorkMeter) throws -> Int { + if let sub, let sup { return try node("msubsup", "", [base, sub, sup], &m) } + if let sub { return try node("msub", "", [base, sub], &m) } + if let sup { return try node("msup", "", [base, sup], &m) } + return base + } + try push(.expression, &meter) + var pending: (base: Int, sub: Int?, sup: Int?)? + while true { + try meter.work() + try space(&meter) + if let p = pending { + if i < scalars.count && (scalars[i] == "_" || scalars[i] == "^") { + let isSub = scalars[i] == "_" + try token(&meter) + i += 1 + try space(&meter) + guard isSub ? p.sub == nil : p.sup == nil else { throw JSONHTMLPreviewRejection.syntax } + if i < scalars.count && scalars[i] == "{" { + try token(&meter) + i += 1 + pending = nil + try push(.script(p.base, isSub, p.sub, p.sup), &meter) + continue + } + let arg = try simple(&meter, single: true) + // Unbraced alphanumeric runs are ambiguous, never repaired. + if i < scalars.count { + let v = scalars[i].value + if (48...57).contains(v) || (65...90).contains(v) || (97...122).contains(v) { + throw JSONHTMLPreviewRejection.syntax + } + } + pending = (p.base, isSub ? arg : p.sub, isSub ? p.sup : arg) + continue + } + try append(try scripted(p.base, p.sub, p.sup, &meter), &meter) + pending = nil + continue + } + if i == scalars.count || scalars[i] == "}" { + guard let frame = frames.popLast(), !frame.items.isEmpty else { + throw JSONHTMLPreviewRejection.syntax + } + try meter.release(64 + UInt64(frame.items.count) * 8) + let result = + frame.items.count == 1 ? frame.items[0] : try node("mrow", "", frame.items, &meter) + switch frame.kind { + case .expression: + guard i == scalars.count, frames.isEmpty else { throw JSONHTMLPreviewRejection.syntax } + try meter.release(UInt64(scalars.count) * 4 + utf8 + 128) + return .init(nodes: nodes, root: result, reservation: meter.used(.retainedBytes) - entry) + case .group: + guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax } + try token(&meter) + i += 1 + pending = (result, nil, nil) + case .radical: + guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax } + try token(&meter) + i += 1 + pending = (try node("msqrt", "", [result], &meter), nil, nil) + case .fractionNumerator: + guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax } + try token(&meter) + i += 1 + try space(&meter) + guard i < scalars.count && scalars[i] == "{" else { + throw JSONHTMLPreviewRejection.syntax + } + try token(&meter) + i += 1 + try push(.fractionDenominator(result), &meter) + case .fractionDenominator(let numerator): + guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax } + try token(&meter) + i += 1 + pending = (try node("mfrac", "", [numerator, result], &meter), nil, nil) + case .script(let base, let isSub, let sub, let sup): + guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax } + try token(&meter) + i += 1 + pending = (base, isSub ? result : sub, isSub ? sup : result) + } + continue + } + let c = scalars[i] + if c == "{" { + try token(&meter) + i += 1 + try push(.group, &meter) + continue + } + if c == "\\" { + let name = try command(&meter) + if name == "frac" || name == "sqrt" { + try space(&meter) + guard i < scalars.count && scalars[i] == "{" else { + throw JSONHTMLPreviewRejection.syntax + } + try token(&meter) + i += 1 + try push(name == "frac" ? .fractionNumerator : .radical, &meter) + continue + } + if let g = greek[name] { + pending = (try node("mi", g, [], &meter), nil, nil) + continue + } + if let op = operators[name] { + try append(try node("mo", op, [], &meter), &meter) + continue + } + throw JSONHTMLPreviewRejection.syntax + } + if "+-=(),/×⋅±≤≥≠".unicodeScalars.contains(c) { + try token(&meter) + i += 1 + try append(try node("mo", String(c), [], &meter), &meter) + continue + } + pending = (try simple(&meter), nil, nil) + } + } +} diff --git a/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift b/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift new file mode 100644 index 0000000..99d4cc4 --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Immutable policy accompanying an already-rendered static HTML artifact. +/// Browser fonts/raster readiness is awaited by explicit PDF preparation, not claimed by native admission. +public struct JSONHTMLPreparedPolicy: Sendable { + /// Native structure used by both the static artifact and its print representation. + public let layout: JSONHTMLLayout + /// Main artifact string profile; inspection is explicitly labelled. + public let valueProfile: JSONHTMLValueProfile + /// Print/PDF profile retained by the artifact, with complete values by default. + public let printValueProfile: JSONHTMLValueProfile + /// Fixed native MathML and system fonts; no downloaded fonts. + public let fontPolicy: String + /// PNG/JPEG headers admitted natively; browser decode must complete before PDF capture. + public let mediaPolicy: String + package init(options: JSONExportOptions) { + layout = options.layout + valueProfile = options.valueProfile + printValueProfile = options.printValueProfile + fontPolicy = "System fonts and native MathML Core; await document.fonts.ready" + mediaPolicy = + "Offline canonical PNG/JPEG only; await image decode; native admission is not a raster codec" + } +} diff --git a/Sources/JSONHTML/JSONHTMLPreviewOptions.swift b/Sources/JSONHTML/JSONHTMLPreviewOptions.swift new file mode 100644 index 0000000..4f4e14a --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLPreviewOptions.swift @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Independent document shape; changing it never changes exact JSON bytes. +public enum JSONHTMLLayout: String, Sendable { + /// Ordered forms, record cards and scalar lists. + case structured + /// Full exact highlighted JSON syntax. + case syntax +} +/// String presentation; both profiles retain full exact source. +public enum JSONHTMLValueProfile: String, Sendable { + /// Labelled bounded prefixes with native original-value disclosure. + case inspection + /// Full expanded original strings, with optional derived previews alongside. + case complete +} +/// The closed inline dialect applied to string previews. +public enum JSONHTMLInlineFormatting: String, Sendable { + /// Literal strings only. + case off + /// Apply only to explicitly or effectively hinted inline text. + case declared + /// Apply to bounded string prefixes after specialized preview resolution. + case automatic +} +/// Whether validated inline link destinations can be activated by a user. +public enum JSONHTMLLinkActivation: String, Sendable { + /// Destinations remain inert text; the default. + case disabled + /// Explicitly allow absolute credential-free HTTP/HTTPS user navigation. + case httpAndHTTPS +} +/// Explicit units for derived color channels, independent of JSON arithmetic. +public enum JSONHTMLColorUnits: String, Sendable { + /// Integral RGB channels from zero through 255; alpha remains zero through one. + case integer8 + /// Exact channels from zero through one; appearance rounds to nearest 8-bit channel. + case normalized +} +/// Explicit channel sequence for an array or named r/g/b/a object. +public enum JSONHTMLColorOrder: String, Sendable { + /// Three r/g/b channels. + case rgb + /// Four r/g/b/a channels. + case rgba +} +/// The only declared raster media types admitted by the native header pipeline. +public enum JSONHTMLRasterMediaType: String, Sendable { + /// A signature/header-admitted PNG; no animation claim. + case png = "image/png" + /// A signature/header-admitted JPEG; decoding remains browser work. + case jpeg = "image/jpeg" +} +/// A closed caller declaration at an exact typed instance location. +public struct JSONHTMLPreviewHint: Sendable { + /// Supported declaration kinds; no callback or arbitrary markup is accepted. + public enum Kind: Sendable { + /// Suppress all inference at this node. + case literal + /// Apply the bounded inline text dialect. + case inlineText + /// Explicit units and channel order; strings still use the complete sRGB grammar. + /// - Parameters: + /// - units: Exact integer8 or normalized interpretation before appearance conversion. + /// - order: Required r/g/b or r/g/b/a field order. + case color(units: JSONHTMLColorUnits, order: JSONHTMLColorOrder) + /// Declare a bare canonical Base64 string with an allowlisted media type. + case embeddedImage(JSONHTMLRasterMediaType) + } + /// Keys and indices retain distinct scalar identity. + public let path: [JSONLocationComponent] + /// Closed declaration; annotation intent is not a schema verdict or codec proof. + public let kind: Kind + /// Creates a declaration without traversing or retaining a source document. + /// - Parameters: + /// - path: Exact typed instance location. + /// - kind: Closed preview declaration. + public init(path: [JSONLocationComponent], kind: Kind) { + self.path = path + self.kind = kind + } +} +/// Finite preview eligibility ceilings, additional to unchanged global core/HTML limits. +/// +/// These initial policies are not raster decoding, browser instruction or RSS caps. +/// Local ineligibility preserves source; actual enclosing exhaustion is terminal. +public struct JSONHTMLPreviewPolicy: Sendable { + /// Decoded source scalars in an inspection prefix. + public var maxSourceScalars: UInt64 = 512 + /// UTF-8 bytes in that prefix. + public var maxPrefixUTF8Bytes: UInt64 = 2048 + /// Reversible display scalars, excluding quotation/status chrome. + public var maxEscapedDisplayScalars: UInt64 = 512 + /// Inline delimiter stack depth. + public var maxInlineNesting: UInt64 = 8 + /// Inline staged token count. + public var maxInlineTokens: UInt64 = 1024 + /// Decoded inline destination UTF-8 bytes. + public var maxDestinationUTF8Bytes: UInt64 = 2048 + /// Inline image alternative text scalars. + public var maxAltScalars: UInt64 = 128 + /// Math candidate source scalars. + public var maxMathSourceScalars: UInt64 = 256 + /// Math candidate UTF-8 bytes. + public var maxMathSourceUTF8Bytes: UInt64 = 1024 + /// Math syntax stack depth. + public var maxMathSyntaxDepth: UInt64 = 16 + /// Math descriptor nodes per candidate. + public var maxMathNodes: UInt64 = 256 + /// Math tokens per candidate. + public var maxMathTokens: UInt64 = 512 + /// Declared encoded raster candidate bytes. + public var maxEncodedImageBytes: UInt64 = 3_145_728 + /// Decoded bytes per image. + public var maxDecodedImageBytes: UInt64 = 2_097_152 + /// Aggregate admitted decoded raster bytes across unique previews. + public var maxAggregateDecodedImageBytes: UInt64 = 8_388_608 + /// Raster width in pixels. + public var maxImageWidth: UInt64 = 2048 + /// Raster height in pixels. + public var maxImageHeight: UInt64 = 2048 + /// Checked raster width times height. + public var maxImagePixels: UInt64 = 4_194_304 + /// Maximum native header bytes examined per candidate. + public var maxImageHeaderBytes: UInt64 = 262_144 + /// Unique admitted raster previews. + public var maxImagePreviews: UInt64 = 64 + /// Caller declaration records per hint tier. + public var maxHintRecords: UInt64 = 1024 + /// Complete color candidate UTF-8 bytes. + public var maxColorCandidateBytes: UInt64 = 256 + /// Creates the initial finite policy; mutate specific ceilings before rendering. + public init() {} + package func validate() throws { + let values = [ + maxSourceScalars, maxPrefixUTF8Bytes, maxEscapedDisplayScalars, maxInlineNesting, + maxInlineTokens, + maxDestinationUTF8Bytes, maxAltScalars, maxMathSourceScalars, maxMathSourceUTF8Bytes, + maxMathSyntaxDepth, + maxMathNodes, maxMathTokens, maxEncodedImageBytes, maxDecodedImageBytes, + maxAggregateDecodedImageBytes, + maxImageWidth, maxImageHeight, maxImagePixels, maxImageHeaderBytes, maxImagePreviews, + maxHintRecords, maxColorCandidateBytes, + ] + guard values.allSatisfy({ $0 < UInt64(Int.max) }) else { + throw JSONHTMLFailure.invalidConfiguration + } + } +} diff --git a/Sources/JSONHTML/JSONHTMLPreviewPlan.swift b/Sources/JSONHTML/JSONHTMLPreviewPlan.swift new file mode 100644 index 0000000..080a37d --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLPreviewPlan.swift @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +package enum JSONHTMLPrimaryPreview { + case literal + case inline(JSONHTMLInlinePlan) + case color(JSONHTMLColor) + case image(JSONHTMLRaster) +} +package struct JSONHTMLNodePreview { + let prefix: JSONHTMLStringPrefix? + let primary: JSONHTMLPrimaryPreview +} +package struct JSONHTMLPreviewIssue { + let path: [JSONLocationComponent] + let message: String +} +package struct JSONHTMLPreviewPlan { + var nodes: [JSONHTMLNodePreview] + var issues: [JSONHTMLPreviewIssue] + let media: JSONHTMLMediaLedger + let reservation: UInt64 + var shortened: Bool + package static func make( + _ value: JSON, options: JSONHTMLRenderingOptions, meter: inout JSONWorkMeter + ) throws -> Self { + let entry = meter.used(.retainedBytes) + try options.previewPolicy.validate() + let explicit = options.previewHints + let effective = options.effectivePreviewHints + for tier in [explicit, effective] { + try meter.work() + guard UInt64(tier.count) <= options.previewPolicy.maxHintRecords else { + throw JSONHTMLFailure.invalidConfiguration + } + for i in tier.indices { + try meter.work() + try meter.require(.pointerTokens, UInt64(tier[i].path.count)) + for j in 0.. Bool { + let v = c.value + return c.properties.generalCategory == .format || v <= 31 || (127...159).contains(v) + || [0xad, 0x61c, 0xfeff, 0xe0001].contains(v) + || (0x200b...0x200f).contains(v) || (0x2028...0x202e).contains(v) + || (0x2060...0x206f).contains(v) || (0xfff9...0xfffb).contains(v) + || (0xe0020...0xe007f).contains(v) + } + package static func units(_ c: Unicode.Scalar) -> UInt64 { + if protected(c) { return c.value > 0xffff ? 12 : 6 } + return c == "\"" || c == "\\" ? 2 : 1 + } + package static func prefix( + _ text: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter + ) throws -> JSONHTMLStringPrefix { + var iterator = text.unicodeScalars.makeIterator() + var source = "" + var count: UInt64 = 0 + var bytes: UInt64 = 0 + var display: UInt64 = 0 + while let c = iterator.next() { + try meter.work() + let width = UInt64(c.utf8.count) + let cost = units(c) + if count >= policy.maxSourceScalars || bytes + width > policy.maxPrefixUTF8Bytes + || display + cost > policy.maxEscapedDisplayScalars + { + return .init( + source: source, shortened: true, scalars: count, displayScalars: display, + reservation: bytes) + } + for _ in c.utf8 { + try meter.work() + try meter.retain(1) + } + source.unicodeScalars.append(c) + count += 1 + bytes += width + display += cost + } + return .init( + source: source, shortened: false, scalars: count, displayScalars: display, reservation: bytes) + } + package static func text( + _ text: String, quoted: Bool, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter + ) throws { + if quoted { try output.escaped(34, meter: &meter) } + func escape(_ value: UInt32, _ out: inout JSONHTMLDocumentBuffer, _ m: inout JSONWorkMeter) + throws + { + try out.append("\\u", meter: &m) + for shift in [12, 8, 4, 0] { + try m.work() + try out.appendByte(hex[Int(value >> shift & 15)], meter: &m) + } + } + for c in text.unicodeScalars { + try meter.work() + if protected(c) { + if c.value <= 0xffff { + try escape(c.value, &output, &meter) + } else { + let v = c.value - 0x10000 + try escape(0xd800 + (v >> 10), &output, &meter) + try escape(0xdc00 + (v & 1023), &output, &meter) + } + } else if c == "\"" || c == "\\" { + try output.appendByte(92, meter: &meter) + for b in c.utf8 { try output.escaped(b, meter: &meter) } + } else { + for b in c.utf8 { try output.escaped(b, meter: &meter) } + } + } + if quoted { try output.escaped(34, meter: &meter) } + } + package static func equal( + _ lhs: [JSONLocationComponent], _ rhs: [JSONLocationComponent], meter: inout JSONWorkMeter + ) throws -> Bool { + try meter.work() + guard lhs.count == rhs.count else { return false } + for (a, b) in zip(lhs, rhs) { + try meter.work() + switch (a, b) { + case (.index(let x), .index(let y)): if x != y { return false } + case (.key(let x), .key(let y)): + var l = x.string.utf8.makeIterator() + var r = y.string.utf8.makeIterator() + while true { + try meter.work() + let a = l.next() + let b = r.next() + if a != b { return false } + if a == nil { break } + } + default: return false + } + } + return true + } +} +package struct JSONHTMLColor { + package let r: UInt8, g: UInt8, b: UInt8, a: Double + package let derived: Bool + package func emit( + ordinal: UInt64, namespace: String, label: JSONHTMLStringPrefix?, + output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter + ) throws { + guard a.isFinite, a >= 0, a <= 1 else { throw JSONHTMLFailure.invalidConfiguration } + try meter.retain(64) + try output.addDOM(18, meter: &meter) + try output.append( + "", + meter: &meter) + if !derived, let label { + try output.escaped(label.source, meter: &meter) + if label.shortened { try output.append(" (preview)", meter: &meter) } + } else { + try output.append("Derived #", meter: &meter) + for v in [r, g, b, UInt8((a * 255).rounded())] { + for n in [v >> 4, v & 15] { + try meter.work() + try output.appendByte(n < 10 ? n + 48 : n + 87, meter: &meter) + } + } + } + try output.append("", meter: &meter) + try output.append(derived ? "Derived sRGB channels: " : "sRGB channels: ", meter: &meter) + for (i, v) in [r, g, b].enumerated() { + if i > 0 { try output.append(", ", meter: &meter) } + try output.decimal(UInt64(v), meter: &meter) + } + try output.append("; alpha ", meter: &meter) + try output.append(String(a), meter: &meter) + try output.append("", meter: &meter) + try meter.release(64) + } + package static func parse( + _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter + ) throws -> Self? { + var bytes: [UInt8] = [] + for b in source.utf8 { + try meter.work() + guard UInt64(bytes.count) < policy.maxColorCandidateBytes, b >= 32, b < 127 else { + try meter.release(UInt64(bytes.count)) + return nil + } + try meter.retain(1) + bytes.append(b) + } + try meter.retain(128) + let result = try parseBytes(bytes, meter: &meter) + try meter.release(128 + UInt64(bytes.count)) + return result + } + private static func parseBytes(_ bytes: [UInt8], meter: inout JSONWorkMeter) throws -> Self? { + func nibble(_ x: UInt8) -> UInt8? { + switch x { + case 48...57: return x - 48 + case 65...70: return x - 55 + case 97...102: return x - 87 + default: return nil + } + } + if bytes.first == 35 { + guard [4, 5, 7, 9].contains(bytes.count) else { return nil } + var values: [UInt8] = [] + let short = bytes.count < 6 + var i = 1 + while i < bytes.count { + try meter.work() + guard let n = nibble(bytes[i]) else { return nil } + if short { + values.append(n * 17) + i += 1 + } else { + guard let m = nibble(bytes[i + 1]) else { return nil } + values.append(n * 16 + m) + i += 2 + } + } + return .init( + r: values[0], g: values[1], b: values[2], + a: values.count == 4 ? Double(values[3]) / 255 : 1, derived: false) + } + let rgba = bytes.starts(with: Array("rgba(".utf8)) + let rgb = bytes.starts(with: Array("rgb(".utf8)) + guard rgba || rgb else { return nil } + var i = rgba ? 5 : 4 + var values: [Double] = [] + for channel in 0..<(rgba ? 4 : 3) { + while i < bytes.count && bytes[i] == 32 { + try meter.work() + i += 1 + } + var value: UInt64 = 0 + var digits = 0 + while i < bytes.count && (48...57).contains(bytes[i]) { + try meter.work() + guard value <= 255 else { return nil } + value = value * 10 + UInt64(bytes[i] - 48) + digits += 1 + i += 1 + } + guard digits > 0 || (channel == 3 && i < bytes.count && bytes[i] == 46) else { return nil } + var projected = Double(value) + if channel == 3 && i < bytes.count && bytes[i] == 46 { + i += 1 + let start = i + var factor = 0.1 + while i < bytes.count && (48...57).contains(bytes[i]) { + try meter.work() + if value == 1 && bytes[i] != 48 { return nil } + projected += Double(bytes[i] - 48) * factor + factor /= 10 + i += 1 + } + guard i > start else { return nil } + } + guard projected <= Double(channel == 3 ? 1 : 255) else { return nil } + values.append(projected) + while i < bytes.count && bytes[i] == 32 { + try meter.work() + i += 1 + } + let separator: UInt8 = channel == (rgba ? 3 : 2) ? 41 : 44 + guard i < bytes.count && bytes[i] == separator else { return nil } + i += 1 + } + guard i == bytes.count else { return nil } + return .init( + r: UInt8(values[0]), g: UInt8(values[1]), b: UInt8(values[2]), a: rgba ? values[3] : 1, + derived: true) + } + package static func hinted( + _ value: JSON, units: JSONHTMLColorUnits, order: JSONHTMLColorOrder, meter: inout JSONWorkMeter + ) throws -> Self? { + try meter.retain(512) + let result = try hintedBody(value, units: units, order: order, meter: &meter) + try meter.release(512) + return result + } + private static func hintedBody( + _ value: JSON, units: JSONHTMLColorUnits, order: JSONHTMLColorOrder, meter: inout JSONWorkMeter + ) throws -> Self? { + var channels: [JSON] = [] + switch value { + case .array(let v): + guard v.count <= 4 else { return nil } + channels = v + case .object(let o): + for key in order == .rgb ? ["r", "g", "b"] : ["r", "g", "b", "a"] { + try meter.work() + guard let v = try o.lookup(key, meter: &meter) else { return nil } + channels.append(v) + } + guard o.count == channels.count else { return nil } + default: return nil + } + guard channels.count == (order == .rgb ? 3 : 4) else { return nil } + var projected: [Double] = [] + for (i, c) in channels.enumerated() { + try meter.work() + guard case .number(let n) = c else { return nil } + if units == .integer8 && i < 3 { + guard n.components.exponent >= 0 else { return nil } + // Range/type rejection is local; a real numeric meter failure must propagate. + guard try n.compare(to: .init(integer: 0), meter: &meter) != .orderedAscending, + try n.compare(to: .init(integer: 255), meter: &meter) != .orderedDescending + else { return nil } + projected.append(Double(try n.integer(as: UInt8.self, meter: &meter))) + } else { + guard try n.compare(to: .init(integer: 0), meter: &meter) != .orderedAscending, + try n.compare(to: .init(integer: 1), meter: &meter) != .orderedDescending + else { return nil } + projected.append(try n.doubleValue(policy: .rounded, meter: &meter)) + } + } + func byte(_ i: Int) -> UInt8 { + UInt8(units == .normalized ? (projected[i] * 255).rounded() : projected[i]) + } + return .init( + r: byte(0), g: byte(1), b: byte(2), a: projected.count == 4 ? projected[3] : 1, derived: true) + } +} diff --git a/Sources/JSONHTML/JSONHTMLRaster.swift b/Sources/JSONHTML/JSONHTMLRaster.swift new file mode 100644 index 0000000..e7c5bef --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLRaster.swift @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +package struct JSONHTMLRaster { + package let type: JSONHTMLRasterMediaType + package let bytes: [UInt8] + package let width: UInt32, height: UInt32 + package let progressive: Bool + package var reservation: UInt64 { UInt64(bytes.count) } + package func emit( + alt: String? = nil, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter + ) throws { + try output.addDOM(6, meter: &meter) + try output.append("
\"",
", meter: &meter) + try output.append(type.rawValue, meter: &meter) + try output.append(" · ", meter: &meter) + try output.decimal(UInt64(width), meter: &meter) + try output.append(" × ", meter: &meter) + try output.decimal(UInt64(height), meter: &meter) + try output.append( + " pixels · native header admitted; raster decoding is browser work
", + meter: &meter) + } +} +package struct JSONHTMLMediaLedger { + package var bytes: UInt64 = 0 + package var images: UInt64 = 0 +} +package enum JSONHTMLRasterAdmission { + private static let pngDepths: [UInt8: [UInt8]] = [ + 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16], + ] + package static func decode( + _ source: String, type: JSONHTMLRasterMediaType?, policy: JSONHTMLPreviewPolicy, + ledger: inout JSONHTMLMediaLedger, meter: inout JSONWorkMeter + ) throws -> JSONHTMLRaster? { + let retained = meter.used(.retainedBytes) + do { + var iterator = source.utf8.makeIterator() + var declared = type + var encoded: [UInt8] = [] + if declared == nil { + var header: [UInt8] = [] + while let b = iterator.next() { + try meter.work() + guard header.count < 32 else { throw JSONHTMLPreviewRejection.syntax } + try meter.retain(1) + header.append(b) + if b == 44 { break } + } + if header == Array("data:image/png;base64,".utf8) { + declared = .png + } else if header == Array("data:image/jpeg;base64,".utf8) { + declared = .jpeg + } else { + throw JSONHTMLPreviewRejection.syntax + } + try meter.release(UInt64(header.count)) + } + while let b = iterator.next() { + try meter.work() + guard UInt64(encoded.count) < policy.maxEncodedImageBytes else { + throw JSONHTMLPreviewRejection.eligibility + } + try meter.retain(1) + encoded.append(b) + } + guard !encoded.isEmpty, encoded.count % 4 == 0, ledger.images < policy.maxImagePreviews else { + throw JSONHTMLPreviewRejection.eligibility + } + let padding = encoded.last == 61 ? (encoded[encoded.count - 2] == 61 ? 2 : 1) : 0 + let size = UInt64(encoded.count / 4) * 3 - UInt64(padding) + guard size <= policy.maxDecodedImageBytes, + size <= policy.maxAggregateDecodedImageBytes - ledger.bytes + else { throw JSONHTMLPreviewRejection.eligibility } + let decoded = try canonical(encoded, maximum: size, meter: &meter) + try meter.release(UInt64(encoded.count)) + let geometry = try dimensions(decoded, type: declared!, policy: policy, meter: &meter) + let pixels = UInt64(geometry.0) * UInt64(geometry.1) + guard geometry.0 > 0, geometry.1 > 0, UInt64(geometry.0) <= policy.maxImageWidth, + UInt64(geometry.1) <= policy.maxImageHeight, pixels <= policy.maxImagePixels + else { throw JSONHTMLPreviewRejection.eligibility } + ledger.bytes += size + ledger.images += 1 + return .init( + type: declared!, bytes: decoded, width: geometry.0, height: geometry.1, + progressive: geometry.2) + } catch is JSONHTMLPreviewRejection { + try meter.release(meter.used(.retainedBytes) - retained) + return nil + } catch { throw error } + } + package static func canonical(_ encoded: [UInt8], maximum: UInt64, meter: inout JSONWorkMeter) + throws -> [UInt8] + { + guard encoded.count % 4 == 0 else { throw JSONHTMLPreviewRejection.syntax } + func digit(_ b: UInt8) -> UInt32? { + switch b { + case 65...90: return UInt32(b - 65) + case 97...122: return UInt32(b - 71) + case 48...57: return UInt32(b + 4) + case 43: return 62 + case 47: return 63 + default: return nil + } + } + var bytes: [UInt8] = [] + var i = 0 + while i < encoded.count { + try meter.work(4) + guard let a = digit(encoded[i]), let b = digit(encoded[i + 1]) else { + throw JSONHTMLPreviewRejection.syntax + } + let c = encoded[i + 2] + let d = encoded[i + 3] + let last = i + 4 == encoded.count + var value = a << 18 | b << 12 + let emitted: Int + if c == 61 { + guard last, d == 61, b & 15 == 0 else { throw JSONHTMLPreviewRejection.syntax } + emitted = 1 + } else { + guard let cc = digit(c) else { throw JSONHTMLPreviewRejection.syntax } + value |= cc << 6 + if d == 61 { + guard last, cc & 3 == 0 else { throw JSONHTMLPreviewRejection.syntax } + emitted = 2 + } else { + guard let dd = digit(d) else { throw JSONHTMLPreviewRejection.syntax } + value |= dd + emitted = 3 + } + } + for n in 0..> UInt32(16 - 8 * n) & 255)) + } + i += 4 + } + return bytes + } + private static func dimensions( + _ data: [UInt8], type: JSONHTMLRasterMediaType, policy: JSONHTMLPreviewPolicy, + meter: inout JSONWorkMeter + ) throws -> (UInt32, UInt32, Bool) { + var scanned: UInt64 = 0 + func read(_ i: Int, _ m: inout JSONWorkMeter) throws -> UInt8 { + try m.work() + guard i >= 0, i < data.count, scanned < policy.maxImageHeaderBytes else { + throw JSONHTMLPreviewRejection.eligibility + } + scanned += 1 + return data[i] + } + func u32(_ i: Int, _ m: inout JSONWorkMeter) throws -> UInt32 { + var n: UInt32 = 0 + for x in i..<(i + 4) { n = n << 8 | UInt32(try read(x, &m)) } + return n + } + if type == .png { + guard data.count >= 45 else { throw JSONHTMLPreviewRejection.syntax } + for (i, b) in [UInt8(137), 80, 78, 71, 13, 10, 26, 10].enumerated() { + guard try read(i, &meter) == b else { throw JSONHTMLPreviewRejection.syntax } + } + guard try u32(8, &meter) == 13 else { throw JSONHTMLPreviewRejection.syntax } + for (i, b) in Array("IHDR".utf8).enumerated() { + guard try read(i + 12, &meter) == b else { throw JSONHTMLPreviewRejection.syntax } + } + let width = try u32(16, &meter) + let height = try u32(20, &meter) + let depth = try read(24, &meter) + let color = try read(25, &meter) + guard pngDepths[color]?.contains(depth) == true, try read(26, &meter) == 0, + try read(27, &meter) == 0, try read(28, &meter) <= 1 + else { throw JSONHTMLPreviewRejection.syntax } + // Owned IHDR CRC check; compressed raster validation is deliberately separate. + var crc: UInt32 = 0xffff_ffff + for i in 12..<29 { + crc ^= UInt32(try read(i, &meter)) + for _ in 0..<8 { + try meter.work() + crc = crc & 1 == 1 ? (crc >> 1) ^ 0xedb8_8320 : crc >> 1 + } + } + guard crc ^ 0xffff_ffff == (try u32(29, &meter)) else { + throw JSONHTMLPreviewRejection.syntax + } + var at = 33 + var seenIDAT = false + var ended = false + while at < data.count { + guard data.count - at >= 12 else { throw JSONHTMLPreviewRejection.syntax } + let length = UInt64(try u32(at, &meter)) + guard length <= UInt64(data.count - at) - 12 else { throw JSONHTMLPreviewRejection.syntax } + let tag = try u32(at + 4, &meter) + if tag == 0x6163_544c { throw JSONHTMLPreviewRejection.syntax } // acTL: no animation claim. + if tag == 0x4948_4452 { throw JSONHTMLPreviewRejection.syntax } + if tag == 0x4944_4154 { seenIDAT = true } + if tag == 0x4945_4e44 { + guard length == 0, seenIDAT, at + 12 == data.count else { + throw JSONHTMLPreviewRejection.syntax + } + ended = true + break + } + at += Int(length) + 12 + } + guard ended else { throw JSONHTMLPreviewRejection.syntax } + return (width, height, false) + } + guard data.count >= 4, try read(0, &meter) == 255, try read(1, &meter) == 216, + data[data.count - 2] == 255, data.last == 217 + else { throw JSONHTMLPreviewRejection.syntax } + var at = 2 + while at < data.count { + guard try read(at, &meter) == 255 else { throw JSONHTMLPreviewRejection.syntax } + at += 1 + while try at < data.count && read(at, &meter) == 255 { at += 1 } + let marker = try read(at, &meter) + at += 1 + guard marker != 0, marker != 0xd9, marker != 0xda else { + throw JSONHTMLPreviewRejection.syntax + } + if marker == 0x01 || (0xd0...0xd7).contains(marker) { continue } + let length = Int(try read(at, &meter)) * 256 + Int(try read(at + 1, &meter)) + guard length >= 2, length <= data.count - at else { throw JSONHTMLPreviewRejection.syntax } + if marker == 0xc0 || marker == 0xc2 { + guard length >= 8, try read(at + 2, &meter) == 8 else { + throw JSONHTMLPreviewRejection.syntax + } + let height = UInt32(try read(at + 3, &meter)) * 256 + UInt32(try read(at + 4, &meter)) + let width = UInt32(try read(at + 5, &meter)) * 256 + UInt32(try read(at + 6, &meter)) + let components = Int(try read(at + 7, &meter)) + guard [1, 3, 4].contains(components), length == 8 + 3 * components else { + throw JSONHTMLPreviewRejection.syntax + } + for n in 0..> 4), (1...4).contains(sampling & 15), + try read(at + 10 + n * 3, &meter) <= 3 + else { throw JSONHTMLPreviewRejection.syntax } + } + return (width, height, marker == 0xc2) + } + // Count skipped header bytes as scan work before advancing a bounded segment. + guard UInt64(length) <= policy.maxImageHeaderBytes - scanned else { + throw JSONHTMLPreviewRejection.eligibility + } + try meter.work(UInt64(length)) + scanned += UInt64(length) + at += length + } + throw JSONHTMLPreviewRejection.syntax + } +} diff --git a/Sources/JSONHTML/JSONHTMLRenderReport.swift b/Sources/JSONHTML/JSONHTMLRenderReport.swift index 04de733..c614dad 100644 --- a/Sources/JSONHTML/JSONHTMLRenderReport.swift +++ b/Sources/JSONHTML/JSONHTMLRenderReport.swift @@ -15,14 +15,23 @@ public struct JSONHTMLStatistics: Sendable, Equatable { public let outlineEntries: UInt64 /// Emitted Base64 metadata bytes; distinct from complete document and raw copy bytes. public let metadataBytes: UInt64 - /// Raw exact JSON bytes embedded for interactive copy/download; zero in formatted mode. + /// Raw exact JSON bytes embedded for exact download/copy; zero in legacy static syntax. public let copyJSONBytes: UInt64 /// Accepted caller-supplied diagnostics; zero does not assert schema validity. public let annotations: UInt64 - package init(operation: JSONOperationStatistics, nodes: UInt64, dom: UInt64, bytes: UInt64, outline: UInt64, metadata: UInt64, copyBytes: UInt64, annotations: UInt64) { + /// Whether at least one inspection prefix is shorter than its original source. + public let hasShortenedStrings: Bool + /// Bounded preview diagnostics; these are not schema-validation verdicts. + public let previewIssues: UInt64 + /// Unique header-admitted raster previews, counted once across source/print copies. + public let imagePreviews: UInt64 + /// Logical admitted raster bytes, not browser decoding or process RSS. + public let decodedImageBytes: UInt64 + package init(operation: JSONOperationStatistics, nodes: UInt64, dom: UInt64, bytes: UInt64, outline: UInt64, metadata: UInt64, copyBytes: UInt64, annotations: UInt64, shortened: Bool = false, previewIssues: UInt64 = 0, imagePreviews: UInt64 = 0, imageBytes: UInt64 = 0) { self.operation = operation; renderedJSONNodes = nodes domNodeUpperBound = dom; documentBytes = bytes outlineEntries = outline; metadataBytes = metadata; copyJSONBytes = copyBytes; self.annotations = annotations + hasShortenedStrings = shortened; self.previewIssues = previewIssues; self.imagePreviews = imagePreviews; decodedImageBytes = imageBytes } } diff --git a/Sources/JSONHTML/JSONHTMLRenderer.swift b/Sources/JSONHTML/JSONHTMLRenderer.swift index cf411d1..2766754 100644 --- a/Sources/JSONHTML/JSONHTMLRenderer.swift +++ b/Sources/JSONHTML/JSONHTMLRenderer.swift @@ -119,7 +119,7 @@ public struct JSONHTMLRenderer: Sendable { } private func renderBody(_ value: JSON, admitted: Bool = false, document: JSONDocument? = nil, meter: inout JSONWorkMeter) throws -> JSONHTMLDocumentBuffer { try meter.checkpoint() - try options.limits.validate() + try options.limits.validate(); try options.previewPolicy.validate() guard (0...8).contains(options.indentation), (1...8).contains(options.tabWidth), options.initiallyExpandedDepth >= 0, options.outlineDepth.map({ $0 >= 0 }) ?? true else { throw JSONHTMLFailure.invalidConfiguration @@ -133,8 +133,8 @@ public struct JSONHTMLRenderer: Sendable { if !admitted { try JSONAdmission.value(value, meter: &meter) } try JSONHTMLDocumentBuffer.require(.renderedJSONNodes, meter.used(.nodes), options.limits.maxRenderedJSONNodes) let annotations = try bindAnnotations(value, document: document, meter: &meter) - if options.presentation == .interactive { - let result = try renderInteractive(value, annotations: annotations, meter: &meter) + if options.presentation == .interactive || options.layout == .structured { + let result = try renderRichDocument(value, annotations: annotations, meter: &meter) try meter.release(UInt64(annotations.count) * 64) return result } @@ -204,6 +204,10 @@ package struct JSONHTMLDocumentBuffer { private var metadata: UInt64 = 0 private var annotations: UInt64 = 0 private var copyBytes: UInt64 = 0 + private var shortened = false + private var previewIssues: UInt64 = 0 + private var imagePreviews: UInt64 = 0 + private var imageBytes: UInt64 = 0 private let limits: JSONHTMLLimits package init(limits: JSONHTMLLimits) { self.limits = limits } package static func require(_ metric: JSONHTMLResourceMetric, _ count: UInt64, _ limit: UInt64) throws { @@ -223,6 +227,9 @@ package struct JSONHTMLDocumentBuffer { try meter.work() nodes = try Self.adding(.renderedJSONNodes, nodes, 1, limits.maxRenderedJSONNodes) } + package mutating func setPreviews(shortened: Bool, issues: UInt64, images: UInt64, imageBytes: UInt64) { + self.shortened = shortened; previewIssues = issues; imagePreviews = images; self.imageBytes = imageBytes + } package mutating func setAnnotations(_ count: UInt64) { annotations = count } package mutating func setCopyBytes(_ count: UInt64) { copyBytes = count } package mutating func addOutline(meter: inout JSONWorkMeter) throws { @@ -256,6 +263,6 @@ package struct JSONHTMLDocumentBuffer { } } package func statistics(_ operation: JSONOperationStatistics) -> JSONHTMLStatistics { - .init(operation: operation, nodes: nodes, dom: dom, bytes: UInt64(bytes.count), outline: outline, metadata: metadata, copyBytes: copyBytes, annotations: annotations) + .init(operation: operation, nodes: nodes, dom: dom, bytes: UInt64(bytes.count), outline: outline, metadata: metadata, copyBytes: copyBytes, annotations: annotations, shortened: shortened, previewIssues: previewIssues, imagePreviews: imagePreviews, imageBytes: imageBytes) } } diff --git a/Sources/JSONHTML/JSONHTMLRenderingOptions.swift b/Sources/JSONHTML/JSONHTMLRenderingOptions.swift index 267b8af..6e61a61 100644 --- a/Sources/JSONHTML/JSONHTMLRenderingOptions.swift +++ b/Sources/JSONHTML/JSONHTMLRenderingOptions.swift @@ -13,9 +13,9 @@ public enum JSONHTMLTheme: String, Sendable { /// Presentation of the complete standalone JSON document. public enum JSONHTMLPresentation: String, Sendable { - /// Expanded highlighted JSON without scripting. + /// Script-free expanded values in the independently selected layout. case formatted - /// Native disclosures, optional outline and fixed offline enhancement script. + /// Native structured disclosures and exact JSON pane with optional offline enhancement. case interactive } @@ -92,7 +92,7 @@ public struct JSONHTMLLimits: Sendable { /// Configuration for complete highlighted HTML with optional offline interaction. public struct JSONHTMLRenderingOptions: Sendable { - /// Plain document title; control characters are rejected and HTML is escaped. + /// Plain document title; ASCII control characters are rejected and HTML is escaped. public var title: String /// Automatic, light or dark color policy. public var theme: JSONHTMLTheme @@ -108,8 +108,28 @@ public struct JSONHTMLRenderingOptions: Sendable { /// Caller-supplied typed diagnostics; rendering does not execute schema validation. public var annotations: [JSONHTMLAnnotation] = [] - /// Formatted by default; interactive behavior is explicitly selected. - public var presentation: JSONHTMLPresentation = .formatted + /// Interactive by default; formatted retains the script-free static spelling. + public var presentation: JSONHTMLPresentation = .interactive + /// Structured by default; syntax is explicitly selectable independently of presentation. + public var layout: JSONHTMLLayout = .structured + /// Direct structured rendering defaults to labelled inspection prefixes. + public var valueProfile: JSONHTMLValueProfile = .inspection + /// Complete print is independent of the active screen pane and disclosure state. + public var printValueProfile: JSONHTMLValueProfile = .complete + /// Closed inline dialect; syntax and exact copy bytes never run it. + public var inlineFormatting: JSONHTMLInlineFormatting = .automatic + /// Recognize complete supported sRGB strings and explicitly hinted channels. + public var colorPreviews: Bool = true + /// Admit only declared PNG/JPEG previews through the native bounded pipeline. + public var embeddedImagePreviews: Bool = true + /// User navigation is disabled unless explicitly enabled for checked HTTP/HTTPS links. + public var linkActivation: JSONHTMLLinkActivation = .disabled + /// Explicit typed declarations; literal suppression takes precedence. + public var previewHints: [JSONHTMLPreviewHint] = [] + /// Caller-translated effective instance annotations; lower priority than explicit declarations. + public var effectivePreviewHints: [JSONHTMLPreviewHint] = [] + /// Additional checked finite preview eligibility policy. + public var previewPolicy: JSONHTMLPreviewPolicy = .init() /// Containers at levels below this depth start open; root is level zero. public var initiallyExpandedDepth: Int = 2 /// Include containers through this level in the outline; nil disables the outline. @@ -126,7 +146,7 @@ public struct JSONHTMLRenderingOptions: Sendable { /// - indentationUnit: Space units by default, or literal tabs. /// - tabWidth: Visual tab width, one through eight. /// - limits: Inclusive core and HTML capacities and work quotas. - public init(title: String = "JSON", theme: JSONHTMLTheme = .automatic, + public init(title: String = "JSON document", theme: JSONHTMLTheme = .automatic, indentation: Int = 2, indentationUnit: JSONIndentationUnit = .spaces, tabWidth: Int = 4, limits: JSONHTMLLimits = .init()) { self.title = title; self.theme = theme; self.indentation = indentation @@ -146,7 +166,7 @@ public struct JSONHTMLRenderingOptions: Sendable { /// - presentation: Formatted or interactive standalone document. /// - initiallyExpandedDepth: Nonnegative initial disclosure depth; root is level zero. /// - outlineDepth: Nonnegative inclusive container level, or nil to omit the outline. - public init(title: String = "JSON", theme: JSONHTMLTheme = .automatic, + public init(title: String = "JSON document", theme: JSONHTMLTheme = .automatic, indentation: Int = 2, indentationUnit: JSONIndentationUnit = .spaces, tabWidth: Int = 4, limits: JSONHTMLLimits = .init(), presentation: JSONHTMLPresentation, initiallyExpandedDepth: Int = 2, @@ -156,6 +176,22 @@ public struct JSONHTMLRenderingOptions: Sendable { self.presentation = presentation; self.initiallyExpandedDepth = initiallyExpandedDepth self.outlineDepth = outlineDepth } + /// Creates an explicitly selected layout with independent presentation/value policies. + /// - Parameters: + /// - layout: Structured or complete syntax layout. + /// - presentation: Interactive by default; formatted means script-free static output. + /// - valueProfile: Inspection by default for direct structured rendering. + /// - printValueProfile: Complete by default, independent of screen previews. + /// - title: Escaped plain title; ASCII controls are rejected. + /// - theme: Automatic/light/dark color policy. + /// - limits: Unchanged core and HTML ceilings. + public init(layout: JSONHTMLLayout, presentation: JSONHTMLPresentation = .interactive, + valueProfile: JSONHTMLValueProfile = .inspection, printValueProfile: JSONHTMLValueProfile = .complete, + title: String = "JSON document", theme: JSONHTMLTheme = .automatic, limits: JSONHTMLLimits = .init()) { + self.init(title: title, theme: theme, limits: limits, presentation: presentation) + self.layout = layout; self.valueProfile = valueProfile; self.printValueProfile = printValueProfile + } + } /// Stable identifiers for additional HTML capacities. diff --git a/Sources/JSONHTML/JSONHTMLStructured.swift b/Sources/JSONHTML/JSONHTMLStructured.swift new file mode 100644 index 0000000..1670568 --- /dev/null +++ b/Sources/JSONHTML/JSONHTMLStructured.swift @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +private enum StructuredStep { + case node(JSON, JSONLocationComponent?, Int) + case end(String, Bool) + case objectMember(JSON, JSONKey, Int) + case arrayMember(JSON, Int, Int, Bool) +} +private struct StructuredOutline { + let ordinal: UInt64 + let component: JSONLocationComponent? + let depth: Int +} + +extension JSONHTMLRenderer { + package func renderRichDocument( + _ value: JSON, annotations: [JSONHTMLBoundAnnotation], meter: inout JSONWorkMeter + ) throws -> JSONHTMLDocumentBuffer { + var planning = options + if options.layout == .syntax { + // An explicit syntax invocation never invokes the inline/media pipeline. + planning.inlineFormatting = .off + planning.colorPreviews = false + planning.embeddedImagePreviews = false + planning.previewHints = [] + planning.effectivePreviewHints = [] + } + let plan = try JSONHTMLPreviewPlan.make(value, options: planning, meter: &meter) + var output = JSONHTMLDocumentBuffer(limits: options.limits) + output.setAnnotations(UInt64(annotations.count)) + output.setPreviews( + shortened: plan.shortened && options.valueProfile == .inspection + && options.layout == .structured, issues: UInt64(plan.issues.count), + images: plan.media.images, imageBytes: plan.media.bytes) + let interactive = options.presentation == .interactive + try output.addDOM(110, meter: &meter) + try output.append( + " 0 { try output.append("img-src data:; ", meter: &meter) } + try output.append( + "base-uri 'none'; form-action 'none'; object-src 'none'\">", meter: &meter) + try output.escaped(options.title, meter: &meter) + try output.append("

", meter: &meter) + try output.escaped(options.title, meter: &meter) + try output.append("

", meter: &meter) + try output.append( + options.valueProfile == .inspection && options.layout == .structured + ? "Inspection view · bounded string previews; exact values remain available." + : "Complete values · exact JSON source remains available.", meter: &meter) + try output.append("

", meter: &meter) + let copy = try nativeBytes(value, options: writingOptions, meter: &meter) + output.setCopyBytes(UInt64(copy.count)) + try output.append( + "

Download exact JSON (UTF-8; works without JavaScript)

", meter: &meter) + if interactive { + try output.append( + "

    ", meter: &meter) + } + try meter.release(UInt64(copy.count)) + var outline: [StructuredOutline] = [] + if options.layout == .structured || interactive { + try output.append( + "
    ", + meter: &meter) + try structuredBody( + value, plan: plan, profile: options.valueProfile, namespace: "jh-n-", + interactive: interactive, countNodes: true, annotations: annotations, outline: &outline, + output: &output, meter: &meter) + try output.append("
    ", meter: &meter) + if interactive, options.outlineDepth != nil { + try output.append( + "", meter: &meter) + } + try output.append("
    ", meter: &meter) + } + if interactive || options.layout == .syntax { + try output.append( + "

    Exact JSON source

    ",
    +        meter: &meter)
    +      try highlighted(value, options: writingOptions, output: &output, meter: &meter)
    +      try output.append("
    ", meter: &meter) + } else { + try output.append( + "
    Exact JSON source (complete)
    ",
    +        meter: &meter)
    +      try highlighted(value, options: writingOptions, output: &output, meter: &meter)
    +      try output.append("
    ", meter: &meter) + } + if !plan.issues.isEmpty { + try output.addDOM(UInt64(plan.issues.count) * 4 + 4, meter: &meter) + try output.append( + "

    Preview issues

      ", + meter: &meter) + for issue in plan.issues { + try meter.work() + try output.append("
    • ", meter: &meter) + let pointer = try pointerBytes(issue.path, meter: &meter) + try output.escaped(String(decoding: pointer, as: UTF8.self), meter: &meter) + try meter.release(UInt64(pointer.count)) + try output.append(": ", meter: &meter) + try output.escaped(issue.message, meter: &meter) + try output.append("
    • ", meter: &meter) + } + try output.append("
    ", meter: &meter) + } + try annotationSection(annotations, output: &output, meter: &meter) + // Printing is a separately charged representation of the same immutable plan/root. + try output.append( + "
    ", meter: &meter) + if options.layout == .structured { + var ignored: [StructuredOutline] = [] + try structuredBody( + value, plan: plan, profile: options.printValueProfile, namespace: "jh-p-", + interactive: false, countNodes: false, annotations: [], outline: &ignored, output: &output, + meter: &meter) + } else { + try output.append("
    ", meter: &meter)
    +      try highlighted(value, options: writingOptions, output: &output, meter: &meter)
    +      try output.append("
    ", meter: &meter) + } + try meter.release(plan.reservation) + try output.append("
    ", meter: &meter) + if interactive { + try output.append("", meter: &meter) + } + try output.append("\n", meter: &meter) + try meter.checkpoint() + return output + } + private var writingOptions: JSONWritingOptions { + .init( + limits: options.limits.core, indentation: options.indentation, numberStyle: .preserved, + indentationUnit: options.indentationUnit) + } + private func scalarFlow(_ values: [JSON], meter: inout JSONWorkMeter) throws -> Bool { + guard values.count <= 10 else { return false } + for value in values { + try meter.work() + var length: UInt64 = 0 + switch value { + case .array, .object: return false + case .string(let s): + length = 2 + for scalar in s.unicodeScalars { + try meter.work() + length += JSONHTMLDisplay.units(scalar) + if length > 60 { return false } + } + default: + var cursor = JSONWritingCursor(value: value, options: writingOptions) + while (try cursor.next(meter: &meter)) != nil { + length += 1 + if length > 60 { return false } + } + } + if length > 60 { return false } + } + return true + } + private func structuredBody( + _ value: JSON, plan: JSONHTMLPreviewPlan, profile: JSONHTMLValueProfile, namespace: String, + interactive: Bool, countNodes: Bool, annotations: [JSONHTMLBoundAnnotation], + outline: inout [StructuredOutline], output: inout JSONHTMLDocumentBuffer, + meter: inout JSONWorkMeter + ) throws { + var stack: [StructuredStep] = [] + var path: [JSONLocationComponent] = [] + var ordinal: UInt64 = 0 + func push(_ step: StructuredStep, _ m: inout JSONWorkMeter) throws { + try m.retain(JSONLogicalWeights.frame) + stack.append(step) + } + try push(.node(value, nil, 0), &meter) + while let step = stack.popLast() { + try meter.release(JSONLogicalWeights.frame) + try meter.work() + try meter.retain(256) + switch step { + case .end(let markup, let pop): + try output.append(markup, meter: &meter) + if pop { + path.removeLast() + try meter.release(JSONLogicalWeights.frame) + } + case .objectMember(let child, let key, let depth): + try output.addDOM(6, meter: &meter) + try output.append( + "
    ", meter: &meter) + try JSONHTMLDisplay.text(key.string, quoted: true, output: &output, meter: &meter) + if key.string.isEmpty { + try output.append(" Empty key", meter: &meter) + } + try output.append("
    ", meter: &meter) + try push(.end("
    ", false), &meter) + try push(.node(child, .key(key), depth), &meter) + case .arrayMember(let child, let index, let depth, let flow): + try output.addDOM(6, meter: &meter) + try output.append( + flow + ? "
  • Index " + : "
  • #", meter: &meter) + try output.decimal(UInt64(index), meter: &meter) + try output.append("", meter: &meter) + try push(.end("
  • ", false), &meter) + try push(.node(child, .index(index), depth), &meter) + case .node(let node, let component, let depth): + if let component { + try meter.retain(JSONLogicalWeights.frame) + path.append(component) + } + let id = ordinal + ordinal = try JSONHTMLDocumentBuffer.adding( + .renderedJSONNodes, ordinal, 1, options.limits.maxRenderedJSONNodes) + if countNodes { try output.addNode(meter: &meter) } + let container: Bool + let empty: Bool + let kind: String + switch node { + case .object(let o): + container = true + empty = o.isEmpty + kind = "object" + case .array(let a): + container = true + empty = a.isEmpty + kind = "array" + case .string: + container = false + empty = false + kind = "string" + case .number: + container = false + empty = false + kind = "number" + case .boolean: + container = false + empty = false + kind = "bool" + case .null: + container = false + empty = false + kind = "null" + } + if interactive, container, let maxDepth = options.outlineDepth, depth <= maxDepth { + try output.addOutline(meter: &meter) + try meter.retain(40) + outline.append(.init(ordinal: id, component: component, depth: depth)) + } + try output.addDOM(24, meter: &meter) + try output.append( + container && !empty + ? (interactive + ? "
    " : ">
    ") : ">", + meter: &meter) + if !container { + try output.append("", meter: &meter) + try output.append( + kind == "bool" + ? "Boolean" : kind == "null" ? "Null" : kind == "string" ? "String" : "Number", + meter: &meter) + try output.append("", meter: &meter) + } + if container { + try output.append("", meter: &meter) + try output.append(empty ? "Empty " + kind : kind, meter: &meter) + switch node { + case .object(let o): + try output.append(" · ", meter: &meter) + try output.decimal(UInt64(o.count), meter: &meter) + try output.append(" members", meter: &meter) + case .array(let a): + try output.append(" · ", meter: &meter) + try output.decimal(UInt64(a.count), meter: &meter) + try output.append(" items", meter: &meter) + default: break + } + try output.append("", meter: &meter) + } + if interactive { + try output.append( + "", meter: &meter) + } + let preview = plan.nodes[Int(id)] + if case .color(let color) = preview.primary { + try color.emit( + ordinal: id, namespace: namespace, label: preview.prefix, output: &output, meter: &meter + ) + } + if case .image(let image) = preview.primary { + try image.emit(output: &output, meter: &meter) + } + switch node { + case .object(let members) where !members.isEmpty: + try output.append( + interactive + ? "
    " : "
    ", + meter: &meter) + try push( + .end(interactive ? "
    " : "", component != nil), &meter) + for member in members.reversed() { + try meter.work() + try push(.objectMember(member.value, member.key, depth + 1), &meter) + } + case .array(let values) where !values.isEmpty: + let flow = try scalarFlow(values, meter: &meter) + try output.append(interactive ? "" : "", meter: &meter) + try output.append( + flow ? "
      " : "
        ", meter: &meter) + try push( + .end(interactive ? "
    " : "", component != nil), &meter) + for index in values.indices.reversed() { + try meter.work() + try push(.arrayMember(values[index], index, depth + 1, flow), &meter) + } + case .string(let string): + let prefix = preview.prefix! + let usePrefix = profile == .inspection + try output.append("", meter: &meter) + if case .inline(let inline) = preview.primary, inline.formatted { + try output.append("Inline preview: ", meter: &meter) + try inline.emit(output: &output, meter: &meter) + } else { + try JSONHTMLDisplay.text( + usePrefix ? prefix.source : string, quoted: true, output: &output, meter: &meter) + } + try output.append("", meter: &meter) + if prefix.shortened + && (usePrefix + || { + if case .inline = preview.primary { return true } + return false + }()) + { + try output.addDOM(4, meter: &meter) + try output.append( + "… String preview · first ", meter: &meter) + try output.decimal(prefix.scalars, meter: &meter) + try output.append(" decoded scalars (bounded display)", meter: &meter) + } + let needsOriginal: Bool + switch preview.primary { + case .literal: needsOriginal = usePrefix && prefix.shortened + default: needsOriginal = true + } + if needsOriginal { + try output.addDOM(6, meter: &meter) + if profile == .complete { + try output.append( + "
    Original value (complete)", + meter: &meter) + } else { + try output.append( + "
    Original value (complete)", + meter: &meter) + } + try JSONHTMLDisplay.text(string, quoted: true, output: &output, meter: &meter) + try output.append( + profile == .complete ? "
    " : "", meter: &meter) + } + try push(.end("", component != nil), &meter) + default: + try output.append("", meter: &meter) + if empty { + try output.append(kind == "object" ? "{}" : "[]", meter: &meter) + } else { + try highlighted(node, options: writingOptions, output: &output, meter: &meter) + } + try output.append("", meter: &meter) + try push(.end("", component != nil), &meter) + } + } + try meter.release(256) + } + } +} diff --git a/Sources/JSONHTML/JSONPDFBackend.swift b/Sources/JSONHTML/JSONPDFBackend.swift index 2a990eb..70995e5 100644 --- a/Sources/JSONHTML/JSONPDFBackend.swift +++ b/Sources/JSONHTML/JSONPDFBackend.swift @@ -15,10 +15,17 @@ import UIKit func layout(width: Double) async throws -> JSONPDFGeometry func textInfo() async throws -> JSONPDFTextInfo func textRun(at index: Int) async throws -> JSONPDFTextRun + func beginFragments() async throws + func nextFragment() async throws -> JSONPDFFragmentStep func capture(_ rectangle: JSONPDFRectangle) async throws -> Data func stop() } +extension JSONPDFPageBackend { + package func beginFragments() async throws { throw JSONPDFFailure.layoutFailed } + package func nextFragment() async throws -> JSONPDFFragmentStep { throw JSONPDFFailure.layoutFailed } +} + private struct JSONPDFOfflineNavigation: WebPage.NavigationDeciding { @MainActor mutating func decidePolicy(for action: WebPage.NavigationAction, preferences: inout WebPage.NavigationPreferences) async -> WKNavigationActionPolicy { @@ -32,7 +39,9 @@ private struct JSONPDFOfflineNavigation: WebPage.NavigationDeciding { package init() { var configuration = WebPage.Configuration() configuration.websiteDataStore = .nonPersistent() - configuration.loadsSubresources = false + // Generated artifacts have default-src none and img-src data: only when + // native media was admitted. Permit those data images; no remote source is emitted. + configuration.loadsSubresources = true configuration.defaultNavigationPreferences.allowsContentJavaScript = false configuration.deviceSensorAuthorization = .init(decision: .deny) let page = WebPage(configuration: configuration, navigationDecider: JSONPDFOfflineNavigation()) @@ -59,11 +68,11 @@ private struct JSONPDFOfflineNavigation: WebPage.NavigationDeciding { // Fixed client-world print-profile code; content JS stays disabled. Fonts // readiness and a synchronous layout query replace correctness sleeps. let value = try await page.callJavaScript( - "const s=document.styleSheets[0]; s.insertRule('body{margin:0!important;padding:0!important;width:'+width+'px!important;background:#fff!important;color:#000!important}',s.cssRules.length); s.insertRule('main{margin:0!important}',s.cssRules.length); s.insertRule('h1{display:none!important}',s.cssRules.length); s.insertRule('pre{margin:0!important;white-space:pre-wrap!important;overflow-wrap:anywhere!important;font:11px/16px Menlo,monospace!important}',s.cssRules.length); await document.fonts.ready; const p=document.querySelector('pre'); const r=p.getBoundingClientRect(); return [r.width,r.height,parseFloat(getComputedStyle(p).lineHeight),document.fonts.status==='loaded'?1:0,r.x,r.y];", + "const s=document.styleSheets[0]; s.insertRule('body{margin:0!important;padding:0!important;width:'+width+'px!important;background:#fff!important;color:#000!important}',s.cssRules.length); s.insertRule('main{margin:0!important;padding:0!important;max-width:none!important}',s.cssRules.length); s.insertRule('h1{display:none!important}',s.cssRules.length); s.insertRule('.rich .print-json{display:flow-root!important;margin:0!important;padding:0!important}',s.cssRules.length); s.insertRule('.rich .source-download,.rich .screen-view,.rich .document-header,.rich .exact-source,.rich .view-tabs,.rich .preview-issues,.rich .toolbar,.rich #jh-status,.rich .search-results,.rich dialog,.rich noscript{display:none!important}',s.cssRules.length); s.insertRule('pre{margin:0!important;white-space:pre-wrap!important;overflow-wrap:anywhere!important;font:11px/16px Menlo,monospace!important}',s.cssRules.length); await document.fonts.ready; const images=document.querySelectorAll('.print-json img'); for(const image of images){await image.decode();if(!image.complete||image.naturalWidth===0)throw new Error('Image decode failed');} const rich=document.body.classList.contains('rich'); const p=rich?document.querySelector('.print-json'):document.querySelector('pre'); const r=p.getBoundingClientRect(); return [r.width,r.height,16,document.fonts.status==='loaded'?1:0,r.x,r.y];", arguments: ["width": width], contentWorld: .defaultClient) guard let values = value as? [Double], values.count == 6, values[3] == 1, values[4] == 0, values[5] == 0 else { throw JSONPDFFailure.layoutFailed } - return .init(width: values[0], height: values[1], lineHeight: values[2]) + return .init(width: values[0], height: ceil(values[1]), lineHeight: values[2]) } catch is CancellationError { throw CancellationError() } catch { throw JSONPDFFailure.layoutFailed } } @@ -96,6 +105,29 @@ private struct JSONPDFOfflineNavigation: WebPage.NavigationDeciding { } catch is CancellationError { throw CancellationError() } catch { throw JSONPDFFailure.layoutFailed } } + package func beginFragments() async throws { + guard let page else { throw CancellationError() } + do { + _ = try await page.callJavaScript("const root=document.querySelector('.print-json'); window.__jhPDFScan={walker:document.createTreeWalker(root,NodeFilter.SHOW_TEXT),rects:null,index:0,media:root.querySelectorAll('img,math'),mediaIndex:0}; return 1;", contentWorld: .defaultClient) + } catch is CancellationError { throw CancellationError() } + catch { throw JSONPDFFailure.layoutFailed } + } + package func nextFragment() async throws -> JSONPDFFragmentStep { + guard let page else { throw CancellationError() } + do { + // At most one text node/media element/rectangle per invocation. DOM + // lists are SDK state; no owned JavaScript loop or generated array grows. + let value = try await page.callJavaScript("const s=window.__jhPDFScan;if(s.rects&&s.index Data { guard let page else { throw CancellationError() } do { diff --git a/Sources/JSONHTML/JSONPDFOptions.swift b/Sources/JSONHTML/JSONPDFOptions.swift index ac2daf6..bc0be6f 100644 --- a/Sources/JSONHTML/JSONPDFOptions.swift +++ b/Sources/JSONHTML/JSONPDFOptions.swift @@ -151,6 +151,14 @@ public struct JSONPDFPreparationReport: Sendable { public let pageCount: UInt64 /// Actual backend identity: CoreText pagination or WebPage continuous capture. public let backend: String + /// Configured native HTML shape retained by the immutable input artifact. + public let htmlLayout: JSONHTMLLayout + /// Actual print profile used for capture. + public let valueProfile: JSONHTMLValueProfile + /// Native backend font readiness policy; no font download. + public let fontReadiness: String + /// Native backend awaits admitted print-image decoding before capture. + public let mediaReadiness: String /// Actual elapsed time, including platform waits; not an owned-work quota. public let elapsed: Duration } @@ -165,6 +173,8 @@ package func pdfAdd(_ metric: JSONPDFResourceMetric, _ a: UInt64, _ b: UInt64, _ } package enum JSONPDFStage: String, Sendable { case input, loading, layout, extraction, capture, composition, installation } package struct JSONPDFGeometry: Sendable { let width: Double; let height: Double; let lineHeight: Double } +package struct JSONPDFFragment: Sendable { let top: Double; let bottom: Double } +package enum JSONPDFFragmentStep: Sendable { case advance, span(JSONPDFFragment), end } package struct JSONPDFRectangle: Sendable { let x: Double; let y: Double; let width: Double; let height: Double } package struct JSONPDFTextRun: Sendable { let kind: String; let text: String } diff --git a/Sources/JSONHTML/JSONPDFPreparation.swift b/Sources/JSONHTML/JSONPDFPreparation.swift index 3c868c0..3cf2342 100644 --- a/Sources/JSONHTML/JSONPDFPreparation.swift +++ b/Sources/JSONHTML/JSONPDFPreparation.swift @@ -146,7 +146,7 @@ private final class JSONPDFCancellationState: Sendable { try advance(.loading); try meter.work(8) let backend = factory(); self.backend = backend try await backend.load(html) - try advance(.layout); try work() + try advance(.layout); try meter.work(UInt64(input.html.count)); try work() let geometry = try await backend.layout(width: width) try check() guard geometry.width.isFinite, geometry.height.isFinite, geometry.lineHeight.isFinite, @@ -169,7 +169,7 @@ private final class JSONPDFCancellationState: Sendable { var captured: UInt64 = 0 var actualPages: UInt64 = 0 var textReservation: UInt64 = 0 - if options.layout == .paginated { + if options.layout == .paginated && document.htmlPolicy?.layout != .structured { // Fixed client script and conversion loops are conservatively admitted // under this same meter, before native extraction/attributed layout. try meter.work(UInt64(input.html.count)) @@ -206,15 +206,51 @@ private final class JSONPDFCancellationState: Sendable { complete = page.isComplete } } else { - try advance(.capture); try work() - let region = JSONPDFRectangle(x: 0, y: 0, width: width, height: geometry.height) - let bytes = try await backend.capture(region) - try check() - captured = try pdfAdd(.captureBytes, captured, UInt64(bytes.count), options.limits.maxCaptureBytes) - try retainBytes(bytes, output: true) - try advance(.composition); try work() - try await compositor.append(bytes, region: region) - try check(); try meter.release(UInt64(bytes.count)); actualPages = 1 + var spans: [JSONPDFFragment] = [] + if options.layout == .paginated { + try advance(.extraction); try work(); try meter.retain(128); textReservation += 128 + try await backend.beginFragments() + var complete = false + while !complete { + try advance(.extraction); try work() + switch try await backend.nextFragment() { + case .end: complete = true + case .advance: break + case .span(let span): + guard span.top.isFinite, span.bottom.isFinite, span.top >= 0, span.bottom >= span.top, span.bottom <= geometry.height + 1 else { throw JSONPDFFailure.layoutFailed } + guard UInt64(spans.count) < UInt64(input.html.count) else { throw JSONPDFFailure.layoutFailed } + try meter.retain(32); textReservation += 32; spans.append(span) + } + } + } + var offset = 0.0 + while offset < geometry.height { + try advance(.capture); try work() + var end = min(offset + chunk, geometry.height) + if options.layout == .paginated && end < geometry.height { + // Cut through actual whitespace between line/media bounds. A tall + // unbreakable fragment is an explicit layout failure, never a syntax fallback. + var changed = true + while changed { + changed = false + for span in spans { + try work() + if span.top < end && span.bottom > end { end = floor(span.top); changed = true } + } + } + guard end > offset else { throw JSONPDFFailure.layoutFailed } + } + let region = JSONPDFRectangle(x: 0, y: offset, width: width, height: end - offset) + let bytes = try await backend.capture(region) + try check() + captured = try pdfAdd(.captureBytes, captured, UInt64(bytes.count), options.limits.maxCaptureBytes) + try retainBytes(bytes, output: true) + try advance(.composition); try work() + try await compositor.append(bytes, region: region) + try check(); try meter.release(UInt64(bytes.count)) + actualPages = try pdfAdd(.pages, actualPages, 1, options.limits.maxPages) + offset += region.height + } } try advance(.composition); try work() let pdf = try await compositor.finish(maximumBytes: min(options.limits.maxPDFBytes, input.representationLimit), @@ -237,7 +273,9 @@ private final class JSONPDFCancellationState: Sendable { return .init(result: result, operation: meter.statistics(), inputHTMLBytes: UInt64(input.html.count), existingPreparedBytes: input.bytes, capturedPDFBytes: captured, outputPDFBytes: UInt64(pdf.count), pageCount: actualPages, - backend: options.layout == .paginated ? "WebPage static syntax + CoreText visible ranges + CoreGraphics" : "WebPage continuous region + CoreGraphics", + backend: document.htmlPolicy?.layout == .structured ? "WebPage forced print structured regions + line/media bounds + CoreGraphics" : options.layout == .paginated ? "WebPage static syntax + CoreText visible ranges + CoreGraphics" : "WebPage continuous region + CoreGraphics", + htmlLayout: document.htmlPolicy?.layout ?? .syntax, valueProfile: document.htmlPolicy?.printValueProfile ?? .complete, + fontReadiness: "document.fonts.ready loaded", mediaReadiness: "print images decoded before layout/capture", elapsed: started.duration(to: .now)) } } diff --git a/Tests/JSONCLITests/HTMLPreviewTests.swift b/Tests/JSONCLITests/HTMLPreviewTests.swift new file mode 100644 index 0000000..364e874 --- /dev/null +++ b/Tests/JSONCLITests/HTMLPreviewTests.swift @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import Testing + +@testable import JSONCLIEngine +@testable import JSONFilterRuntime + +@Test(arguments: [ + ["render", "--layout", "bad"], ["render", "--string-preview", "-1"], + ["render", "--string-preview", "4611686018427387904"], + ["render", "--string-preview", "0", "--full-values"], + ["render", "--inline-formatting", "markdown"], ["render", "--value-previews", "remote"], + ["render", "--format", "text", "--layout", "structured"], + ["render", "--format", "markdown", "--full-values"], + ["render", "--format", "text", "--string-preview", "0"], + ["render", "--format", "text", "--inline-formatting", "off"], + ["render", "--format", "text", "--value-previews", "off"], +]) +func cliHTMLPreviewFlagsRejectUsageBeforeInput(argv: [String]) throws { + var context = try CLIJobContext() + do { + _ = try CLIArguments(argv, context: &context) + Issue.record("Expected usage failure") + } catch let error as CLIError { #expect(error.exit == 2) } + #expect(context.used(.totalInputBytes) == 0 && context.committedResults == 0) +} +@Test func cliHTMLPreviewZeroCompleteAndSyntaxShareAtomicOutput() throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent( + "html-preview-" + UUID().uuidString) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("input.json") + let output = root.appendingPathComponent("output.html") + try Data("\"**bold** $x_i^2$ TAIL\"".utf8).write(to: input) + for flags in [ + ["--string-preview", "0"], ["--full-values"], ["--layout", "syntax"], + ["--inline-formatting", "off", "--value-previews", "off"], + ] { + var context = try CLIJobContext() + let args = try CLIArguments( + ["render", input.path, "--output", output.path, "--overwrite"] + flags, context: &context) + #expect(try CLIEngine.execute(args, context: &context) == 0) + let html = String(decoding: try Data(contentsOf: output), as: UTF8.self) + #expect(!html.contains("&"), options: .init(title: "&")) let html = String(decoding: try item.data(for: .html), as: UTF8.self) - #expect(html.contains("class=\"light tabs-4\"")) + #expect(html.contains("class=\"rich light tabs-4\"")) #expect(html.contains("</style><script>boom</script>&")) #expect(html.contains("script-src 'none'") && html.contains("default-src 'none'")) - for forbidden in ["x)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN26", + "source": "[x](file:///private/file)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN27", + "source": "[x](//example.invalid/)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN28", + "source": "[x](https://user:secret@example.invalid/)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN29", + "source": "[x](https://example.invalid/\u0000x)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN30", + "source": "[x](javascript:alert(1))", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN31", + "source": "![photo](https://example.invalid/photo.png)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + }, + { + "id": "IN32", + "source": "![svg](data:image/svg+xml;base64,PHN2Zy8+)", + "expected": "literal_or_inert_reference", + "reason": "No unsafe navigation, remote image request or SVG input", + "status": "not_run" + } + ] +} diff --git a/Tests/JSONHTMLTests/Fixtures/Enrichment/math-cases.json b/Tests/JSONHTMLTests/Fixtures/Enrichment/math-cases.json new file mode 100644 index 0000000..44bc11c --- /dev/null +++ b/Tests/JSONHTMLTests/Fixtures/Enrichment/math-cases.json @@ -0,0 +1,411 @@ +{ + "version": 1, + "scope": "Hand-authored structural gold cases; no native parser has run", + "cases": [ + { + "id": "MT01", + "source": "x", + "expected": "converted", + "mathml": "x", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT02", + "source": "2", + "expected": "converted", + "mathml": "2", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT03", + "source": "123.45", + "expected": "converted", + "mathml": "123.45", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT04", + "source": "x^2", + "expected": "converted", + "mathml": "x2", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT05", + "source": "x_i", + "expected": "converted", + "mathml": "xi", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT06", + "source": "x_i^2", + "expected": "converted", + "mathml": "xi2", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT07", + "source": "x^2_i", + "expected": "converted", + "mathml": "xi2", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT08", + "source": "x^{12}", + "expected": "converted", + "mathml": "x12", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT09", + "source": "x_{i+1}", + "expected": "converted", + "mathml": "xi+1", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT10", + "source": "\\frac{a}{b}", + "expected": "converted", + "mathml": "ab", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT11", + "source": "\\sqrt{x}", + "expected": "converted", + "mathml": "x", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT12", + "source": "\\alpha", + "expected": "converted", + "mathml": "α", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT13", + "source": "\\pi", + "expected": "converted", + "mathml": "π", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT14", + "source": "\\Delta", + "expected": "converted", + "mathml": "Δ", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT15", + "source": "a\\times b", + "expected": "converted", + "mathml": "a×b", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT16", + "source": "x\\le y", + "expected": "converted", + "mathml": "x≤y", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT17", + "source": "\\frac{x}{0}", + "expected": "converted", + "mathml": "x0", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT18", + "source": "9007199254740993", + "expected": "converted", + "mathml": "9007199254740993", + "fallbackSource": null, + "reason": "Hand-authored expected tree; single-child groups may be unwrapped only by a documented canonicalization with equivalent tree semantics", + "status": "not_run" + }, + { + "id": "MT19", + "source": "x^2+\\unknown{a}", + "expected": "literal", + "mathml": null, + "fallbackSource": "x^2+\\unknown{a}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT20", + "source": "\\frac{a}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\frac{a}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT21", + "source": "\\frac{}{b}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\frac{}{b}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT22", + "source": "\\sqrt{}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\sqrt{}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT23", + "source": "x^", + "expected": "literal", + "mathml": null, + "fallbackSource": "x^", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT24", + "source": "x_", + "expected": "literal", + "mathml": null, + "fallbackSource": "x_", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT25", + "source": "x^12", + "expected": "literal", + "mathml": null, + "fallbackSource": "x^12", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT26", + "source": "x_ab", + "expected": "literal", + "mathml": null, + "fallbackSource": "x_ab", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT27", + "source": "x_i_j", + "expected": "literal", + "mathml": null, + "fallbackSource": "x_i_j", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT28", + "source": "x^2^3", + "expected": "literal", + "mathml": null, + "fallbackSource": "x^2^3", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT29", + "source": "{x", + "expected": "literal", + "mathml": null, + "fallbackSource": "{x", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT30", + "source": "x}", + "expected": "literal", + "mathml": null, + "fallbackSource": "x}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT31", + "source": "\\alphabet", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\alphabet", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT32", + "source": "\\sum_{i=1}^{n}i", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\sum_{i=1}^{n}i", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT33", + "source": "\\int x", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\int x", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT34", + "source": "\\sin{x}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\sin{x}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT35", + "source": "\\text{hello}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\text{hello}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT36", + "source": "\\color{red}{x}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\color{red}{x}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT37", + "source": "\\href{javascript:alert(1)}{x}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\href{javascript:alert(1)}{x}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT38", + "source": "\\begin{matrix}a\\end{matrix}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\begin{matrix}a\\end{matrix}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT39", + "source": "\\sqrt[3]{x}", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\sqrt[3]{x}", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT40", + "source": "", + "expected": "literal", + "mathml": null, + "fallbackSource": "", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT41", + "source": "x\n+y", + "expected": "literal", + "mathml": null, + "fallbackSource": "x\n+y", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT42", + "source": "x&y", + "expected": "literal", + "mathml": null, + "fallbackSource": "x&y", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT43", + "source": "x^2\\garbage", + "expected": "literal", + "mathml": null, + "fallbackSource": "x^2\\garbage", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT44", + "source": "", + "expected": "literal", + "mathml": null, + "fallbackSource": "", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + }, + { + "id": "MT45", + "source": "\\frac{a}{b}#", + "expected": "literal", + "mathml": null, + "fallbackSource": "\\frac{a}{b}#", + "reason": "Unsupported/malformed/ambiguous/trailing syntax; original entire delimited expression must remain literal", + "status": "not_run" + } + ] +} diff --git a/Tests/JSONHTMLTests/Fixtures/Enrichment/media.json b/Tests/JSONHTMLTests/Fixtures/Enrichment/media.json new file mode 100644 index 0000000..d3511c0 --- /dev/null +++ b/Tests/JSONHTMLTests/Fixtures/Enrichment/media.json @@ -0,0 +1,25 @@ +{ + "pngDataURI": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAH0lEQVR42mOwqmn//////wYGBob/Zzpc/jMxoAHCAgArzQlWwCUN4AAAAABJRU5ErkJggg==", + "jpegDataURI": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAEAAQDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDb0X/kafFX/cD/APUb0iiiivyDOf8AkYVv8R+o5L/yLqH+FH//2Q==", + "declaredBarePNG": "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAH0lEQVR42mOwqmn//////wYGBob/Zzpc/jMxoAHCAgArzQlWwCUN4AAAAABJRU5ErkJggg==", + "unhintedBarePNG": "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAH0lEQVR42mOwqmn//////wYGBob/Zzpc/jMxoAHCAgArzQlWwCUN4AAAAABJRU5ErkJggg==", + "mismatch": "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAH0lEQVR42mOwqmn//////wYGBob/Zzpc/jMxoAHCAgArzQlWwCUN4AAAAABJRU5ErkJggg==", + "invalidEncoding": "data:image/png;base64,!!!!", + "noncanonicalPadBits": "data:image/png;base64,Zh==", + "oversizedHeader": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgABhqAAAYagCAIAAAAnMJyfAAAAAElFTkSuQmCC", + "remote": "https://example.invalid/image.png", + "svg": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ+YWxlcnQoMSk8L3NjcmlwdD48L3N2Zz4=", + "inlineSmallPNG": "![sample](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAH0lEQVR42mOwqmn//////wYGBob/Zzpc/jMxoAHCAgArzQlWwCUN4AAAAABJRU5ErkJggg==)", + "declarations": [ + { + "path": [ + { + "key": "declaredBarePNG" + } + ], + "kind": "embeddedImage", + "contentEncoding": "base64", + "contentMediaType": "image/png" + } + ] +} diff --git a/Tests/JSONHTMLTests/Fixtures/Enrichment/oversized-header.png b/Tests/JSONHTMLTests/Fixtures/Enrichment/oversized-header.png new file mode 100644 index 0000000000000000000000000000000000000000..a9608c6ff0256f9cc8ec53fa1428a8626f566c63 GIT binary patch literal 45 vcmeAS@N?(olHy`uVBq!ia0y~yY+C@r985qFb%QzcffR$MtDnm{r-UW|$npug literal 0 HcmV?d00001 diff --git a/Tests/JSONHTMLTests/Fixtures/Enrichment/sample.jpg b/Tests/JSONHTMLTests/Fixtures/Enrichment/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24a7434630569ef35d66cf158d0ce979ce99b179 GIT binary patch literal 661 zcmex=``2_j6xdp@o1cgOJMMZh|#U;coyG6VInuyV4pa*FVB^NNrR z{vTivX!XqN1l2cOC(lau%ic3n%$}1|Xnp;}i+B-VC zCQY6)b=ve9GiNPYykzOJeA&aSFc^aar4&0 zM~|O8efIpt%U2&ieg5+G+xH(oe}VkP$iNKo7LbH^49#DHKz}i@urRZ*gZ#zFR1U<< zf-J0xhHOHPf$WKe!b(Ps93oB=7j8VrscandK{To8BA1wo$wSqTAg_UaMx4i*$nqK7 zV+eoUV&GwB1V$dSAcH-_?ThtKGCzj@J@B95tMny}MO}ZIE&nr2Y|Z~Dzv9XMPkIag JiPZnU2>^$~)xrP( literal 0 HcmV?d00001 diff --git a/Tests/JSONHTMLTests/Fixtures/Enrichment/sample.png b/Tests/JSONHTMLTests/Fixtures/Enrichment/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..c0615b55e1fd845298bca8c433d40901769a6670 GIT binary patch literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1|(O0oL2{=}1Z&;Q29|YLg*xLT5 iTgCh{HeA4Xh>1b_EN9pORo(|cr3{{~elF{r5}E-1SsK&; literal 0 HcmV?d00001 diff --git a/Tests/JSONHTMLTests/Fixtures/Enrichment/strings.json b/Tests/JSONHTMLTests/Fixtures/Enrichment/strings.json new file mode 100644 index 0000000..7589890 --- /dev/null +++ b/Tests/JSONHTMLTests/Fixtures/Enrichment/strings.json @@ -0,0 +1,17 @@ +{ + "empty": "", + "ascii511": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ascii512": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ascii513": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "astral511": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", + "astral512": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", + "astral513": "😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀", + "combining": "á́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́́", + "controlExpansion": "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + "bidi": "‮abc‬", + "cutoffMath": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa $x^{2}$", + "cutoffLink": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa [x](https://example.invalid/)", + "blockDocument": "# Heading\n\n**bold**\n```swift\n$x^2$\n```\n\n|a|b|", + "exactLargeIntegerText": "9007199254740993", + "fullSuffix": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaTAIL_SENTINEL" +} diff --git a/Tests/JSONHTMLTests/FormattedRendererTests.swift b/Tests/JSONHTMLTests/FormattedRendererTests.swift index a8cfdea..3f0b135 100644 --- a/Tests/JSONHTMLTests/FormattedRendererTests.swift +++ b/Tests/JSONHTMLTests/FormattedRendererTests.swift @@ -4,6 +4,12 @@ import Testing import JSON @testable import JSONHTML +// The revision changes direct defaults; these unchanged exact-syntax invariants +// explicitly select the original formatted syntax contract. +private func syntaxRenderer(options: JSONHTMLRenderingOptions = .init()) -> JSONHTMLRenderer { + var explicit = options; explicit.layout = .syntax; explicit.presentation = .formatted + return JSONHTMLRenderer(options: explicit) +} // Independent text extraction: fixed span markup is removed before decoding entities. private func body(_ html: String) throws -> [UInt8] { let start = try #require(html.range(of: "
    "))
    @@ -22,7 +28,7 @@ private func body(_ html: String) throws -> [UInt8] {
     @Test func formattedHTMLHandlesSixRootsAndEmptyContainers() throws {
         for text in ["null", "false", "9007199254740993", #""é\u0000""#, "[true,null]", #"{"a":1}"#, "[]", "{}"] {
             let root = try JSONParser().parse(text)
    -        let html = try JSONHTMLRenderer().renderString(root)
    +        let html = try syntaxRenderer().renderString(root)
             #expect(html.hasPrefix(""))
             #expect(html.hasSuffix("\n"))
             #expect(!html.contains(" [UInt8] {
     @Test func formattedHTMLPreservesExactLexemesScalarKeysAndInsertionOrder() throws {
         let text = #"{"é":9007199254740993,"é":-0.00e+2,"":1.2300E-7,"0":"é","__proto__":true,"constructor":null}"#
         let root = try JSONParser().parse(text)
    -    let actual = try body(JSONHTMLRenderer().renderString(root))
    +    let actual = try body(syntaxRenderer().renderString(root))
         let expected = "{\n  \"é\": 9007199254740993,\n  \"e\u{301}\": -0.00e+2,\n  \"\": 1.2300E-7,\n  \"0\": \"e\u{301}\",\n  \"__proto__\": true,\n  \"constructor\": null\n}"
         #expect(actual == Array(expected.utf8)) // Swift String equality alone normalizes scalars.
    -    #expect(try body(JSONHTMLRenderer().renderString(.number(42))) == Array("42".utf8))
    +    #expect(try body(syntaxRenderer().renderString(.number(42))) == Array("42".utf8))
     }
     
     @Test func formattedHTMLKeepsPayloadAndTitleInert() throws {
         let payload = " " & < > \" ' \u{0}\n\t😀"
         let options = JSONHTMLRenderingOptions(title: "&\"'")
    -    let html = try JSONHTMLRenderer(options: options).renderString(.string(payload))
    +    let html = try syntaxRenderer(options: options).renderString(.string(payload))
         #expect(!html.contains("") }
    -    return try JSONExportDocument.prepare(.array(values + [.string("LAST-CONTENT-SENTINEL")]))
    +    return try syntaxPDFInput(.array(values + [.string("LAST-CONTENT-SENTINEL")]))
     }
     @Suite(.serialized) struct ApplePDFTests {
     @Test @MainActor func applePDFPaginatedA4AndLetterHaveCompleteSelectableRegions() async throws {
    @@ -97,7 +104,7 @@ import PDFKit
         #expect(try #require(pdf.page(at: 0)).bounds(for: .mediaBox).height > 2_000)
         #expect(pdf.string?.contains("LAST-CONTENT-SENTINEL") == true)
         for source in ["null", "true", "9007199254740993", #""é😀{}\\""#, "[]", "{}"] {
    -        let item = try JSONExportDocument.prepare(JSONParser().parse(source))
    +        let item = try syntaxPDFInput(JSONParser().parse(source))
             let result = try await item.preparingPDF()
             let document = try #require(PDFDocument(data: result.data(for: .pdf)))
             #expect(document.pageCount == 1 && !(document.string ?? "").isEmpty)
    @@ -107,7 +114,7 @@ import PDFKit
     @Test @MainActor func applePDFWrapsWideContentAndHonorsZeroAndWideMargins() async throws {
         pdfBootstrap()
         let root: JSON = .string("FIRST-SENTINEL " + String(repeating: "https://example.test/long-key-😀-e\u{301}/", count: 300) + " LAST-CONTENT-SENTINEL")
    -    let item = try JSONExportDocument.prepare(root)
    +    let item = try syntaxPDFInput(root)
         for margin in [0.0, 144.0] {
             let report = try await item.preparingPDFWithReport(options: .init(marginPoints: margin))
             let pdf = try #require(PDFDocument(data: report.result.data(for: .pdf)))
    @@ -121,7 +128,7 @@ import PDFKit
         pdfBootstrap()
         let item = try pdfFixture()
         let originalJSON = try item.data(for: .json)
    -    let small = try JSONExportDocument.prepare(.null)
    +    let small = try syntaxPDFInput(.null)
         let accepted = try await small.preparingPDFWithReport()
         let inclusive = [JSONPDFLimits(maxHTMLBytes: accepted.inputHTMLBytes),
             .init(maxCaptureBytes: accepted.capturedPDFBytes), .init(maxPDFBytes: accepted.outputPDFBytes),
    @@ -160,7 +167,7 @@ import PDFKit
     
     @Test @MainActor func applePDFOwnedBudgetsAreCumulativeAndInclusive() async throws {
         pdfBootstrap()
    -    let item = try JSONExportDocument.prepare(.string("é e\u{301} 😀 exact 9007199254740993"))
    +    let item = try syntaxPDFInput(.string("é e\u{301} 😀 exact 9007199254740993"))
         let baseline = try await item.preparingPDFWithReport()
         #expect(baseline.operation[.workUnits] > baseline.inputHTMLBytes + baseline.outputPDFBytes)
         let resultBytes = baseline.existingPreparedBytes + baseline.outputPDFBytes
    @@ -179,14 +186,14 @@ import PDFKit
     
     @Test @MainActor func applePDFHiddenHTMLAndReplacementPreserveImmutablePolicy() async throws {
         pdfBootstrap()
    -    let original = try JSONExportDocument.prepare(.null).selecting([.json])
    +    let original = try syntaxPDFInput(.null).selecting([.json])
         let first = try await original.preparingPDFWithReport()
         #expect(first.result.advertisedFormats == [.json, .pdf] && first.result.availableFormats.count == 5)
         let second = try await first.result.preparingPDFWithReport()
         #expect(second.result.availableFormats.count == 5 && second.existingPreparedBytes == first.existingPreparedBytes + first.outputPDFBytes)
         #expect(try second.result.data(for: .json) == original.data(for: .json))
         let sum = try original.pdfInput().bytes
    -    let tight = try JSONExportDocument.prepare(.null, options: .init(limits: .init(maxPreparedBytes: sum)))
    +    let tight = try syntaxPDFInput(.null, options: .init(limits: .init(maxPreparedBytes: sum)))
         do { _ = try await tight.preparingPDF(); Issue.record("Original aggregate cap was widened") }
         catch { #expect((error as? JSONPDFFailure) != nil) }
         #expect(!tight.availableFormats.contains(.pdf))
    @@ -194,7 +201,7 @@ import PDFKit
     
     @Test @MainActor func applePDFFailureAndImmediateDeadlineNeverAdvertiseAnArtifact() async throws {
         pdfBootstrap()
    -    let item = try JSONExportDocument.prepare(.null)
    +    let item = try syntaxPDFInput(.null)
         do { _ = try await item.preparingPDF(options: .init(timeoutMilliseconds: 0)); Issue.record("Expired deadline accepted PDF") }
         catch { #expect(error as? JSONPDFFailure == .timedOut) }
         // Real iOS emoji fallback produced height 162 with nominal 16-point lines.
    @@ -212,7 +219,7 @@ import PDFKit
     
     @Test @MainActor func applePDFTransferOffersOnlyPreparedSelectedBytes() async throws {
         pdfBootstrap()
    -    let item = try JSONExportDocument.prepare(.null)
    +    let item = try syntaxPDFInput(.null)
         let report = try await item.preparingPDFWithReport()
         #if canImport(CoreTransferable) && canImport(UniformTypeIdentifiers)
         let selected = try report.result.selecting([.pdf])
    @@ -253,7 +260,7 @@ import PDFKit
     }
     @Test @MainActor func applePDFTimeoutReturnsWithoutWaitingForUncooperativeCapture() async throws {
         pdfBootstrap()
    -    let item = try JSONExportDocument.prepare(.null)
    +    let item = try syntaxPDFInput(.null)
         for stage in [JSONPDFStage.loading, .layout, .capture] {
         let backend = SuspendedPDFBackend(ready: {}); backend.waitAt = stage
         let start = ContinuousClock.now
    @@ -289,7 +296,7 @@ import PDFKit
     }
     #else
     @Test @MainActor func exportPDFUnavailableBackendRemainsUnadvertised() async throws {
    -    let item = try JSONExportDocument.prepare(.null)
    +    let item = try syntaxPDFInput(.null)
         do { _ = try await item.preparingPDF(); Issue.record("Unsupported platform advertised PDF") }
         catch { #expect(error as? JSONPDFFailure == .backendUnavailable) }
         #expect(!item.availableFormats.contains(.pdf))
    diff --git a/Tests/JSONHTMLTests/StructuredPDFTests.swift b/Tests/JSONHTMLTests/StructuredPDFTests.swift
    new file mode 100644
    index 0000000..9910ace
    --- /dev/null
    +++ b/Tests/JSONHTMLTests/StructuredPDFTests.swift
    @@ -0,0 +1,89 @@
    +// SPDX-License-Identifier: Apache-2.0
    +import Foundation
    +import JSON
    +import Testing
    +
    +@testable import JSONHTML
    +
    +#if canImport(PDFKit) && canImport(WebKit)
    +  import PDFKit
    +  #if canImport(AppKit)
    +    import AppKit
    +  #endif
    +  @Suite(.serialized) struct StructuredApplePDFTests {
    +    @Test @MainActor func structuredPDFPrintCaptureKeepsMediaMathFullStringsAndTail() async throws {
    +      #if canImport(AppKit)
    +        _ = NSApplication.shared
    +      #endif
    +      let url = try #require(
    +        Bundle.module.url(
    +          forResource: "media", withExtension: "json", subdirectory: "Fixtures/Enrichment"))
    +      guard case .object(let media) = try JSONParser().parse(Array(Data(contentsOf: url))) else {
    +        Issue.record("Missing media fixture")
    +        return
    +      }
    +      let root = JSON.array([
    +        .object(
    +          try JSONObject(members: [
    +            JSONMember("image", value: media[JSONKey("pngDataURI")]!),
    +            JSONMember("math", value: .string("$x_i^2$")),
    +            JSONMember("color", value: .string("#369c")),
    +          ])),
    +        .string("FIRST " + String(repeating: "long text ", count: 180) + " LAST-CONTENT-SENTINEL"),
    +      ])
    +      let original = try JSONExportDocument.prepare(root)
    +      let hashInput = try original.data(for: .html)
    +      for layout in [JSONPDFLayout.paginated, .continuous] {
    +        let report = try await original.preparingPDFWithReport(options: .init(layout: layout))
    +        let pdf = try #require(PDFDocument(data: report.result.data(for: .pdf)))
    +        #expect(report.htmlLayout == .structured && report.valueProfile == .complete)
    +        #expect(report.backend.contains("structured regions"))
    +        #expect(report.mediaReadiness.contains("decoded"))
    +        #expect(pdf.pageCount > 0 && UInt64(pdf.pageCount) == report.pageCount)
    +        let extracted = pdf.string ?? ""
    +        let compact = extracted.filter { !$0.isWhitespace }
    +        #expect(compact.contains("LAST-CONTENT-SENTINEL"))  // Native PDF text extraction inserts wrap newlines, including after hyphens.
    +        #expect(compact.components(separatedBy: "longtext").count - 1 == 180)
    +        let evidence = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
    +          .appendingPathComponent(".build/structured-pdf-" + layout.rawValue + ".pdf")
    +        try report.result.data(for: .pdf).write(to: evidence)
    +        #expect(try report.result.data(for: .html) == hashInput)
    +        #expect(report.operation[.nodes] == 0)
    +      }
    +    }
    +    @Test @MainActor func structuredPDFCapsAndFailedRasterDecodeInstallNothing() async throws {
    +      #if canImport(AppKit)
    +        _ = NSApplication.shared
    +      #endif
    +      let small = try JSONExportDocument.prepare(.null)
    +      let measured = try await small.preparingPDFWithReport()
    +      #expect(measured.htmlLayout == .structured && measured.pageCount == 1)
    +      let inclusive = JSONPDFLimits(
    +        maxHTMLBytes: measured.inputHTMLBytes, maxCaptureBytes: measured.capturedPDFBytes,
    +        maxPDFBytes: measured.outputPDFBytes)
    +      #expect(
    +        try await small.preparingPDFWithReport(options: .init(limits: inclusive)).pageCount == 1)
    +      do {
    +        _ = try await small.preparingPDF(options: .init(limits: .init(maxPages: 0)))
    +        Issue.record("Expected checked cap")
    +      } catch { #expect(error is JSONPDFFailure) }
    +      #expect(!small.availableFormats.contains(.pdf))
    +      let url = try #require(
    +        Bundle.module.url(
    +          forResource: "sample", withExtension: "jpg", subdirectory: "Fixtures/Enrichment"))
    +      let jpeg = Array(try Data(contentsOf: url))
    +      let sof = try #require(
    +        (0..<(jpeg.count - 1)).first(where: { jpeg[$0] == 255 && jpeg[$0 + 1] == 192 }))
    +      let length = Int(jpeg[sof + 2]) * 256 + Int(jpeg[sof + 3])
    +      let damaged = Data(jpeg[..<(sof + 2 + length)] + [255, 217])  // Geometry exists; SOS/entropy data is deliberately absent.
    +      let undecodable = try JSONExportDocument.prepare(
    +        .string("data:image/jpeg;base64," + damaged.base64EncodedString()))
    +      #expect(String(decoding: try undecodable.data(for: .html), as: UTF8.self).contains(": Decodable { let cases: [T] }
    +private func gold(_ name: String, _: T.Type) throws -> T {
    +  let url = try #require(
    +    Bundle.module.url(forResource: name, withExtension: "json", subdirectory: "Fixtures/Enrichment")
    +  )
    +  return try JSONDecoder().decode(T.self, from: Data(contentsOf: url))
    +}
    +private let mathGold = try! gold("math-cases", GoldFile.self).cases
    +private let inlineGold = try! gold("inline-cases", GoldFile.self).cases
    +private let base64Gold = try! gold("base64-cases", GoldFile.self).cases
    +private func inlineBody(_ source: String, options: JSONHTMLRenderingOptions = .init()) throws -> (
    +  String, Bool
    +) {
    +  var meter = try JSONWorkMeter(limits: options.limits.core, domain: .writing)
    +  var ledger = JSONHTMLMediaLedger()
    +  let plan = try JSONHTMLInlineScanner.scan(
    +    source, options: options, ledger: &ledger, meter: &meter)
    +  var out = JSONHTMLDocumentBuffer(limits: options.limits)
    +  try plan.emit(output: &out, meter: &meter)
    +  try meter.release(plan.reservation)
    +  #expect(meter.used(.retainedBytes) == UInt64(out.bytes.count))
    +  return (String(decoding: out.bytes, as: UTF8.self), plan.formatted)
    +}
    +// Canonicalization approved by the gold fixture: single-child groups are unwrapped.
    +private func unwrapSingleRows(_ source: String) -> String {
    +  var s = source
    +  for tag in ["mi", "mn", "mo"] {
    +    let expression = "(<" + tag + ">[^<]*)"
    +    while s.range(of: expression, options: .regularExpression) != nil {
    +      s = s.replacingOccurrences(of: expression, with: "$1", options: .regularExpression)
    +    }
    +  }
    +  return s
    +}
    +@Test(arguments: mathGold) private func structuredMiniMathMatchesIndependentGold(_ item: MathGold)
    +  throws
    +{
    +  var meter = try JSONWorkMeter(limits: .balanced, domain: .writing)
    +  let tree = try JSONHTMLMiniMath.parse(item.source, policy: .init(), meter: &meter)
    +  if item.expected == "converted" {
    +    let tree = try #require(tree, Comment(rawValue: item.id))
    +    var output = JSONHTMLDocumentBuffer(limits: .init())
    +    try tree.emit(output: &output, meter: &meter)
    +    #expect(
    +      String(decoding: output.bytes, as: UTF8.self) == unwrapSingleRows(try #require(item.mathml)),
    +      Comment(rawValue: item.id))
    +    try meter.release(tree.reservation)
    +    #expect(meter.used(.retainedBytes) == UInt64(output.bytes.count))
    +  } else {
    +    #expect(tree == nil, Comment(rawValue: item.id))
    +    #expect(meter.used(.retainedBytes) == 0)
    +  }
    +  let publicReport = try JSONHTMLRenderer(
    +    options: .init(layout: .structured, presentation: .formatted)
    +  ).renderStringWithReport(.string("\\(" + item.source + "\\)"))
    +  #expect(publicReport.statistics.renderedJSONNodes == 1)
    +  if item.expected == "converted" {
    +    #expect(
    +      publicReport.result.components(separatedBy: unwrapSingleRows(try #require(item.mathml))).count
    +        - 1 == 2)
    +  } else {
    +    #expect(!publicReport.result.contains("bold")
    +  case "em": #expect(html == "italic")
    +  case "strong_with_em": #expect(html == "outer inner")
    +  case "em_with_strong": #expect(html == "outer inner")
    +  case "math": #expect(html.contains("") && !html.contains(""),
    +      Comment(rawValue: item.id))
    +  case "literal_or_inert_reference":
    +    #expect(!html.contains(""))
    +  default:
    +    Issue.record(Comment(rawValue: "Unrecognized independent fixture verdict: " + item.expected))
    +  }
    +}
    +@Test(arguments: base64Gold) private func structuredCanonicalBase64MatchesPadBitGold(
    +  _ item: Base64Gold
    +) throws {
    +  var meter = try JSONWorkMeter(limits: .balanced, domain: .writing)
    +  do {
    +    let decoded = try JSONHTMLRasterAdmission.canonical(
    +      Array(item.encoded.utf8), maximum: 100, meter: &meter)
    +    #expect(item.valid)
    +    #expect(decoded.map { String(format: "%02x", $0) }.joined() == item.decodedHex)
    +  } catch is JSONHTMLPreviewRejection { #expect(!item.valid) }
    +}
    +@Test func structuredDefaultsAndOldInitializerShapesAreIndependent() throws {
    +  let old:
    +    (String, JSONHTMLTheme, Int, JSONIndentationUnit, Int, JSONHTMLLimits) ->
    +      JSONHTMLRenderingOptions = JSONHTMLRenderingOptions.init
    +  let explicit:
    +    (
    +      String, JSONHTMLTheme, Int, JSONIndentationUnit, Int, JSONHTMLLimits, JSONHTMLPresentation,
    +      Int, Int?
    +    ) -> JSONHTMLRenderingOptions = JSONHTMLRenderingOptions.init
    +  #expect(old("JSON", .light, 2, .spaces, 4, .init()).layout == .structured)
    +  #expect(
    +    explicit("JSON", .light, 2, .spaces, 4, .init(), .formatted, 2, nil).presentation == .formatted)
    +  #expect(JSONHTMLRenderingOptions().presentation == .interactive)
    +  for layout in [JSONHTMLLayout.structured, .syntax] {
    +    for presentation in [JSONHTMLPresentation.formatted, .interactive] {
    +      let report = try JSONHTMLRenderer(options: .init(layout: layout, presentation: presentation))
    +        .renderStringWithReport(.array([.null]))
    +      #expect(report.statistics.renderedJSONNodes == 2)
    +      #expect(report.statistics.operation[.nodes] == 2)
    +      #expect(report.result.contains("", presentation: .interactive)),
         ("deep", deep, .init(presentation: .interactive, initiallyExpandedDepth: 0)),
    @@ -28,9 +28,29 @@ let fixtures: [(String, JSON, JSONHTMLRenderingOptions)] = [
         ("null", .null, .init(presentation: .interactive)),
         ("zero-query", exact, zero),
         ("no-outline", exact, .init(presentation: .interactive, outlineDepth: nil)),
    -    ("formatted", exact, .init()),
    +    ("formatted", exact, .init(layout: .syntax, presentation: .formatted)),
         ("annotated", exact, annotations),
         ("annotated-formatted", exact, formattedAnnotations)]
    +let enrichmentURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent("Tests/JSONHTMLTests/Fixtures/Enrichment/media.json")
    +guard case .object(let media) = try JSONParser().parse(Array(Data(contentsOf: enrichmentURL))) else { fatalError("Missing pinned media object") }
    +let previews = try object([
    +    JSONMember("message", value: .string("**Bold** and *italic*, [inert link](https://example.invalid/a_(b)) and $x_i^2$.")),
    +    JSONMember("color", value: .string("#3A7C8780")),
    +    JSONMember("declared", value: media[JSONKey("declaredBarePNG")]!),
    +    JSONMember("image", value: media[JSONKey("pngDataURI")]!),
    +    JSONMember("jpeg", value: media[JSONKey("jpegDataURI")]!),
    +    JSONMember("inlineImage", value: media[JSONKey("inlineSmallPNG")]!),
    +    JSONMember("channels", value: try JSONParser().parse("[1,0,0,0.5]")),
    +    JSONMember("long", value: .string(String(repeating: "😀", count: 513) + "ORIGINAL-TAIL-ONLY")),
    +    JSONMember("unsupportedMath", value: .string("$x^12 + \\unknown$ **outside**")),
    +    JSONMember("records", value: .array([try object([JSONMember("id", value: .number(9007199254740993)), JSONMember("name", value: .string("record"))]), .null]))])
    +var previewOptions = JSONHTMLRenderingOptions(layout: .structured, presentation: .interactive)
    +previewOptions.previewHints = [.init(path: [.key("declared")], kind: .embeddedImage(.png)), .init(path: [.key("channels")], kind: .color(units: .normalized, order: .rgba))]
    +var staticPreview = previewOptions; staticPreview.presentation = .formatted
    +var completePreview = staticPreview; completePreview.valueProfile = .complete
    +fixtures += [("previews", previews, previewOptions), ("static-inspection", previews, staticPreview), ("complete", previews, completePreview),
    +             ("syntax", previews, .init(layout: .syntax, presentation: .interactive)),
    +             ("inspection-long", .string(String(repeating: "😀", count: 513) + "ORIGINAL-TAIL-ONLY"), .init(layout: .structured, presentation: .interactive))]
     var records: [JSON] = []
     for (name, root, options) in fixtures {
         let renderer = JSONHTMLRenderer(options: options)
    @@ -41,6 +61,8 @@ for (name, root, options) in fixtures {
         records.append(try object([
             JSONMember("name", value: .string(name)),
             JSONMember("presentation", value: .string(options.presentation.rawValue)),
    +        JSONMember("layout", value: .string(options.layout.rawValue)),
    +        JSONMember("valueProfile", value: .string(options.valueProfile.rawValue)),
             JSONMember("annotations", value: .number(JSONNumber(integer: report.statistics.annotations))),
             JSONMember("nodes", value: .number(JSONNumber(integer: report.statistics.renderedJSONNodes))),
             JSONMember("domUpperBound", value: .number(JSONNumber(integer: report.statistics.domNodeUpperBound))),
    @@ -50,4 +72,4 @@ for (name, root, options) in fixtures {
             JSONMember("logicalPeakBytes", value: .number(JSONNumber(integer: report.statistics.operation.peakLogicalRetainedBytes))) ]))
     }
     try JSONWriter(options: .init(indentation: 2)).write(.array(records)).write(to: output.appendingPathComponent("fixtures.json"))
    -print("JSONHTML_BROWSER_FIXTURES passed count=10")
    +print("JSONHTML_BROWSER_FIXTURES passed count=15")
    diff --git a/Tools/JSONHTMLBrowser/browser.cjs b/Tools/JSONHTMLBrowser/browser.cjs
    index 598a8e3..fe07d6f 100644
    --- a/Tools/JSONHTMLBrowser/browser.cjs
    +++ b/Tools/JSONHTMLBrowser/browser.cjs
    @@ -13,7 +13,7 @@ assert.equal(version, '1.62.1', 'Qualification requires the pinned Playwright ve
     const fixtures = path.resolve(process.argv[2]), out = path.resolve(process.argv[3]);
     fs.mkdirSync(out, {recursive: true});
     const records = JSON.parse(fs.readFileSync(path.join(fixtures, 'fixtures.json')));
    -assert.equal(records.length, 10);
    +assert.equal(records.length, 15);
     const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
     const receipt = {status: 'running', playwright: version, fixtures: [], engines: [], cases: [], limitations: ['Screenshots are captured in Chromium; WebKit screenshots would inject a CSP-blocked synchronization stylesheet. WebKit DOM/layout/interaction checks still enforce CSP.', 'Playwright WebKit is not native Safari qualification.', 'Viewport tests do not qualify physical mobile browsers or VoiceOver.', 'Clipboard API acceptance is separate from platform clipboard transformations; exact downloads are independently compared.', 'Search chunking is not an instruction, CPU, heap or process RSS guarantee.']};
     for (const name of fs.readdirSync(fixtures).sort()) {
    @@ -43,7 +43,7 @@ async function main() {
         }
         try { browser = await playwright[engine].launch(launch); }
         catch (error) {
    -      receipt.engines.push({name: engine, status: 'blocked', error: error.message, executedCases: 0, unexecutedCases: engine === 'chromium' ? 16 : 15});
    +      receipt.engines.push({name: engine, status: 'blocked', error: error.message, executedCases: 0, unexecutedCases: engine === 'chromium' ? 26 : 25});
           save(); continue;
         }
         activeBrowsers.push(browser);
    @@ -89,7 +89,7 @@ async function main() {
               assert.equal(await page.locator('#jh-tree .node').count(), record.nodes);
               assert.equal(await page.locator('#jh-toolbar').isVisible(), javaScriptEnabled);
               assert.ok(await page.locator('.pointer-actions').count() > 0);
    -          assert.equal((await page.locator('.pointer-actions:visible').count()) > 0, javaScriptEnabled, 'Pointer actions require installed enhancement handlers');
    +          assert.equal((await page.locator('.pointer-actions:visible').count()) > 0, javaScriptEnabled && record.layout !== 'syntax', 'Pointer actions appear in the active structured view after enhancement');
             }
             await action(page, context, record);
             if (javaScriptEnabled) {
    @@ -133,17 +133,21 @@ async function main() {
         }
         await run('keyboard-outline', 'deep', 'http', true, async page => {
           await page.locator('.outline a').last().click();
    -      assert.equal(await page.locator('details[open]').count(), 4);
    +      assert.equal(await page.locator('#jh-tree details[open]').count(), 4);
           assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-n-3');
           await page.getByRole('button', {name: 'Expand all', exact: true}).click(); await status(page, 'Expanded 40');
    -      assert.equal(await page.locator('details[open]').count(), 40);
    +      assert.equal(await page.locator('#jh-tree details[open]').count(), 40);
           await page.getByRole('button', {name: 'Collapse all', exact: true}).click(); await status(page, 'Collapsed 40');
    -      assert.equal(await page.locator('details[open]').count(), 0);
    +      assert.equal(await page.locator('#jh-tree details[open]').count(), 0);
         });
         await run('literal-search', 'wide', 'http', true, async page => {
           await page.locator('#jh-query').fill('[needle]'); await status(page, '150 matching nodes');
           assert.equal(await page.locator('#jh-results a').count(), 100);
    -      assert.equal(await page.locator('.search-hit').count(), 150);
    +      assert.equal(await page.locator('.search-hit').count(), 100);
    +      await page.getByRole('button', {name: 'Previous match', exact: true}).click();
    +      assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-n-100');
    +      await page.getByRole('button', {name: 'Next match', exact: true}).click();
    +      assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-n-1');
           const peakDOM = await page.evaluate(() => {let n = 0; const it = document.createTreeWalker(document, NodeFilter.SHOW_ALL); while (it.nextNode()) n++; return n;});
           assert.ok(peakDOM <= records.find(r => r.name === 'wide').domUpperBound);
           await page.locator('#jh-query').fill('Needle'); await status(page, '0 matching nodes');
    @@ -173,9 +177,12 @@ async function main() {
           await page.locator('#jh-n-1 > summary [data-action="copy-pointer"]').click(); await status(page, 'Clipboard unavailable or rejected');
           // Textareas normalize CRLF; fallback is visibly manual and does not promise byte fidelity.
           assert.equal(await page.locator('#jh-manual-text').inputValue(), raw.replace(/\r\n/g, '\n'));
    +      await page.keyboard.press('Escape');
    +      await page.waitForFunction(() => document.activeElement.dataset.action === 'copy-pointer');
           await page.locator('#jh-n-1 > summary [data-action="copy-pointer-escaped"]').click();
           await page.waitForFunction(() => document.getElementById('jh-manual-text').value.startsWith('"'));
           assert.equal(await page.locator('#jh-manual-text').inputValue(), '"/~01~1a\\u0000\\u000d\\u000a"');
    +      await page.getByRole('button', {name: 'Close', exact: true}).click();
           assert.equal(await page.locator('#jh-n-1').evaluate(n => n.open), true);
           await page.evaluate(() => Object.defineProperty(navigator, 'clipboard', {configurable: true, value: undefined}));
           await page.getByRole('button', {name: 'Copy JSON', exact: true}).click(); await status(page, 'Clipboard unavailable or rejected');
    @@ -187,7 +194,7 @@ async function main() {
           assert.equal(await page.evaluate(() => typeof window.pwned), 'undefined');
           const download = await downloaded(page, '[data-action="download-json"]');
           assert.deepEqual(download.bytes, fs.readFileSync(path.join(fixtures, 'hostile.json')));
    -      assert.equal(await page.locator('code[data-search]').textContent(), fs.readFileSync(path.join(fixtures, 'hostile.json'), 'utf8'));
    +      assert.ok((await page.locator('#jh-tree [data-search]').textContent()).includes('\\u2028\\u2029\\u0000'));
         });
         await run('mobile-layout', 'exact', 'http', true, async page => {
           assert.ok(await page.getByRole('button', {name: 'Download JSON (UTF-8)', exact: true}).isVisible());
    @@ -201,13 +208,15 @@ async function main() {
           if (engine === 'chromium') await page.screenshot({path: path.join(out, engine + '-desktop.png'), fullPage: true, caret: 'initial'});
         }, {viewport: {width: 390, height: 844}});
         await run('complete-print', 'deep', 'http', true, async page => {
    -      assert.equal(await page.locator('details[open]').count(), 0);
    +      assert.equal(await page.locator('#jh-tree details[open]').count(), 0);
           await page.emulateMedia({media: 'print'});
           assert.equal(await page.locator('#jh-tree').isVisible(), false);
           assert.equal(await page.locator('.print-json').isVisible(), true);
    -      assert.equal(await page.locator('.print-json code').textContent(), fs.readFileSync(path.join(fixtures, 'deep.json'), 'utf8'));
    +      assert.equal(await page.locator('.print-json .node').count(), records.find(r => r.name === 'deep').nodes);
    +      assert.ok((await page.locator('.print-json').textContent()).includes('last-sentinel'));
    +      assert.equal(await page.locator('.print-json details:not([open])').count(), 0);
           if (engine === 'chromium') await page.pdf({path: path.join(out, 'chromium-complete.pdf'), format: 'A4'});
    -      assert.equal(await page.locator('details[open]').count(), 0);
    +      assert.equal(await page.locator('#jh-tree details[open]').count(), 0);
         });
         await run('query-limit-no-outline', 'zero-query', 'http', true, async page => {
           await page.locator('#jh-query').fill('é'); await status(page, 'exceeds the UTF-8 byte limit');
    @@ -255,6 +264,73 @@ async function main() {
           await page.emulateMedia({media: 'print'});
           assert.ok(await page.locator('#jh-a-4').isVisible());
         });
    +    await run('tabs-keyboard', 'previews', 'http', true, async page => {
    +      assert.equal(await page.getByRole('tab', {name: 'Structured', exact: true}).getAttribute('aria-selected'), 'true');
    +      await page.getByRole('tab', {name: 'Structured', exact: true}).focus(); await page.keyboard.press('ArrowRight');
    +      assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-tab-syntax');
    +      assert.equal(await page.locator('#jh-syntax').isVisible(), true);
    +      await page.keyboard.press('Home'); assert.equal(await page.locator('#jh-structured').isVisible(), true);
    +      await page.keyboard.press('End'); assert.equal(await page.locator('#jh-syntax').isVisible(), true);
    +    });
    +    await run('specialized-previews', 'previews', 'http', true, async page => {
    +      assert.equal(await page.locator('#jh-tree img').count(), 4);
    +      for (const image of await page.locator('#jh-tree img').all()) assert.equal(await image.evaluate(n => n.complete && n.naturalWidth === 4 && n.naturalHeight === 4), true);
    +      assert.equal(await page.locator('#jh-tree svg.color-swatch').count(), 2);
    +      assert.equal(await page.locator('#jh-tree math').count(), 1);
    +      assert.equal(await page.locator('#jh-tree math msubsup').count(), 1);
    +      assert.ok((await page.locator('#jh-tree').textContent()).includes('$x^12'));
    +      assert.equal(await page.locator('#jh-tree a[href^="https:"]').count(), 0);
    +      const ids = await page.locator('[id]').evaluateAll(nodes => nodes.map(n => n.id)); assert.equal(new Set(ids).size, ids.length);
    +      const download = await downloaded(page, '[data-action="download-json"]'); assert.deepEqual(download.bytes, fs.readFileSync(path.join(fixtures, 'previews.json')));
    +    });
    +    for (const protocol of ['file', 'http']) await run('static-previews-' + protocol, 'static-inspection', protocol, false, async page => {
    +      assert.equal(await page.locator('script').count(), 0); assert.equal(await page.locator('button,input').count(), 0);
    +      assert.equal(await page.locator('#jh-tree math').count(), 1);
    +      assert.equal(await page.locator('#jh-tree img').count(), 4);
    +      await page.locator('#jh-tree .original-value').last().locator('summary').click();
    +      assert.ok((await page.locator('#jh-tree').textContent()).includes('ORIGINAL-TAIL-ONLY'));
    +      assert.equal(await page.locator('#jh-tree details.node:not([open])').count(), 0);
    +      const d=await downloaded(page,'.source-download a');assert.deepEqual(d.bytes,fs.readFileSync(path.join(fixtures,'static-inspection.json')));
    +    });
    +    await run('no-script-both-views-math', 'previews', 'file', false, async page => {
    +      assert.equal(await page.locator('#jh-structured').isVisible(), true); assert.equal(await page.locator('#jh-syntax').isVisible(), true);
    +      assert.equal(await page.locator('#jh-tree math').isVisible(), true);
    +      assert.equal(await page.locator('#jh-syntax pre code').textContent(), fs.readFileSync(path.join(fixtures, 'previews.json'), 'utf8'));
    +    });
    +    await run('cutoff-native-original', 'inspection-long', 'file', false, async page => {
    +      assert.equal(await page.locator('#jh-tree .display-value').textContent(), '"' + '😀'.repeat(512) + '"');
    +      assert.equal(await page.locator('#jh-tree .original-value code').isVisible(), false);
    +      await page.locator('#jh-tree .original-value summary').click();
    +      assert.ok((await page.locator('#jh-tree .original-value code').textContent()).endsWith('ORIGINAL-TAIL-ONLY"'));
    +      assert.equal(await page.locator('#jh-tree .original-value code').isVisible(), true);
    +    });
    +    await run('search-displayed-preview-boundary', 'inspection-long', 'http', true, async page => {
    +      await page.locator('#jh-query').fill('ORIGINAL-TAIL-ONLY'); await status(page, '0 matching nodes');
    +      await page.getByRole('tab', {name: 'JSON source', exact: true}).click(); await status(page, '1 matching nodes');
    +      await page.locator('#jh-query').press('Enter'); assert.equal(await page.evaluate(() => document.activeElement.id), 'jh-syntax');
    +      await page.locator('#jh-query').focus(); await page.keyboard.press('Escape'); await status(page, '0 matching nodes');
    +    });
    +    await run('previews-320-css-200-percent', 'previews', 'http', true, async page => {
    +      assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
    +      await page.evaluate(() => {document.body.style.zoom = '2';});
    +      assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
    +      if (engine === 'chromium') await page.screenshot({path: path.join(out, 'chromium-previews-320-zoom.png'), fullPage: true, caret: 'initial'});
    +      await page.evaluate(() => {document.body.style.zoom = '';});
    +      if (engine === 'chromium') await page.screenshot({path: path.join(out, 'chromium-previews-320.png'), fullPage: true, caret: 'initial'});
    +    }, {viewport: {width: 320, height: 900}});
    +    await run('complete-structured-print-previews', 'complete', 'http', false, async page => {
    +      await page.emulateMedia({media: 'print'});
    +      assert.equal(await page.locator('#jh-tree').isVisible(), false); assert.equal(await page.locator('.print-json').isVisible(), true);
    +      assert.equal(await page.locator('.print-json details:not([open])').count(), 0);
    +      assert.equal(await page.locator('.print-json math').count(), 1); assert.equal(await page.locator('.print-json img').count(), 4);
    +      assert.ok((await page.locator('.print-json').textContent()).includes('😀'.repeat(513) + 'ORIGINAL-TAIL-ONLY'));
    +      if (engine === 'chromium') await page.pdf({path: path.join(out, 'chromium-previews-complete.pdf'), format: 'A4'});
    +    });
    +    await run('explicit-syntax-no-enrichment', 'syntax', 'file', true, async page => {
    +      assert.equal(await page.locator('#jh-syntax').isVisible(), true);
    +      assert.equal(await page.locator('math,img,svg.color-swatch').count(), 0);
    +      assert.equal(await page.locator('#jh-syntax pre code').textContent(), fs.readFileSync(path.join(fixtures, 'syntax.json'), 'utf8'));
    +    });
         if (engine === 'chromium') await run('native-browser-clipboard', 'exact', 'http', true, async (page, context) => {
           await context.grantPermissions(['clipboard-read', 'clipboard-write']);
           // Keep previous clipboard text in browser memory only; never put it in receipts.
    @@ -273,12 +349,12 @@ async function main() {
         });
         await browser.close();
       }
    -  receipt.expectedCases = 46; receipt.passedCases = receipt.cases.filter(c => c.status === 'passed').length;
    +  receipt.expectedCases = 76; receipt.passedCases = receipt.cases.filter(c => c.status === 'passed').length;
       receipt.unexecutedCases = receipt.engines.reduce((sum, e) => sum + (e.unexecutedCases || 0), 0);
       if (receipt.unexecutedCases) {receipt.status = 'blocked'; save(); console.error('Browser startup blocked; unexecuted cases=' + receipt.unexecutedCases); process.exitCode = 2; return;}
    -  assert.equal(receipt.cases.length, 46);
    -  assert.equal(receipt.cases.filter(c => c.status === 'passed').length, 46);
    -  receipt.status = 'passed'; receipt.expectedCases = 46; receipt.passedCases = 46; receipt.unexpectedSkips = 0;
    -  save(); console.log('JSONHTML_BROWSER passed engines=3 cases=46 skips=0');
    +  assert.equal(receipt.cases.length, 76);
    +  assert.equal(receipt.cases.filter(c => c.status === 'passed').length, 76);
    +  receipt.status = 'passed'; receipt.expectedCases = 76; receipt.passedCases = 76; receipt.unexpectedSkips = 0;
    +  save(); console.log('JSONHTML_BROWSER passed engines=3 cases=76 skips=0');
     }
     main().catch(error => {receipt.status = 'failed'; receipt.error = error.stack; save(); console.error(error); process.exitCode = 1;}).finally(async () => {await Promise.all(activeBrowsers.map(b => b.close().catch(() => {}))); for (const data of isolatedDataDirectories) fs.rmSync(data, {recursive: true, force: true}); server.close();});
    
    From cac8f25fab237f0a3f451bbf51043289df79831b Mon Sep 17 00:00:00 2001
    From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com>
    Date: Thu, 15 Oct 2026 00:36:11 +0200
    Subject: [PATCH 45/59] Qualify indexed scalar lists and native preview
     integration boundaries
    
    ---
     Scripts/ci.py                                 |    2 +-
     Scripts/wasm_support.py                       |   11 +-
     Sources/JSONHTML/Assets/Interactive.css       |    1 +
     Sources/JSONHTML/JSONHTMLInline.swift         | 1100 +++++++++--------
     .../JSONHTML/JSONHTMLInteractiveAssets.swift  |    3 +-
     Sources/JSONHTML/JSONHTMLMiniMath.swift       |  649 +++++-----
     Sources/JSONHTML/JSONHTMLPreparedPolicy.swift |   36 +-
     Sources/JSONHTML/JSONHTMLPreviewOptions.swift |  230 ++--
     Sources/JSONHTML/JSONHTMLPreviewPlan.swift    |  395 +++---
     .../JSONHTML/JSONHTMLPreviewPrimitives.swift  |  591 ++++-----
     Sources/JSONHTML/JSONHTMLRaster.swift         |  513 ++++----
     Sources/JSONHTML/JSONHTMLStructured.swift     |  917 +++++++-------
     Tests/JSONCLITests/HTMLPreviewTests.swift     |   99 +-
     Tests/JSONHTMLTests/StructuredPDFTests.swift  |  168 +--
     .../StructuredPreviewTests.swift              |  974 ++++++++-------
     Tools/JSONHTMLBrowser/browser.cjs             |    2 +-
     16 files changed, 2921 insertions(+), 2770 deletions(-)
    
    diff --git a/Scripts/ci.py b/Scripts/ci.py
    index e67f907..51723c3 100644
    --- a/Scripts/ci.py
    +++ b/Scripts/ci.py
    @@ -132,7 +132,7 @@ def check_result(command, code, log, events=None):
             passed = data['observed'] == data['passed'] == 53 and data['failed'] == 0
         if command[:3] == ['python3', '-B', 'Scripts/qualify_cli_integration.py'] and passed:
             d = json.loads(log.strip().splitlines()[-1])
    -        passed = d['status'] == 'passed' and (d['observed'], d['passed'], d['failed']) == (36, 36, 0)
    +        passed = d['status'] == 'passed' and (d['observed'], d['passed'], d['failed']) == (37, 37, 0)
         if command[:3] == ['python3', '-B', 'Scripts/qualify_cli_limits.py'] and passed:
             data = json.loads(log.strip().splitlines()[-1])
             passed = data['observed'] == data['passed'] == 33 and data['failed'] == 0 and data['metrics'] == 16
    diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py
    index 796ff43..cfab492 100644
    --- a/Scripts/wasm_support.py
    +++ b/Scripts/wasm_support.py
    @@ -53,7 +53,16 @@ def binary(scratch, config, name):
             run('compiler', [swift, '--version'], lambda c, t: c == 0 and 'swift-6.4-RELEASE' in t)
             run('sdk', [swift, 'sdk', 'list'], lambda c, t: c == 0 and SDK in t.splitlines())
             run('wasmkit-version', [wasmkit, '--version']); run('node-version', [node, '--version'])
    -        manifest = json.loads(run('root-manifest', [swift, 'package', 'dump-package']))
    +        manifest_text = run('root-manifest', [swift, 'package', 'dump-package'])
    +        # SwiftPM can write its lock-wait diagnostic before JSON on the merged log.
    +        # Accept only that precise preamble; the decoded manifest and trailing bytes
    +        # remain exact. The full diagnostic is retained in the command receipt.
    +        start = manifest_text.find('{')
    +        if start < 0: raise ValueError('Missing dump-package JSON')
    +        preamble = manifest_text[:start].strip()
    +        if preamble and not re.fullmatch(r"Another instance of SwiftPM .*waiting until that process has finished execution\.\.\.", preamble):
    +            raise ValueError('Unexpected manifest preamble: ' + preamble)
    +        manifest = json.loads(manifest_text[start:])
             if manifest['dependencies'] or sorted(p['name'] for p in manifest['products']) != ['JSON', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming', 'swift-json']:
                 raise ValueError('Changed public products/dependencies')
             def invoke(engine, path, mode='command', arguments=(), repo=True, resources=True):
    diff --git a/Sources/JSONHTML/Assets/Interactive.css b/Sources/JSONHTML/Assets/Interactive.css
    index 20871c9..4d3dce5 100644
    --- a/Sources/JSONHTML/Assets/Interactive.css
    +++ b/Sources/JSONHTML/Assets/Interactive.css
    @@ -17,3 +17,4 @@
     .rich .color-swatch{max-width:100%;height:auto;aspect-ratio:5/2}.rich .view-tabs{flex-wrap:wrap}.rich .pointer-actions{max-width:100%;min-width:0}.rich button,.rich input,.rich select{max-width:100%;min-width:0;box-sizing:border-box}.rich button{white-space:normal;overflow-wrap:anywhere}
     
     @media print{.rich .source-download{display:none!important}}
    +.rich .scalar-item{display:flex;align-items:flex-start;gap:.55rem;min-width:0;padding:.3rem 0;border-top:1px solid var(--line)}.rich .scalar-item>.index-label{flex:none;min-width:2.5rem;margin:0;padding-top:.2rem}.rich .scalar-item>.node{min-width:0;flex:1}
    diff --git a/Sources/JSONHTML/JSONHTMLInline.swift b/Sources/JSONHTML/JSONHTMLInline.swift
    index 16974c4..23af805 100644
    --- a/Sources/JSONHTML/JSONHTMLInline.swift
    +++ b/Sources/JSONHTML/JSONHTMLInline.swift
    @@ -2,581 +2,609 @@
     import JSON
     
     package enum JSONHTMLInlineToken {
    -  case text(Range)
    -  case escaped(Int)
    -  case open(String)
    -  case close(String)
    -  case math(JSONHTMLMathTree)
    -  case link(String, [JSONHTMLInlineToken], Bool)
    -  case image(JSONHTMLRaster, String)
    -  case imageReference(String, String)
    +    case text(Range)
    +    case escaped(Int)
    +    case open(String)
    +    case close(String)
    +    case math(JSONHTMLMathTree)
    +    case link(String, [JSONHTMLInlineToken], Bool)
    +    case image(JSONHTMLRaster, String)
    +    case imageReference(String, String)
     }
     package struct JSONHTMLInlinePlan {
    -  package let source: [Unicode.Scalar]
    -  package let tokens: [JSONHTMLInlineToken]
    -  package let formatted: Bool
    -  package let reservation: UInt64
    -  package func emit(output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws {
    -    enum Step {
    -      case token(JSONHTMLInlineToken)
    -      case closeLink
    -    }
    -    var stack: [Step] = []
    -    func push(_ v: Step, _ m: inout JSONWorkMeter) throws {
    -      try m.retain(64)
    -      stack.append(v)
    -    }
    -    for t in tokens.reversed() {
    -      try meter.work()
    -      try push(.token(t), &meter)
    -    }
    -    while let step = stack.popLast() {
    -      try meter.release(64)
    -      try meter.work()
    -      switch step {
    -      case .closeLink: try output.append("", meter: &meter)
    -      case .token(let t):
    -        switch t {
    -        case .text(let r):
    -          for i in r {
    +    package let source: [Unicode.Scalar]
    +    package let tokens: [JSONHTMLInlineToken]
    +    package let formatted: Bool
    +    package let reservation: UInt64
    +    package func emit(output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws {
    +        enum Step {
    +            case token(JSONHTMLInlineToken)
    +            case closeLink
    +        }
    +        var stack: [Step] = []
    +        func push(_ v: Step, _ m: inout JSONWorkMeter) throws {
    +            try m.retain(64)
    +            stack.append(v)
    +        }
    +        for t in tokens.reversed() {
                 try meter.work()
    -            try JSONHTMLDisplay.text(
    -              String(source[i]), quoted: false, output: &output, meter: &meter)
    -          }
    -        case .escaped(let i):
    -          try JSONHTMLDisplay.text(String(source[i]), quoted: false, output: &output, meter: &meter)
    -        case .open(let tag):
    -          try output.addDOM(2, meter: &meter)
    -          try output.append("<" + tag + ">", meter: &meter)
    -        case .close(let tag): try output.append("", meter: &meter)
    -        case .math(let tree): try tree.emit(output: &output, meter: &meter)
    -        case .link(let destination, let label, let active):
    -          try output.addDOM(4, meter: &meter)
    -          if active {
    -            try output.append(
    -              "", meter: &meter)
    -            try push(.closeLink, &meter)
    -          } else {
    -            try output.append("", meter: &meter)
    -            try JSONHTMLDisplay.text(destination, quoted: false, output: &output, meter: &meter)
    -            try output.append(" · ", meter: &meter)
    -          }
    -          for v in label.reversed() {
    +            try push(.token(t), &meter)
    +        }
    +        while let step = stack.popLast() {
    +            try meter.release(64)
                 try meter.work()
    -            try push(.token(v), &meter)
    -          }
    -        case .image(let image, let alt):
    -          try image.emit(alt: alt, output: &output, meter: &meter)
    -          try output.addDOM(2, meter: &meter)
    -          try output.append("", meter: &meter)
    -          try JSONHTMLDisplay.text(alt, quoted: false, output: &output, meter: &meter)
    -          try output.append("", meter: &meter)
    -        case .imageReference(let alt, let destination):
    -          try output.addDOM(2, meter: &meter)
    -          try output.append(
    -            "Image reference (not loaded): ", meter: &meter)
    -          try JSONHTMLDisplay.text(alt, quoted: false, output: &output, meter: &meter)
    -          try output.append(" · ", meter: &meter)
    -          try JSONHTMLDisplay.text(destination, quoted: false, output: &output, meter: &meter)
    -          try output.append("", meter: &meter)
    +            switch step {
    +            case .closeLink: try output.append("", meter: &meter)
    +            case .token(let t):
    +                switch t {
    +                case .text(let r):
    +                    for i in r {
    +                        try meter.work()
    +                        try JSONHTMLDisplay.text(
    +                            String(source[i]), quoted: false, output: &output, meter: &meter)
    +                    }
    +                case .escaped(let i):
    +                    try JSONHTMLDisplay.text(
    +                        String(source[i]), quoted: false, output: &output, meter: &meter)
    +                case .open(let tag):
    +                    try output.addDOM(2, meter: &meter)
    +                    try output.append("<" + tag + ">", meter: &meter)
    +                case .close(let tag): try output.append("", meter: &meter)
    +                case .math(let tree): try tree.emit(output: &output, meter: &meter)
    +                case .link(let destination, let label, let active):
    +                    try output.addDOM(4, meter: &meter)
    +                    if active {
    +                        try output.append(
    +                            "", meter: &meter)
    +                        try push(.closeLink, &meter)
    +                    } else {
    +                        try output.append("", meter: &meter)
    +                        try JSONHTMLDisplay.text(
    +                            destination, quoted: false, output: &output, meter: &meter)
    +                        try output.append(" · ", meter: &meter)
    +                    }
    +                    for v in label.reversed() {
    +                        try meter.work()
    +                        try push(.token(v), &meter)
    +                    }
    +                case .image(let image, let alt):
    +                    try image.emit(alt: alt, output: &output, meter: &meter)
    +                    try output.addDOM(2, meter: &meter)
    +                    try output.append("", meter: &meter)
    +                    try JSONHTMLDisplay.text(alt, quoted: false, output: &output, meter: &meter)
    +                    try output.append("", meter: &meter)
    +                case .imageReference(let alt, let destination):
    +                    try output.addDOM(2, meter: &meter)
    +                    try output.append(
    +                        "Image reference (not loaded): ", meter: &meter)
    +                    try JSONHTMLDisplay.text(alt, quoted: false, output: &output, meter: &meter)
    +                    try output.append(" · ", meter: &meter)
    +                    try JSONHTMLDisplay.text(
    +                        destination, quoted: false, output: &output, meter: &meter)
    +                    try output.append("", meter: &meter)
    +                }
    +            }
             }
    -      }
         }
    -  }
     }
     private struct InlineFrame { let marker: Int, start: Int, token: Int }
     package enum JSONHTMLInlineScanner {
    -  package static func scan(
    -    _ source: String, options: JSONHTMLRenderingOptions, ledger: inout JSONHTMLMediaLedger,
    -    meter: inout JSONWorkMeter
    -  ) throws -> JSONHTMLInlinePlan {
    -    let entry = meter.used(.retainedBytes)
    -    var chars: [Unicode.Scalar] = []
    -    for c in source.unicodeScalars {
    -      try meter.work()
    -      try meter.retain(4)
    -      chars.append(c)
    -    }
    -    let beforeBody = meter.used(.retainedBytes)
    -    let previousLedger = ledger
    -    let result: ([JSONHTMLInlineToken], Bool)
    -    do {
    -      result = try scanBody(
    -        chars, options: options, labelOnly: false, ledger: &ledger, meter: &meter)
    -    } catch is JSONHTMLPreviewRejection {
    -      try meter.release(meter.used(.retainedBytes) - beforeBody)
    -      ledger = previousLedger
    -      try meter.retain(64)
    -      result = ([.text(0.. ([JSONHTMLInlineToken], Bool) {
    -    var tokens: [JSONHTMLInlineToken] = []
    -    var frames: [InlineFrame] = []
    -    var i = 0
    -    var formatted = false
    -    func append(_ t: JSONHTMLInlineToken, _ m: inout JSONWorkMeter) throws {
    -      try m.work()
    -      guard UInt64(tokens.count) < options.previewPolicy.maxInlineTokens else {
    -        throw JSONHTMLPreviewRejection.eligibility
    -      }
    -      try m.retain(64)
    -      tokens.append(t)
    -    }
    -    func plain(_ range: Range, _ m: inout JSONWorkMeter) throws {
    -      if !range.isEmpty { try append(.text(range), &m) }
    -    }
    -    func isSpace(_ c: Unicode.Scalar) -> Bool { c == " " || c == "\n" || c == "\r" || c == "\t" }
    -    func slice(_ r: Range, _ m: inout JSONWorkMeter) throws -> String {
    -      var s = ""
    -      for n in r {
    -        try m.work()
    -        for _ in chars[n].utf8 {
    -          try m.work()
    -          try m.retain(1)
    +    package static func scan(
    +        _ source: String, options: JSONHTMLRenderingOptions, ledger: inout JSONHTMLMediaLedger,
    +        meter: inout JSONWorkMeter
    +    ) throws -> JSONHTMLInlinePlan {
    +        let entry = meter.used(.retainedBytes)
    +        var chars: [Unicode.Scalar] = []
    +        for c in source.unicodeScalars {
    +            try meter.work()
    +            try meter.retain(4)
    +            chars.append(c)
             }
    -        s.unicodeScalars.append(chars[n])
    -      }
    -      return s
    +        let beforeBody = meter.used(.retainedBytes)
    +        let previousLedger = ledger
    +        let result: ([JSONHTMLInlineToken], Bool)
    +        do {
    +            result = try scanBody(
    +                chars, options: options, labelOnly: false, ledger: &ledger, meter: &meter)
    +        } catch is JSONHTMLPreviewRejection {
    +            try meter.release(meter.used(.retainedBytes) - beforeBody)
    +            ledger = previousLedger
    +            try meter.retain(64)
    +            result = ([.text(0.. ([JSONHTMLInlineToken], Bool) {
    +        var tokens: [JSONHTMLInlineToken] = []
    +        var frames: [InlineFrame] = []
    +        var i = 0
    +        var formatted = false
    +        func append(_ t: JSONHTMLInlineToken, _ m: inout JSONWorkMeter) throws {
    +            try m.work()
    +            guard UInt64(tokens.count) < options.previewPolicy.maxInlineTokens else {
    +                throw JSONHTMLPreviewRejection.eligibility
    +            }
    +            try m.retain(64)
    +            tokens.append(t)
             }
    -        listMarker = n + 1 < chars.count && chars[n] == "." && isSpace(chars[n + 1])
    -      }
    -      let indentedBlock =
    -        lineStart
    -        && (chars[i] == "\t"
    -          || i + 3 < chars.count && chars[i] == " " && chars[i + 1] == " " && chars[i + 2] == " "
    -            && chars[i + 3] == " ")
    -      if indentedBlock
    -        || lineStart
    -          && (chars[i] == "#" || chars[i] == ">" || chars[i] == "|"
    -            || ((chars[i] == "-" || chars[i] == "+" || chars[i] == "*") && i + 1 < chars.count
    -              && isSpace(chars[i + 1]))
    -            || listMarker)
    -      {
    -        var end = i
    -        while end < chars.count && chars[end] != "\n" {
    -          try meter.work()
    -          end += 1
    +        func plain(_ range: Range, _ m: inout JSONWorkMeter) throws {
    +            if !range.isEmpty { try append(.text(range), &m) }
             }
    -        try plain(i.. Bool {
    +            c == " " || c == "\n" || c == "\r" || c == "\t"
             }
    -        var at = i + width
    -        var end = chars.count
    -        while at < chars.count {
    -          try meter.work()
    -          if chars[at] == marker {
    -            var run = 1
    -            while at + run < chars.count && chars[at + run] == marker {
    -              try meter.work()
    -              run += 1
    -            }
    -            if run == width && (!(lineStart && width >= 3) || at == 0 || chars[at - 1] == "\n") {
    -              end = at + run
    -              break
    +        func slice(_ r: Range, _ m: inout JSONWorkMeter) throws -> String {
    +            var s = ""
    +            for n in r {
    +                try m.work()
    +                for _ in chars[n].utf8 {
    +                    try m.work()
    +                    try m.retain(1)
    +                }
    +                s.unicodeScalars.append(chars[n])
                 }
    -            at += run
    -          } else {
    -            at += 1
    -          }
    +            return s
             }
    -        try plain(i.." || chars[i] == "|"
    +                        || ((chars[i] == "-" || chars[i] == "+" || chars[i] == "*")
    +                            && i + 1 < chars.count
    +                            && isSpace(chars[i + 1]))
    +                        || listMarker)
                 {
    -              try append(.math(tree), &meter)
    -              formatted = true
    -            } else {
    -              try plain(start..<(close + 2), &meter)
    +                var end = i
    +                while end < chars.count && chars[end] != "\n" {
    +                    try meter.work()
    +                    end += 1
    +                }
    +                try plain(i.. i + 1 && !isSpace(chars[i + 1]) && !isSpace(chars[close - 1])
    -            && (close + 1 == chars.count || chars[close + 1] != "$")
    -          if valid {
    -            let interior = try slice((i + 1).. 0 {
    -            try meter.work()
    -            let c = chars[at]
    -            if c == "\\", at + 1 < chars.count, "()\\".unicodeScalars.contains(chars[at + 1]) {
    -              at += 1
    -              let c = chars[at]
    -              if UInt64(destination.utf8.count) + UInt64(c.utf8.count)
    -                > options.previewPolicy.maxDestinationUTF8Bytes
    -              {
    -                valid = false
    -              }
    -              if valid {
    -                for _ in c.utf8 {
    -                  try meter.work()
    -                  try meter.retain(1)
    +                let marker = chars[i]
    +                var width = 1
    +                while i + width < chars.count && chars[i + width] == marker {
    +                    try meter.work()
    +                    width += 1
                     }
    -                destination.unicodeScalars.append(c)
    -              }
    -              at += 1
    -              continue
    -            }
    -            if c == "(" {
    -              depth += 1
    -              if UInt64(depth) > options.previewPolicy.maxInlineNesting { valid = false }
    -            }
    -            if c == ")" {
    -              depth -= 1
    -              if depth == 0 {
    -                at += 1
    -                break
    -              }
    +                var at = i + width
    +                var end = chars.count
    +                while at < chars.count {
    +                    try meter.work()
    +                    if chars[at] == marker {
    +                        var run = 1
    +                        while at + run < chars.count && chars[at + run] == marker {
    +                            try meter.work()
    +                            run += 1
    +                        }
    +                        if run == width
    +                            && (!(lineStart && width >= 3) || at == 0 || chars[at - 1] == "\n")
    +                        {
    +                            end = at + run
    +                            break
    +                        }
    +                        at += run
    +                    } else {
    +                        at += 1
    +                    }
    +                }
    +                try plain(i.. options.previewPolicy.maxDestinationUTF8Bytes
    +            if chars[i] == "\\", i + 1 < chars.count,
    +                "\\`*[]()!$~".unicodeScalars.contains(chars[i + 1])
                 {
    -              valid = false
    +                // Parenthesized math is recognized before ordinary punctuation escaping.
    +                if !labelOnly && chars[i + 1] == "(" {
    +                    let start = i
    +                    var at = i + 2
    +                    var close: Int?
    +                    while at + 1 < chars.count && chars[at] != "\n" {
    +                        try meter.work()
    +                        if chars[at] == "\\" && chars[at + 1] == ")" {
    +                            close = at
    +                            break
    +                        }
    +                        at += 1
    +                    }
    +                    if let close {
    +                        let interior = try slice((i + 2).. i + 1 && !isSpace(chars[i + 1]) && !isSpace(chars[close - 1])
    +                        && (close + 1 == chars.count || chars[close + 1] != "$")
    +                    if valid {
    +                        let interior = try slice((i + 1).. 0 {
    +                        try meter.work()
    +                        let c = chars[at]
    +                        if c == "\\", at + 1 < chars.count,
    +                            "()\\".unicodeScalars.contains(chars[at + 1])
    +                        {
    +                            at += 1
    +                            let c = chars[at]
    +                            if UInt64(destination.utf8.count) + UInt64(c.utf8.count)
    +                                > options.previewPolicy.maxDestinationUTF8Bytes
    +                            {
    +                                valid = false
    +                            }
    +                            if valid {
    +                                for _ in c.utf8 {
    +                                    try meter.work()
    +                                    try meter.retain(1)
    +                                }
    +                                destination.unicodeScalars.append(c)
    +                            }
    +                            at += 1
    +                            continue
    +                        }
    +                        if c == "(" {
    +                            depth += 1
    +                            if UInt64(depth) > options.previewPolicy.maxInlineNesting {
    +                                valid = false
    +                            }
    +                        }
    +                        if c == ")" {
    +                            depth -= 1
    +                            if depth == 0 {
    +                                at += 1
    +                                break
    +                            }
    +                        }
    +                        if JSONHTMLDisplay.protected(c) || isSpace(c) || c == "\"" { valid = false }
    +                        if UInt64(destination.utf8.count) + UInt64(c.utf8.count)
    +                            > options.previewPolicy.maxDestinationUTF8Bytes
    +                        {
    +                            valid = false
    +                        }
    +                        if valid {
    +                            for _ in c.utf8 {
    +                                try meter.work()
    +                                try meter.retain(1)
    +                            }
    +                            destination.unicodeScalars.append(c)
    +                        }
    +                        at += 1
    +                    }
    +                    var label: [Unicode.Scalar] = []
    +                    var compound = false
    +                    for n in labelStart.. 0 && !isSpace(chars[i - 1]) {
    +                    while let frame = frames.last, remaining >= frame.marker {
    +                        try meter.work()
    +                        frames.removeLast()
    +                        try meter.release(32)
    +                        try append(.close(frame.marker == 2 ? "strong" : "em"), &meter)
    +                        formatted = true
    +                        remaining -= frame.marker
    +                        i += frame.marker
    +                    }
    +                }
    +                while remaining > 0 {
    +                    let width = remaining >= 2 ? 2 : 1
    +                    if i + remaining < chars.count && !isSpace(chars[i + remaining])
    +                        && UInt64(frames.count) < options.previewPolicy.maxInlineNesting
    +                    {
    +                        try meter.retain(32)
    +                        frames.append(.init(marker: width, start: i, token: tokens.count))
    +                        try append(.open(width == 2 ? "strong" : "em"), &meter)
    +                    } else {
    +                        try plain(i..<(i + width), &meter)
    +                    }
    +                    i += width
    +                    remaining -= width
    +                }
    +                if i == start { i += run }
    +                continue
                 }
    -            formatted = true
    -          } else if valid && !image {
    -            // Exactly one bounded label helper; nested links/images/math are rejected above.
    -            var labelTokens = try scanBody(
    -              label, options: options, labelOnly: true, ledger: &ledger, meter: &meter)
    -            // Label text ranges use the parent's scalar array, preserving one source authority.
    -            for n in labelTokens.0.indices {
    -              try meter.work()
    -              switch labelTokens.0[n] {
    -              case .text(let r):
    -                labelTokens.0[n] = .text((r.lowerBound + labelStart)..<(r.upperBound + labelStart))
    -              case .escaped(let i): labelTokens.0[n] = .escaped(i + labelStart)
    -              default: break
    -              }
    +            let start = i
    +            i += 1
    +            while i < chars.count && !"*[$!\\`\n".unicodeScalars.contains(chars[i]) {
    +                try meter.work()
    +                i += 1
                 }
    -            let active =
    -              try options.linkActivation == .httpAndHTTPS && activeURL(destination, meter: &meter)
    -            try append(.link(destination, labelTokens.0, active), &meter)
    -            formatted = true
    -          } else {
    -            try plain(start.. 0 && !isSpace(chars[i - 1]) {
    -          while let frame = frames.last, remaining >= frame.marker {
    +        // Unmatched frames become literal delimiters, never synthetic repaired closers.
    +        for frame in frames {
                 try meter.work()
    -            frames.removeLast()
    +            tokens[frame.token] = .text(frame.start..<(frame.start + frame.marker))
                 try meter.release(32)
    -            try append(.close(frame.marker == 2 ? "strong" : "em"), &meter)
    -            formatted = true
    -            remaining -= frame.marker
    -            i += frame.marker
    -          }
             }
    -        while remaining > 0 {
    -          let width = remaining >= 2 ? 2 : 1
    -          if i + remaining < chars.count && !isSpace(chars[i + remaining])
    -            && UInt64(frames.count) < options.previewPolicy.maxInlineNesting
    -          {
    -            try meter.retain(32)
    -            frames.append(.init(marker: width, start: i, token: tokens.count))
    -            try append(.open(width == 2 ? "strong" : "em"), &meter)
    -          } else {
    -            try plain(i..<(i + width), &meter)
    -          }
    -          i += width
    -          remaining -= width
    -        }
    -        if i == start { i += run }
    -        continue
    -      }
    -      let start = i
    -      i += 1
    -      while i < chars.count && !"*[$!\\`\n".unicodeScalars.contains(chars[i]) {
    -        try meter.work()
    -        i += 1
    -      }
    -      try plain(start.. Bool {
    -    let entry = meter.used(.retainedBytes)
    -    var bytes: [UInt8] = []
    -    for b in source.utf8 {
    -      try meter.work()
    -      guard b > 32, b < 127, !Array("<>\\\"`{}|".utf8).contains(b) else {
    -        try meter.release(meter.used(.retainedBytes) - entry)
    -        return false
    -      }
    -      try meter.retain(1)
    -      bytes.append(b)
    -    }
    -    defer { bytes = [] }
    -    let result = try urlBytes(bytes, meter: &meter)
    -    try meter.release(UInt64(bytes.count))
    -    return result
    -  }
    -  private static func urlBytes(_ bytes: [UInt8], meter: inout JSONWorkMeter) throws -> Bool {
    -    var start = 0
    -    if bytes.prefix(7).map({ $0 >= 65 && $0 <= 90 ? $0 + 32 : $0 }) == Array("http://".utf8) {
    -      start = 7
    -    } else if bytes.prefix(8).map({ $0 >= 65 && $0 <= 90 ? $0 + 32 : $0 }) == Array("https://".utf8)
    -    {
    -      start = 8
    -    } else {
    -      return false
    -    }
    -    var end = start
    -    while end < bytes.count && ![47, 63, 35].contains(bytes[end]) {
    -      try meter.work()
    -      end += 1
    -    }
    -    guard end > start else { return false }
    -    for b in bytes[start.. Bool {
    +        let entry = meter.used(.retainedBytes)
    +        var bytes: [UInt8] = []
    +        for b in source.utf8 {
    +            try meter.work()
    +            guard b > 32, b < 127, !Array("<>\\\"`{}|".utf8).contains(b) else {
    +                try meter.release(meter.used(.retainedBytes) - entry)
    +                return false
    +            }
    +            try meter.retain(1)
    +            bytes.append(b)
    +        }
    +        defer { bytes = [] }
    +        let result = try urlBytes(bytes, meter: &meter)
    +        try meter.release(UInt64(bytes.count))
    +        return result
         }
    -    let authority = bytes[start.. start + 1 else { return false }
    -      for b in bytes[(start + 1).. Bool {
    +        var start = 0
    +        if bytes.prefix(7).map({ $0 >= 65 && $0 <= 90 ? $0 + 32 : $0 }) == Array("http://".utf8) {
    +            start = 7
    +        } else if bytes.prefix(8).map({ $0 >= 65 && $0 <= 90 ? $0 + 32 : $0 })
    +            == Array("https://".utf8)
    +        {
    +            start = 8
    +        } else {
    +            return false
    +        }
    +        var end = start
    +        while end < bytes.count && ![47, 63, 35].contains(bytes[end]) {
    +            try meter.work()
    +            end += 1
    +        }
    +        guard end > start else { return false }
    +        for b in bytes[start.. start else { return false }
    -      var labelStart = start
    -      for n in start...hostEnd {
    -        try meter.work()
    -        if n == hostEnd || bytes[n] == 46 {
    -          guard n > labelStart, n - labelStart <= 63, bytes[labelStart] != 45, bytes[n - 1] != 45
    -          else { return false }
    -          labelStart = n + 1
    +        let authority = bytes[start.. start + 1 else { return false }
    +            for b in bytes[(start + 1).. start else { return false }
    +            var labelStart = start
    +            for n in start...hostEnd {
    +                try meter.work()
    +                if n == hostEnd || bytes[n] == 46 {
    +                    guard n > labelStart, n - labelStart <= 63, bytes[labelStart] != 45,
    +                        bytes[n - 1] != 45
    +                    else { return false }
    +                    labelStart = n + 1
    +                } else {
    +                    guard
    +                        (48...57).contains(bytes[n]) || (65...90).contains(bytes[n])
    +                            || (97...122).contains(bytes[n]) || bytes[n] == 45
    +                    else { return false }
    +                }
    +            }
             }
    -      }
    -    }
    -    var i = end
    -    while i < bytes.count {
    -      try meter.work()
    -      if bytes[i] == 37 {
    -        guard i + 2 < bytes.count else { return false }
    -        for b in bytes[(i + 1)...(i + 2)] {
    -          guard (48...57).contains(b) || (65...70).contains(b) || (97...102).contains(b) else {
    -            return false
    -          }
    +        var i = end
    +        while i < bytes.count {
    +            try meter.work()
    +            if bytes[i] == 37 {
    +                guard i + 2 < bytes.count else { return false }
    +                for b in bytes[(i + 1)...(i + 2)] {
    +                    guard (48...57).contains(b) || (65...70).contains(b) || (97...102).contains(b)
    +                    else {
    +                        return false
    +                    }
    +                }
    +                i += 3
    +            } else {
    +                i += 1
    +            }
             }
    -        i += 3
    -      } else {
    -        i += 1
    -      }
    +        return true
         }
    -    return true
    -  }
    -  private static func port(_ bytes: ArraySlice, meter: inout JSONWorkMeter) throws -> Bool {
    -    guard !bytes.isEmpty, bytes.count <= 5 else { return false }
    -    var n: UInt64 = 0
    -    for b in bytes {
    -      try meter.work()
    -      guard (48...57).contains(b) else { return false }
    -      n = n * 10 + UInt64(b - 48)
    +    private static func port(_ bytes: ArraySlice, meter: inout JSONWorkMeter) throws -> Bool
    +    {
    +        guard !bytes.isEmpty, bytes.count <= 5 else { return false }
    +        var n: UInt64 = 0
    +        for b in bytes {
    +            try meter.work()
    +            guard (48...57).contains(b) else { return false }
    +            n = n * 10 + UInt64(b - 48)
    +        }
    +        return n > 0 && n <= 65535
         }
    -    return n > 0 && n <= 65535
    -  }
     }
    diff --git a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift
    index 69ec520..56e4c67 100644
    --- a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift
    +++ b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift
    @@ -22,6 +22,7 @@ package enum JSONHTMLInteractiveAssets {
     .rich .color-swatch{max-width:100%;height:auto;aspect-ratio:5/2}.rich .view-tabs{flex-wrap:wrap}.rich .pointer-actions{max-width:100%;min-width:0}.rich button,.rich input,.rich select{max-width:100%;min-width:0;box-sizing:border-box}.rich button{white-space:normal;overflow-wrap:anywhere}
     
     @media print{.rich .source-download{display:none!important}}
    +.rich .scalar-item{display:flex;align-items:flex-start;gap:.55rem;min-width:0;padding:.3rem 0;border-top:1px solid var(--line)}.rich .scalar-item>.index-label{flex:none;min-width:2.5rem;margin:0;padding-top:.2rem}.rich .scalar-item>.node{min-width:0;flex:1}
     """#
         package static let script = #"""
     // SPDX-License-Identifier: Apache-2.0
    @@ -230,6 +231,6 @@ package enum JSONHTMLInteractiveAssets {
       say('Interactive controls ready. Search scope: selected displayed previews or exact JSON source.');
     })();
     """#
    -    package static let styleHash = "f+TaCOj0W3wyCPnJkJlaJPTkXaxYdsglIBjynmeClbg="
    +    package static let styleHash = "i9/4be/W5I4qf52IUKpWqBrCgmLKllGtm6fH0XgpMrI="
         package static let scriptHash = "0E/zm4efMpR/ae2G76+pX+c/zABUnOf3Bmlbu8CFcDc="
     }
    diff --git a/Sources/JSONHTML/JSONHTMLMiniMath.swift b/Sources/JSONHTML/JSONHTMLMiniMath.swift
    index e674666..c70abe1 100644
    --- a/Sources/JSONHTML/JSONHTMLMiniMath.swift
    +++ b/Sources/JSONHTML/JSONHTMLMiniMath.swift
    @@ -4,347 +4,360 @@ import JSON
     // Closed grammar rejection is deliberately distinct from operational failure.
     package enum JSONHTMLPreviewRejection: Error { case syntax, eligibility }
     package struct JSONHTMLMathNode {
    -  package let tag: String
    -  package let text: String
    -  package let children: [Int]
    +    package let tag: String
    +    package let text: String
    +    package let children: [Int]
     }
     package struct JSONHTMLMathTree {
    -  package var nodes: [JSONHTMLMathNode]
    -  package let root: Int
    -  package let reservation: UInt64
    -  package func emit(output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws {
    -    guard nodes.indices.contains(root) else { throw JSONHTMLFailure.invalidConfiguration }
    -    for (i, node) in nodes.enumerated() {
    -      try meter.work()
    -      switch node.tag {
    -      case "mi", "mn", "mo":
    -        guard node.children.isEmpty, !node.text.isEmpty else {
    -          throw JSONHTMLFailure.invalidConfiguration
    -        }
    -      case "mrow":
    -        guard !node.children.isEmpty, node.text.isEmpty else {
    -          throw JSONHTMLFailure.invalidConfiguration
    -        }
    -      case "msqrt":
    -        guard node.children.count == 1, node.text.isEmpty else {
    -          throw JSONHTMLFailure.invalidConfiguration
    +    package var nodes: [JSONHTMLMathNode]
    +    package let root: Int
    +    package let reservation: UInt64
    +    package func emit(output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter) throws {
    +        guard nodes.indices.contains(root) else { throw JSONHTMLFailure.invalidConfiguration }
    +        for (i, node) in nodes.enumerated() {
    +            try meter.work()
    +            switch node.tag {
    +            case "mi", "mn", "mo":
    +                guard node.children.isEmpty, !node.text.isEmpty else {
    +                    throw JSONHTMLFailure.invalidConfiguration
    +                }
    +            case "mrow":
    +                guard !node.children.isEmpty, node.text.isEmpty else {
    +                    throw JSONHTMLFailure.invalidConfiguration
    +                }
    +            case "msqrt":
    +                guard node.children.count == 1, node.text.isEmpty else {
    +                    throw JSONHTMLFailure.invalidConfiguration
    +                }
    +            case "msup", "msub", "mfrac":
    +                guard node.children.count == 2, node.text.isEmpty else {
    +                    throw JSONHTMLFailure.invalidConfiguration
    +                }
    +            case "msubsup":
    +                guard node.children.count == 3, node.text.isEmpty else {
    +                    throw JSONHTMLFailure.invalidConfiguration
    +                }
    +            default: throw JSONHTMLFailure.invalidConfiguration
    +            }
    +            for child in node.children {
    +                try meter.work()
    +                guard child >= 0, child < i else { throw JSONHTMLFailure.invalidConfiguration }
    +            }
             }
    -      case "msup", "msub", "mfrac":
    -        guard node.children.count == 2, node.text.isEmpty else {
    -          throw JSONHTMLFailure.invalidConfiguration
    +        try meter.retain(32)
    +        try output.addDOM(UInt64(nodes.count) * 2 + 2, meter: &meter)
    +        try output.append(
    +            "", meter: &meter)
    +        enum Step {
    +            case node(Int)
    +            case close(String)
             }
    -      case "msubsup":
    -        guard node.children.count == 3, node.text.isEmpty else {
    -          throw JSONHTMLFailure.invalidConfiguration
    +        var stack: [Step] = []
    +        func push(_ s: Step, _ m: inout JSONWorkMeter) throws {
    +            try m.retain(32)
    +            stack.append(s)
             }
    -      default: throw JSONHTMLFailure.invalidConfiguration
    -      }
    -      for child in node.children {
    -        try meter.work()
    -        guard child >= 0, child < i else { throw JSONHTMLFailure.invalidConfiguration }
    -      }
    -    }
    -    try meter.retain(32)
    -    try output.addDOM(UInt64(nodes.count) * 2 + 2, meter: &meter)
    -    try output.append(
    -      "", meter: &meter)
    -    enum Step {
    -      case node(Int)
    -      case close(String)
    -    }
    -    var stack: [Step] = []
    -    func push(_ s: Step, _ m: inout JSONWorkMeter) throws {
    -      try m.retain(32)
    -      stack.append(s)
    -    }
    -    try push(.node(root), &meter)
    -    while let step = stack.popLast() {
    -      try meter.release(32)
    -      try meter.work()
    -      switch step {
    -      case .close(let tag): try output.append("", meter: &meter)
    -      case .node(let i):
    -        let n = nodes[i]
    -        try output.append("<" + n.tag + ">", meter: &meter)
    -        if n.children.isEmpty {
    -          try output.escaped(n.text, meter: &meter)
    -          try output.append("", meter: &meter)
    -        } else {
    -          try push(.close(n.tag), &meter)
    -          for child in n.children.reversed() {
    +        try push(.node(root), &meter)
    +        while let step = stack.popLast() {
    +            try meter.release(32)
                 try meter.work()
    -            try push(.node(child), &meter)
    -          }
    +            switch step {
    +            case .close(let tag): try output.append("", meter: &meter)
    +            case .node(let i):
    +                let n = nodes[i]
    +                try output.append("<" + n.tag + ">", meter: &meter)
    +                if n.children.isEmpty {
    +                    try output.escaped(n.text, meter: &meter)
    +                    try output.append("", meter: &meter)
    +                } else {
    +                    try push(.close(n.tag), &meter)
    +                    for child in n.children.reversed() {
    +                        try meter.work()
    +                        try push(.node(child), &meter)
    +                    }
    +                }
    +            }
             }
    -      }
    +        try output.append("", meter: &meter)
    +        try meter.release(32)
         }
    -    try output.append("", meter: &meter)
    -    try meter.release(32)
    -  }
     }
     private struct MathFrame {
    -  enum Kind {
    -    case expression, group, fractionNumerator
    -    case fractionDenominator(Int)
    -    case radical
    -    case script(Int, Bool, Int?, Int?)
    -  }
    -  let kind: Kind
    -  var items: [Int] = []
    +    enum Kind {
    +        case expression, group, fractionNumerator
    +        case fractionDenominator(Int)
    +        case radical
    +        case script(Int, Bool, Int?, Int?)
    +    }
    +    let kind: Kind
    +    var items: [Int] = []
     }
     package enum JSONHTMLMiniMath {
    -  // Fixed original mappings, not a general macro registry.
    -  private static let greek: [String: String] = [
    -    "alpha": "α", "beta": "β", "gamma": "γ", "delta": "δ", "theta": "θ", "lambda": "λ", "mu": "μ",
    -    "pi": "π", "sigma": "σ", "phi": "φ", "omega": "ω", "Delta": "Δ", "Omega": "Ω",
    -  ]
    -  private static let operators: [String: String] = [
    -    "times": "×", "cdot": "⋅", "pm": "±", "le": "≤", "ge": "≥", "ne": "≠",
    -  ]
    -  package static func parse(
    -    _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter
    -  ) throws -> JSONHTMLMathTree? {
    -    let entry = meter.used(.retainedBytes)
    -    do {
    -      return try parseBody(source, policy: policy, meter: &meter, entry: entry)
    -    } catch is JSONHTMLPreviewRejection {
    -      try meter.release(meter.used(.retainedBytes) - entry)
    -      return nil
    -    } catch { throw error }
    -  }
    -  private static func parseBody(
    -    _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter, entry: UInt64
    -  ) throws -> JSONHTMLMathTree {
    -    var scalars: [Unicode.Scalar] = []
    -    var utf8: UInt64 = 0
    -    for scalar in source.unicodeScalars {
    -      try meter.work()
    -      utf8 += UInt64(scalar.utf8.count)
    -      guard UInt64(scalars.count) < policy.maxMathSourceScalars,
    -        utf8 <= policy.maxMathSourceUTF8Bytes
    -      else { throw JSONHTMLPreviewRejection.eligibility }
    -      try meter.retain(4)
    -      scalars.append(scalar)
    -    }
    -    // One number-token construction plus fixed command/child-call scratch.
    -    try meter.retain(utf8 + 128)
    -    var nodes: [JSONHTMLMathNode] = []
    -    var frames: [MathFrame] = []
    -    var tokenCount: UInt64 = 0
    -    var i = 0
    -    func node(_ tag: String, _ text: String = "", _ children: [Int] = [], _ m: inout JSONWorkMeter)
    -      throws -> Int
    -    {
    -      try m.work()
    -      guard UInt64(nodes.count) < policy.maxMathNodes else {
    -        throw JSONHTMLPreviewRejection.eligibility
    -      }
    -      try m.retain(64 + UInt64(children.count) * 8 + UInt64(text.utf8.count))
    -      nodes.append(.init(tag: tag, text: text, children: children))
    -      return nodes.count - 1
    +    // Fixed original mappings, not a general macro registry.
    +    private static let greek: [String: String] = [
    +        "alpha": "α", "beta": "β", "gamma": "γ", "delta": "δ", "theta": "θ", "lambda": "λ",
    +        "mu": "μ",
    +        "pi": "π", "sigma": "σ", "phi": "φ", "omega": "ω", "Delta": "Δ", "Omega": "Ω",
    +    ]
    +    private static let operators: [String: String] = [
    +        "times": "×", "cdot": "⋅", "pm": "±", "le": "≤", "ge": "≥", "ne": "≠",
    +    ]
    +    package static func parse(
    +        _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter
    +    ) throws -> JSONHTMLMathTree? {
    +        let entry = meter.used(.retainedBytes)
    +        do {
    +            return try parseBody(source, policy: policy, meter: &meter, entry: entry)
    +        } catch is JSONHTMLPreviewRejection {
    +            try meter.release(meter.used(.retainedBytes) - entry)
    +            return nil
    +        } catch { throw error }
         }
    -    func push(_ kind: MathFrame.Kind, _ m: inout JSONWorkMeter) throws {
    -      try m.work()
    -      guard UInt64(frames.count) < policy.maxMathSyntaxDepth else {
    -        throw JSONHTMLPreviewRejection.eligibility
    -      }
    -      try m.retain(64)
    -      frames.append(.init(kind: kind))
    -    }
    -    func append(_ n: Int, _ m: inout JSONWorkMeter) throws {
    -      try m.work()
    -      try m.retain(8)
    -      frames[frames.count - 1].items.append(n)
    -    }
    -    func space(_ m: inout JSONWorkMeter) throws {
    -      while i < scalars.count && scalars[i] == " " {
    -        try m.work()
    -        i += 1
    -      }
    -    }
    -    func token(_ m: inout JSONWorkMeter) throws {
    -      try m.work()
    -      guard tokenCount < policy.maxMathTokens else { throw JSONHTMLPreviewRejection.eligibility }
    -      tokenCount += 1
    -    }
    -    func command(_ m: inout JSONWorkMeter) throws -> String {
    -      try token(&m)
    -      i += 1
    -      var text = ""
    -      while i < scalars.count
    -        && ((65...90).contains(scalars[i].value) || (97...122).contains(scalars[i].value))
    -      {
    -        try m.work()
    -        text.unicodeScalars.append(scalars[i])
    -        i += 1
    -        guard text.utf8.count <= 8 else { throw JSONHTMLPreviewRejection.syntax }
    -      }
    -      return text
    -    }
    -    func simple(_ m: inout JSONWorkMeter, single: Bool = false) throws -> Int {
    -      guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    -      let c = scalars[i]
    -      if c == "\\" {
    -        let name = try command(&m)
    -        guard let text = greek[name] else { throw JSONHTMLPreviewRejection.syntax }
    -        return try node("mi", text, [], &m)
    -      }
    -      if (48...57).contains(c.value) {
    -        try token(&m)
    -        var text = ""
    -        text.unicodeScalars.append(c)
    -        i += 1
    -        if !single {
    -          while i < scalars.count && (48...57).contains(scalars[i].value) {
    +    private static func parseBody(
    +        _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter, entry: UInt64
    +    ) throws -> JSONHTMLMathTree {
    +        var scalars: [Unicode.Scalar] = []
    +        var utf8: UInt64 = 0
    +        for scalar in source.unicodeScalars {
    +            try meter.work()
    +            utf8 += UInt64(scalar.utf8.count)
    +            guard UInt64(scalars.count) < policy.maxMathSourceScalars,
    +                utf8 <= policy.maxMathSourceUTF8Bytes
    +            else { throw JSONHTMLPreviewRejection.eligibility }
    +            try meter.retain(4)
    +            scalars.append(scalar)
    +        }
    +        // One number-token construction plus fixed command/child-call scratch.
    +        try meter.retain(utf8 + 128)
    +        var nodes: [JSONHTMLMathNode] = []
    +        var frames: [MathFrame] = []
    +        var tokenCount: UInt64 = 0
    +        var i = 0
    +        func node(
    +            _ tag: String, _ text: String = "", _ children: [Int] = [], _ m: inout JSONWorkMeter
    +        )
    +            throws -> Int
    +        {
                 try m.work()
    -            text.unicodeScalars.append(scalars[i])
    -            i += 1
    -          }
    -          if i < scalars.count && scalars[i] == "." {
    -            text.append(".")
    -            i += 1
    -            let start = i
    -            while i < scalars.count && (48...57).contains(scalars[i].value) {
    -              try m.work()
    -              text.unicodeScalars.append(scalars[i])
    -              i += 1
    +            guard UInt64(nodes.count) < policy.maxMathNodes else {
    +                throw JSONHTMLPreviewRejection.eligibility
                 }
    -            guard i > start else { throw JSONHTMLPreviewRejection.syntax }
    -          }
    +            try m.retain(64 + UInt64(children.count) * 8 + UInt64(text.utf8.count))
    +            nodes.append(.init(tag: tag, text: text, children: children))
    +            return nodes.count - 1
             }
    -        return try node("mn", text, [], &m)
    -      }
    -      if (65...90).contains(c.value) || (97...122).contains(c.value)
    -        || greek.values.contains(String(c))
    -      {
    -        try token(&m)
    -        i += 1
    -        return try node("mi", String(c), [], &m)
    -      }
    -      throw JSONHTMLPreviewRejection.syntax
    -    }
    -    func scripted(_ base: Int, _ sub: Int?, _ sup: Int?, _ m: inout JSONWorkMeter) throws -> Int {
    -      if let sub, let sup { return try node("msubsup", "", [base, sub, sup], &m) }
    -      if let sub { return try node("msub", "", [base, sub], &m) }
    -      if let sup { return try node("msup", "", [base, sup], &m) }
    -      return base
    -    }
    -    try push(.expression, &meter)
    -    var pending: (base: Int, sub: Int?, sup: Int?)?
    -    while true {
    -      try meter.work()
    -      try space(&meter)
    -      if let p = pending {
    -        if i < scalars.count && (scalars[i] == "_" || scalars[i] == "^") {
    -          let isSub = scalars[i] == "_"
    -          try token(&meter)
    -          i += 1
    -          try space(&meter)
    -          guard isSub ? p.sub == nil : p.sup == nil else { throw JSONHTMLPreviewRejection.syntax }
    -          if i < scalars.count && scalars[i] == "{" {
    -            try token(&meter)
    -            i += 1
    -            pending = nil
    -            try push(.script(p.base, isSub, p.sub, p.sup), &meter)
    -            continue
    -          }
    -          let arg = try simple(&meter, single: true)
    -          // Unbraced alphanumeric runs are ambiguous, never repaired.
    -          if i < scalars.count {
    -            let v = scalars[i].value
    -            if (48...57).contains(v) || (65...90).contains(v) || (97...122).contains(v) {
    -              throw JSONHTMLPreviewRejection.syntax
    +        func push(_ kind: MathFrame.Kind, _ m: inout JSONWorkMeter) throws {
    +            try m.work()
    +            guard UInt64(frames.count) < policy.maxMathSyntaxDepth else {
    +                throw JSONHTMLPreviewRejection.eligibility
                 }
    -          }
    -          pending = (p.base, isSub ? arg : p.sub, isSub ? p.sup : arg)
    -          continue
    +            try m.retain(64)
    +            frames.append(.init(kind: kind))
             }
    -        try append(try scripted(p.base, p.sub, p.sup, &meter), &meter)
    -        pending = nil
    -        continue
    -      }
    -      if i == scalars.count || scalars[i] == "}" {
    -        guard let frame = frames.popLast(), !frame.items.isEmpty else {
    -          throw JSONHTMLPreviewRejection.syntax
    +        func append(_ n: Int, _ m: inout JSONWorkMeter) throws {
    +            try m.work()
    +            try m.retain(8)
    +            frames[frames.count - 1].items.append(n)
             }
    -        try meter.release(64 + UInt64(frame.items.count) * 8)
    -        let result =
    -          frame.items.count == 1 ? frame.items[0] : try node("mrow", "", frame.items, &meter)
    -        switch frame.kind {
    -        case .expression:
    -          guard i == scalars.count, frames.isEmpty else { throw JSONHTMLPreviewRejection.syntax }
    -          try meter.release(UInt64(scalars.count) * 4 + utf8 + 128)
    -          return .init(nodes: nodes, root: result, reservation: meter.used(.retainedBytes) - entry)
    -        case .group:
    -          guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    -          try token(&meter)
    -          i += 1
    -          pending = (result, nil, nil)
    -        case .radical:
    -          guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    -          try token(&meter)
    -          i += 1
    -          pending = (try node("msqrt", "", [result], &meter), nil, nil)
    -        case .fractionNumerator:
    -          guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    -          try token(&meter)
    -          i += 1
    -          try space(&meter)
    -          guard i < scalars.count && scalars[i] == "{" else {
    -            throw JSONHTMLPreviewRejection.syntax
    -          }
    -          try token(&meter)
    -          i += 1
    -          try push(.fractionDenominator(result), &meter)
    -        case .fractionDenominator(let numerator):
    -          guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    -          try token(&meter)
    -          i += 1
    -          pending = (try node("mfrac", "", [numerator, result], &meter), nil, nil)
    -        case .script(let base, let isSub, let sub, let sup):
    -          guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    -          try token(&meter)
    -          i += 1
    -          pending = (base, isSub ? result : sub, isSub ? sup : result)
    +        func space(_ m: inout JSONWorkMeter) throws {
    +            while i < scalars.count && scalars[i] == " " {
    +                try m.work()
    +                i += 1
    +            }
             }
    -        continue
    -      }
    -      let c = scalars[i]
    -      if c == "{" {
    -        try token(&meter)
    -        i += 1
    -        try push(.group, &meter)
    -        continue
    -      }
    -      if c == "\\" {
    -        let name = try command(&meter)
    -        if name == "frac" || name == "sqrt" {
    -          try space(&meter)
    -          guard i < scalars.count && scalars[i] == "{" else {
    +        func token(_ m: inout JSONWorkMeter) throws {
    +            try m.work()
    +            guard tokenCount < policy.maxMathTokens else {
    +                throw JSONHTMLPreviewRejection.eligibility
    +            }
    +            tokenCount += 1
    +        }
    +        func command(_ m: inout JSONWorkMeter) throws -> String {
    +            try token(&m)
    +            i += 1
    +            var text = ""
    +            while i < scalars.count
    +                && ((65...90).contains(scalars[i].value) || (97...122).contains(scalars[i].value))
    +            {
    +                try m.work()
    +                text.unicodeScalars.append(scalars[i])
    +                i += 1
    +                guard text.utf8.count <= 8 else { throw JSONHTMLPreviewRejection.syntax }
    +            }
    +            return text
    +        }
    +        func simple(_ m: inout JSONWorkMeter, single: Bool = false) throws -> Int {
    +            guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    +            let c = scalars[i]
    +            if c == "\\" {
    +                let name = try command(&m)
    +                guard let text = greek[name] else { throw JSONHTMLPreviewRejection.syntax }
    +                return try node("mi", text, [], &m)
    +            }
    +            if (48...57).contains(c.value) {
    +                try token(&m)
    +                var text = ""
    +                text.unicodeScalars.append(c)
    +                i += 1
    +                if !single {
    +                    while i < scalars.count && (48...57).contains(scalars[i].value) {
    +                        try m.work()
    +                        text.unicodeScalars.append(scalars[i])
    +                        i += 1
    +                    }
    +                    if i < scalars.count && scalars[i] == "." {
    +                        text.append(".")
    +                        i += 1
    +                        let start = i
    +                        while i < scalars.count && (48...57).contains(scalars[i].value) {
    +                            try m.work()
    +                            text.unicodeScalars.append(scalars[i])
    +                            i += 1
    +                        }
    +                        guard i > start else { throw JSONHTMLPreviewRejection.syntax }
    +                    }
    +                }
    +                return try node("mn", text, [], &m)
    +            }
    +            if (65...90).contains(c.value) || (97...122).contains(c.value)
    +                || greek.values.contains(String(c))
    +            {
    +                try token(&m)
    +                i += 1
    +                return try node("mi", String(c), [], &m)
    +            }
                 throw JSONHTMLPreviewRejection.syntax
    -          }
    -          try token(&meter)
    -          i += 1
    -          try push(name == "frac" ? .fractionNumerator : .radical, &meter)
    -          continue
             }
    -        if let g = greek[name] {
    -          pending = (try node("mi", g, [], &meter), nil, nil)
    -          continue
    +        func scripted(_ base: Int, _ sub: Int?, _ sup: Int?, _ m: inout JSONWorkMeter) throws -> Int
    +        {
    +            if let sub, let sup { return try node("msubsup", "", [base, sub, sup], &m) }
    +            if let sub { return try node("msub", "", [base, sub], &m) }
    +            if let sup { return try node("msup", "", [base, sup], &m) }
    +            return base
             }
    -        if let op = operators[name] {
    -          try append(try node("mo", op, [], &meter), &meter)
    -          continue
    +        try push(.expression, &meter)
    +        var pending: (base: Int, sub: Int?, sup: Int?)?
    +        while true {
    +            try meter.work()
    +            try space(&meter)
    +            if let p = pending {
    +                if i < scalars.count && (scalars[i] == "_" || scalars[i] == "^") {
    +                    let isSub = scalars[i] == "_"
    +                    try token(&meter)
    +                    i += 1
    +                    try space(&meter)
    +                    guard isSub ? p.sub == nil : p.sup == nil else {
    +                        throw JSONHTMLPreviewRejection.syntax
    +                    }
    +                    if i < scalars.count && scalars[i] == "{" {
    +                        try token(&meter)
    +                        i += 1
    +                        pending = nil
    +                        try push(.script(p.base, isSub, p.sub, p.sup), &meter)
    +                        continue
    +                    }
    +                    let arg = try simple(&meter, single: true)
    +                    // Unbraced alphanumeric runs are ambiguous, never repaired.
    +                    if i < scalars.count {
    +                        let v = scalars[i].value
    +                        if (48...57).contains(v) || (65...90).contains(v) || (97...122).contains(v)
    +                        {
    +                            throw JSONHTMLPreviewRejection.syntax
    +                        }
    +                    }
    +                    pending = (p.base, isSub ? arg : p.sub, isSub ? p.sup : arg)
    +                    continue
    +                }
    +                try append(try scripted(p.base, p.sub, p.sup, &meter), &meter)
    +                pending = nil
    +                continue
    +            }
    +            if i == scalars.count || scalars[i] == "}" {
    +                guard let frame = frames.popLast(), !frame.items.isEmpty else {
    +                    throw JSONHTMLPreviewRejection.syntax
    +                }
    +                try meter.release(64 + UInt64(frame.items.count) * 8)
    +                let result =
    +                    frame.items.count == 1
    +                    ? frame.items[0] : try node("mrow", "", frame.items, &meter)
    +                switch frame.kind {
    +                case .expression:
    +                    guard i == scalars.count, frames.isEmpty else {
    +                        throw JSONHTMLPreviewRejection.syntax
    +                    }
    +                    try meter.release(UInt64(scalars.count) * 4 + utf8 + 128)
    +                    return .init(
    +                        nodes: nodes, root: result, reservation: meter.used(.retainedBytes) - entry)
    +                case .group:
    +                    guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    +                    try token(&meter)
    +                    i += 1
    +                    pending = (result, nil, nil)
    +                case .radical:
    +                    guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    +                    try token(&meter)
    +                    i += 1
    +                    pending = (try node("msqrt", "", [result], &meter), nil, nil)
    +                case .fractionNumerator:
    +                    guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    +                    try token(&meter)
    +                    i += 1
    +                    try space(&meter)
    +                    guard i < scalars.count && scalars[i] == "{" else {
    +                        throw JSONHTMLPreviewRejection.syntax
    +                    }
    +                    try token(&meter)
    +                    i += 1
    +                    try push(.fractionDenominator(result), &meter)
    +                case .fractionDenominator(let numerator):
    +                    guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    +                    try token(&meter)
    +                    i += 1
    +                    pending = (try node("mfrac", "", [numerator, result], &meter), nil, nil)
    +                case .script(let base, let isSub, let sub, let sup):
    +                    guard i < scalars.count else { throw JSONHTMLPreviewRejection.syntax }
    +                    try token(&meter)
    +                    i += 1
    +                    pending = (base, isSub ? result : sub, isSub ? sup : result)
    +                }
    +                continue
    +            }
    +            let c = scalars[i]
    +            if c == "{" {
    +                try token(&meter)
    +                i += 1
    +                try push(.group, &meter)
    +                continue
    +            }
    +            if c == "\\" {
    +                let name = try command(&meter)
    +                if name == "frac" || name == "sqrt" {
    +                    try space(&meter)
    +                    guard i < scalars.count && scalars[i] == "{" else {
    +                        throw JSONHTMLPreviewRejection.syntax
    +                    }
    +                    try token(&meter)
    +                    i += 1
    +                    try push(name == "frac" ? .fractionNumerator : .radical, &meter)
    +                    continue
    +                }
    +                if let g = greek[name] {
    +                    pending = (try node("mi", g, [], &meter), nil, nil)
    +                    continue
    +                }
    +                if let op = operators[name] {
    +                    try append(try node("mo", op, [], &meter), &meter)
    +                    continue
    +                }
    +                throw JSONHTMLPreviewRejection.syntax
    +            }
    +            if "+-=(),/×⋅±≤≥≠".unicodeScalars.contains(c) {
    +                try token(&meter)
    +                i += 1
    +                try append(try node("mo", String(c), [], &meter), &meter)
    +                continue
    +            }
    +            pending = (try simple(&meter), nil, nil)
             }
    -        throw JSONHTMLPreviewRejection.syntax
    -      }
    -      if "+-=(),/×⋅±≤≥≠".unicodeScalars.contains(c) {
    -        try token(&meter)
    -        i += 1
    -        try append(try node("mo", String(c), [], &meter), &meter)
    -        continue
    -      }
    -      pending = (try simple(&meter), nil, nil)
         }
    -  }
     }
    diff --git a/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift b/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift
    index 99d4cc4..12adb24 100644
    --- a/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift
    +++ b/Sources/JSONHTML/JSONHTMLPreparedPolicy.swift
    @@ -4,22 +4,22 @@ import JSON
     /// Immutable policy accompanying an already-rendered static HTML artifact.
     /// Browser fonts/raster readiness is awaited by explicit PDF preparation, not claimed by native admission.
     public struct JSONHTMLPreparedPolicy: Sendable {
    -  /// Native structure used by both the static artifact and its print representation.
    -  public let layout: JSONHTMLLayout
    -  /// Main artifact string profile; inspection is explicitly labelled.
    -  public let valueProfile: JSONHTMLValueProfile
    -  /// Print/PDF profile retained by the artifact, with complete values by default.
    -  public let printValueProfile: JSONHTMLValueProfile
    -  /// Fixed native MathML and system fonts; no downloaded fonts.
    -  public let fontPolicy: String
    -  /// PNG/JPEG headers admitted natively; browser decode must complete before PDF capture.
    -  public let mediaPolicy: String
    -  package init(options: JSONExportOptions) {
    -    layout = options.layout
    -    valueProfile = options.valueProfile
    -    printValueProfile = options.printValueProfile
    -    fontPolicy = "System fonts and native MathML Core; await document.fonts.ready"
    -    mediaPolicy =
    -      "Offline canonical PNG/JPEG only; await image decode; native admission is not a raster codec"
    -  }
    +    /// Native structure used by both the static artifact and its print representation.
    +    public let layout: JSONHTMLLayout
    +    /// Main artifact string profile; inspection is explicitly labelled.
    +    public let valueProfile: JSONHTMLValueProfile
    +    /// Print/PDF profile retained by the artifact, with complete values by default.
    +    public let printValueProfile: JSONHTMLValueProfile
    +    /// Fixed native MathML and system fonts; no downloaded fonts.
    +    public let fontPolicy: String
    +    /// PNG/JPEG headers admitted natively; browser decode must complete before PDF capture.
    +    public let mediaPolicy: String
    +    package init(options: JSONExportOptions) {
    +        layout = options.layout
    +        valueProfile = options.valueProfile
    +        printValueProfile = options.printValueProfile
    +        fontPolicy = "System fonts and native MathML Core; await document.fonts.ready"
    +        mediaPolicy =
    +            "Offline canonical PNG/JPEG only; await image decode; native admission is not a raster codec"
    +    }
     }
    diff --git a/Sources/JSONHTML/JSONHTMLPreviewOptions.swift b/Sources/JSONHTML/JSONHTMLPreviewOptions.swift
    index 4f4e14a..3673726 100644
    --- a/Sources/JSONHTML/JSONHTMLPreviewOptions.swift
    +++ b/Sources/JSONHTML/JSONHTMLPreviewOptions.swift
    @@ -3,148 +3,148 @@ import JSON
     
     /// Independent document shape; changing it never changes exact JSON bytes.
     public enum JSONHTMLLayout: String, Sendable {
    -  /// Ordered forms, record cards and scalar lists.
    -  case structured
    -  /// Full exact highlighted JSON syntax.
    -  case syntax
    +    /// Ordered forms, record cards and scalar lists.
    +    case structured
    +    /// Full exact highlighted JSON syntax.
    +    case syntax
     }
     /// String presentation; both profiles retain full exact source.
     public enum JSONHTMLValueProfile: String, Sendable {
    -  /// Labelled bounded prefixes with native original-value disclosure.
    -  case inspection
    -  /// Full expanded original strings, with optional derived previews alongside.
    -  case complete
    +    /// Labelled bounded prefixes with native original-value disclosure.
    +    case inspection
    +    /// Full expanded original strings, with optional derived previews alongside.
    +    case complete
     }
     /// The closed inline dialect applied to string previews.
     public enum JSONHTMLInlineFormatting: String, Sendable {
    -  /// Literal strings only.
    -  case off
    -  /// Apply only to explicitly or effectively hinted inline text.
    -  case declared
    -  /// Apply to bounded string prefixes after specialized preview resolution.
    -  case automatic
    +    /// Literal strings only.
    +    case off
    +    /// Apply only to explicitly or effectively hinted inline text.
    +    case declared
    +    /// Apply to bounded string prefixes after specialized preview resolution.
    +    case automatic
     }
     /// Whether validated inline link destinations can be activated by a user.
     public enum JSONHTMLLinkActivation: String, Sendable {
    -  /// Destinations remain inert text; the default.
    -  case disabled
    -  /// Explicitly allow absolute credential-free HTTP/HTTPS user navigation.
    -  case httpAndHTTPS
    +    /// Destinations remain inert text; the default.
    +    case disabled
    +    /// Explicitly allow absolute credential-free HTTP/HTTPS user navigation.
    +    case httpAndHTTPS
     }
     /// Explicit units for derived color channels, independent of JSON arithmetic.
     public enum JSONHTMLColorUnits: String, Sendable {
    -  /// Integral RGB channels from zero through 255; alpha remains zero through one.
    -  case integer8
    -  /// Exact channels from zero through one; appearance rounds to nearest 8-bit channel.
    -  case normalized
    +    /// Integral RGB channels from zero through 255; alpha remains zero through one.
    +    case integer8
    +    /// Exact channels from zero through one; appearance rounds to nearest 8-bit channel.
    +    case normalized
     }
     /// Explicit channel sequence for an array or named r/g/b/a object.
     public enum JSONHTMLColorOrder: String, Sendable {
    -  /// Three r/g/b channels.
    -  case rgb
    -  /// Four r/g/b/a channels.
    -  case rgba
    +    /// Three r/g/b channels.
    +    case rgb
    +    /// Four r/g/b/a channels.
    +    case rgba
     }
     /// The only declared raster media types admitted by the native header pipeline.
     public enum JSONHTMLRasterMediaType: String, Sendable {
    -  /// A signature/header-admitted PNG; no animation claim.
    -  case png = "image/png"
    -  /// A signature/header-admitted JPEG; decoding remains browser work.
    -  case jpeg = "image/jpeg"
    +    /// A signature/header-admitted PNG; no animation claim.
    +    case png = "image/png"
    +    /// A signature/header-admitted JPEG; decoding remains browser work.
    +    case jpeg = "image/jpeg"
     }
     /// A closed caller declaration at an exact typed instance location.
     public struct JSONHTMLPreviewHint: Sendable {
    -  /// Supported declaration kinds; no callback or arbitrary markup is accepted.
    -  public enum Kind: Sendable {
    -    /// Suppress all inference at this node.
    -    case literal
    -    /// Apply the bounded inline text dialect.
    -    case inlineText
    -    /// Explicit units and channel order; strings still use the complete sRGB grammar.
    +    /// Supported declaration kinds; no callback or arbitrary markup is accepted.
    +    public enum Kind: Sendable {
    +        /// Suppress all inference at this node.
    +        case literal
    +        /// Apply the bounded inline text dialect.
    +        case inlineText
    +        /// Explicit units and channel order; strings still use the complete sRGB grammar.
    +        /// - Parameters:
    +        ///   - units: Exact integer8 or normalized interpretation before appearance conversion.
    +        ///   - order: Required r/g/b or r/g/b/a field order.
    +        case color(units: JSONHTMLColorUnits, order: JSONHTMLColorOrder)
    +        /// Declare a bare canonical Base64 string with an allowlisted media type.
    +        case embeddedImage(JSONHTMLRasterMediaType)
    +    }
    +    /// Keys and indices retain distinct scalar identity.
    +    public let path: [JSONLocationComponent]
    +    /// Closed declaration; annotation intent is not a schema verdict or codec proof.
    +    public let kind: Kind
    +    /// Creates a declaration without traversing or retaining a source document.
         /// - Parameters:
    -    ///   - units: Exact integer8 or normalized interpretation before appearance conversion.
    -    ///   - order: Required r/g/b or r/g/b/a field order.
    -    case color(units: JSONHTMLColorUnits, order: JSONHTMLColorOrder)
    -    /// Declare a bare canonical Base64 string with an allowlisted media type.
    -    case embeddedImage(JSONHTMLRasterMediaType)
    -  }
    -  /// Keys and indices retain distinct scalar identity.
    -  public let path: [JSONLocationComponent]
    -  /// Closed declaration; annotation intent is not a schema verdict or codec proof.
    -  public let kind: Kind
    -  /// Creates a declaration without traversing or retaining a source document.
    -  /// - Parameters:
    -  ///   - path: Exact typed instance location.
    -  ///   - kind: Closed preview declaration.
    -  public init(path: [JSONLocationComponent], kind: Kind) {
    -    self.path = path
    -    self.kind = kind
    -  }
    +    ///   - path: Exact typed instance location.
    +    ///   - kind: Closed preview declaration.
    +    public init(path: [JSONLocationComponent], kind: Kind) {
    +        self.path = path
    +        self.kind = kind
    +    }
     }
     /// Finite preview eligibility ceilings, additional to unchanged global core/HTML limits.
     ///
     /// These initial policies are not raster decoding, browser instruction or RSS caps.
     /// Local ineligibility preserves source; actual enclosing exhaustion is terminal.
     public struct JSONHTMLPreviewPolicy: Sendable {
    -  /// Decoded source scalars in an inspection prefix.
    -  public var maxSourceScalars: UInt64 = 512
    -  /// UTF-8 bytes in that prefix.
    -  public var maxPrefixUTF8Bytes: UInt64 = 2048
    -  /// Reversible display scalars, excluding quotation/status chrome.
    -  public var maxEscapedDisplayScalars: UInt64 = 512
    -  /// Inline delimiter stack depth.
    -  public var maxInlineNesting: UInt64 = 8
    -  /// Inline staged token count.
    -  public var maxInlineTokens: UInt64 = 1024
    -  /// Decoded inline destination UTF-8 bytes.
    -  public var maxDestinationUTF8Bytes: UInt64 = 2048
    -  /// Inline image alternative text scalars.
    -  public var maxAltScalars: UInt64 = 128
    -  /// Math candidate source scalars.
    -  public var maxMathSourceScalars: UInt64 = 256
    -  /// Math candidate UTF-8 bytes.
    -  public var maxMathSourceUTF8Bytes: UInt64 = 1024
    -  /// Math syntax stack depth.
    -  public var maxMathSyntaxDepth: UInt64 = 16
    -  /// Math descriptor nodes per candidate.
    -  public var maxMathNodes: UInt64 = 256
    -  /// Math tokens per candidate.
    -  public var maxMathTokens: UInt64 = 512
    -  /// Declared encoded raster candidate bytes.
    -  public var maxEncodedImageBytes: UInt64 = 3_145_728
    -  /// Decoded bytes per image.
    -  public var maxDecodedImageBytes: UInt64 = 2_097_152
    -  /// Aggregate admitted decoded raster bytes across unique previews.
    -  public var maxAggregateDecodedImageBytes: UInt64 = 8_388_608
    -  /// Raster width in pixels.
    -  public var maxImageWidth: UInt64 = 2048
    -  /// Raster height in pixels.
    -  public var maxImageHeight: UInt64 = 2048
    -  /// Checked raster width times height.
    -  public var maxImagePixels: UInt64 = 4_194_304
    -  /// Maximum native header bytes examined per candidate.
    -  public var maxImageHeaderBytes: UInt64 = 262_144
    -  /// Unique admitted raster previews.
    -  public var maxImagePreviews: UInt64 = 64
    -  /// Caller declaration records per hint tier.
    -  public var maxHintRecords: UInt64 = 1024
    -  /// Complete color candidate UTF-8 bytes.
    -  public var maxColorCandidateBytes: UInt64 = 256
    -  /// Creates the initial finite policy; mutate specific ceilings before rendering.
    -  public init() {}
    -  package func validate() throws {
    -    let values = [
    -      maxSourceScalars, maxPrefixUTF8Bytes, maxEscapedDisplayScalars, maxInlineNesting,
    -      maxInlineTokens,
    -      maxDestinationUTF8Bytes, maxAltScalars, maxMathSourceScalars, maxMathSourceUTF8Bytes,
    -      maxMathSyntaxDepth,
    -      maxMathNodes, maxMathTokens, maxEncodedImageBytes, maxDecodedImageBytes,
    -      maxAggregateDecodedImageBytes,
    -      maxImageWidth, maxImageHeight, maxImagePixels, maxImageHeaderBytes, maxImagePreviews,
    -      maxHintRecords, maxColorCandidateBytes,
    -    ]
    -    guard values.allSatisfy({ $0 < UInt64(Int.max) }) else {
    -      throw JSONHTMLFailure.invalidConfiguration
    +    /// Decoded source scalars in an inspection prefix.
    +    public var maxSourceScalars: UInt64 = 512
    +    /// UTF-8 bytes in that prefix.
    +    public var maxPrefixUTF8Bytes: UInt64 = 2048
    +    /// Reversible display scalars, excluding quotation/status chrome.
    +    public var maxEscapedDisplayScalars: UInt64 = 512
    +    /// Inline delimiter stack depth.
    +    public var maxInlineNesting: UInt64 = 8
    +    /// Inline staged token count.
    +    public var maxInlineTokens: UInt64 = 1024
    +    /// Decoded inline destination UTF-8 bytes.
    +    public var maxDestinationUTF8Bytes: UInt64 = 2048
    +    /// Inline image alternative text scalars.
    +    public var maxAltScalars: UInt64 = 128
    +    /// Math candidate source scalars.
    +    public var maxMathSourceScalars: UInt64 = 256
    +    /// Math candidate UTF-8 bytes.
    +    public var maxMathSourceUTF8Bytes: UInt64 = 1024
    +    /// Math syntax stack depth.
    +    public var maxMathSyntaxDepth: UInt64 = 16
    +    /// Math descriptor nodes per candidate.
    +    public var maxMathNodes: UInt64 = 256
    +    /// Math tokens per candidate.
    +    public var maxMathTokens: UInt64 = 512
    +    /// Declared encoded raster candidate bytes.
    +    public var maxEncodedImageBytes: UInt64 = 3_145_728
    +    /// Decoded bytes per image.
    +    public var maxDecodedImageBytes: UInt64 = 2_097_152
    +    /// Aggregate admitted decoded raster bytes across unique previews.
    +    public var maxAggregateDecodedImageBytes: UInt64 = 8_388_608
    +    /// Raster width in pixels.
    +    public var maxImageWidth: UInt64 = 2048
    +    /// Raster height in pixels.
    +    public var maxImageHeight: UInt64 = 2048
    +    /// Checked raster width times height.
    +    public var maxImagePixels: UInt64 = 4_194_304
    +    /// Maximum native header bytes examined per candidate.
    +    public var maxImageHeaderBytes: UInt64 = 262_144
    +    /// Unique admitted raster previews.
    +    public var maxImagePreviews: UInt64 = 64
    +    /// Caller declaration records per hint tier.
    +    public var maxHintRecords: UInt64 = 1024
    +    /// Complete color candidate UTF-8 bytes.
    +    public var maxColorCandidateBytes: UInt64 = 256
    +    /// Creates the initial finite policy; mutate specific ceilings before rendering.
    +    public init() {}
    +    package func validate() throws {
    +        let values = [
    +            maxSourceScalars, maxPrefixUTF8Bytes, maxEscapedDisplayScalars, maxInlineNesting,
    +            maxInlineTokens,
    +            maxDestinationUTF8Bytes, maxAltScalars, maxMathSourceScalars, maxMathSourceUTF8Bytes,
    +            maxMathSyntaxDepth,
    +            maxMathNodes, maxMathTokens, maxEncodedImageBytes, maxDecodedImageBytes,
    +            maxAggregateDecodedImageBytes,
    +            maxImageWidth, maxImageHeight, maxImagePixels, maxImageHeaderBytes, maxImagePreviews,
    +            maxHintRecords, maxColorCandidateBytes,
    +        ]
    +        guard values.allSatisfy({ $0 < UInt64(Int.max) }) else {
    +            throw JSONHTMLFailure.invalidConfiguration
    +        }
         }
    -  }
     }
    diff --git a/Sources/JSONHTML/JSONHTMLPreviewPlan.swift b/Sources/JSONHTML/JSONHTMLPreviewPlan.swift
    index 080a37d..44420fa 100644
    --- a/Sources/JSONHTML/JSONHTMLPreviewPlan.swift
    +++ b/Sources/JSONHTML/JSONHTMLPreviewPlan.swift
    @@ -2,212 +2,223 @@
     import JSON
     
     package enum JSONHTMLPrimaryPreview {
    -  case literal
    -  case inline(JSONHTMLInlinePlan)
    -  case color(JSONHTMLColor)
    -  case image(JSONHTMLRaster)
    +    case literal
    +    case inline(JSONHTMLInlinePlan)
    +    case color(JSONHTMLColor)
    +    case image(JSONHTMLRaster)
     }
     package struct JSONHTMLNodePreview {
    -  let prefix: JSONHTMLStringPrefix?
    -  let primary: JSONHTMLPrimaryPreview
    +    let prefix: JSONHTMLStringPrefix?
    +    let primary: JSONHTMLPrimaryPreview
     }
     package struct JSONHTMLPreviewIssue {
    -  let path: [JSONLocationComponent]
    -  let message: String
    +    let path: [JSONLocationComponent]
    +    let message: String
     }
     package struct JSONHTMLPreviewPlan {
    -  var nodes: [JSONHTMLNodePreview]
    -  var issues: [JSONHTMLPreviewIssue]
    -  let media: JSONHTMLMediaLedger
    -  let reservation: UInt64
    -  var shortened: Bool
    -  package static func make(
    -    _ value: JSON, options: JSONHTMLRenderingOptions, meter: inout JSONWorkMeter
    -  ) throws -> Self {
    -    let entry = meter.used(.retainedBytes)
    -    try options.previewPolicy.validate()
    -    let explicit = options.previewHints
    -    let effective = options.effectivePreviewHints
    -    for tier in [explicit, effective] {
    -      try meter.work()
    -      guard UInt64(tier.count) <= options.previewPolicy.maxHintRecords else {
    -        throw JSONHTMLFailure.invalidConfiguration
    -      }
    -      for i in tier.indices {
    -        try meter.work()
    -        try meter.require(.pointerTokens, UInt64(tier[i].path.count))
    -        for j in 0.. Self {
    +        let entry = meter.used(.retainedBytes)
    +        try options.previewPolicy.validate()
    +        let explicit = options.previewHints
    +        let effective = options.effectivePreviewHints
    +        for tier in [explicit, effective] {
    +            try meter.work()
    +            guard UInt64(tier.count) <= options.previewPolicy.maxHintRecords else {
    +                throw JSONHTMLFailure.invalidConfiguration
    +            }
    +            for i in tier.indices {
    +                try meter.work()
    +                try meter.require(.pointerTokens, UInt64(tier[i].path.count))
    +                for j in 0.. Bool {
    -    let v = c.value
    -    return c.properties.generalCategory == .format || v <= 31 || (127...159).contains(v)
    -      || [0xad, 0x61c, 0xfeff, 0xe0001].contains(v)
    -      || (0x200b...0x200f).contains(v) || (0x2028...0x202e).contains(v)
    -      || (0x2060...0x206f).contains(v) || (0xfff9...0xfffb).contains(v)
    -      || (0xe0020...0xe007f).contains(v)
    -  }
    -  package static func units(_ c: Unicode.Scalar) -> UInt64 {
    -    if protected(c) { return c.value > 0xffff ? 12 : 6 }
    -    return c == "\"" || c == "\\" ? 2 : 1
    -  }
    -  package static func prefix(
    -    _ text: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter
    -  ) throws -> JSONHTMLStringPrefix {
    -    var iterator = text.unicodeScalars.makeIterator()
    -    var source = ""
    -    var count: UInt64 = 0
    -    var bytes: UInt64 = 0
    -    var display: UInt64 = 0
    -    while let c = iterator.next() {
    -      try meter.work()
    -      let width = UInt64(c.utf8.count)
    -      let cost = units(c)
    -      if count >= policy.maxSourceScalars || bytes + width > policy.maxPrefixUTF8Bytes
    -        || display + cost > policy.maxEscapedDisplayScalars
    -      {
    -        return .init(
    -          source: source, shortened: true, scalars: count, displayScalars: display,
    -          reservation: bytes)
    -      }
    -      for _ in c.utf8 {
    -        try meter.work()
    -        try meter.retain(1)
    -      }
    -      source.unicodeScalars.append(c)
    -      count += 1
    -      bytes += width
    -      display += cost
    +    private static let hex = Array("0123456789abcdef".utf8)
    +    package static func protected(_ c: Unicode.Scalar) -> Bool {
    +        let v = c.value
    +        return c.properties.generalCategory == .format || v <= 31 || (127...159).contains(v)
    +            || [0xad, 0x61c, 0xfeff, 0xe0001].contains(v)
    +            || (0x200b...0x200f).contains(v) || (0x2028...0x202e).contains(v)
    +            || (0x2060...0x206f).contains(v) || (0xfff9...0xfffb).contains(v)
    +            || (0xe0020...0xe007f).contains(v)
         }
    -    return .init(
    -      source: source, shortened: false, scalars: count, displayScalars: display, reservation: bytes)
    -  }
    -  package static func text(
    -    _ text: String, quoted: Bool, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter
    -  ) throws {
    -    if quoted { try output.escaped(34, meter: &meter) }
    -    func escape(_ value: UInt32, _ out: inout JSONHTMLDocumentBuffer, _ m: inout JSONWorkMeter)
    -      throws
    -    {
    -      try out.append("\\u", meter: &m)
    -      for shift in [12, 8, 4, 0] {
    -        try m.work()
    -        try out.appendByte(hex[Int(value >> shift & 15)], meter: &m)
    -      }
    +    package static func units(_ c: Unicode.Scalar) -> UInt64 {
    +        if protected(c) { return c.value > 0xffff ? 12 : 6 }
    +        return c == "\"" || c == "\\" ? 2 : 1
         }
    -    for c in text.unicodeScalars {
    -      try meter.work()
    -      if protected(c) {
    -        if c.value <= 0xffff {
    -          try escape(c.value, &output, &meter)
    -        } else {
    -          let v = c.value - 0x10000
    -          try escape(0xd800 + (v >> 10), &output, &meter)
    -          try escape(0xdc00 + (v & 1023), &output, &meter)
    +    package static func prefix(
    +        _ text: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter
    +    ) throws -> JSONHTMLStringPrefix {
    +        var iterator = text.unicodeScalars.makeIterator()
    +        var source = ""
    +        var count: UInt64 = 0
    +        var bytes: UInt64 = 0
    +        var display: UInt64 = 0
    +        while let c = iterator.next() {
    +            try meter.work()
    +            let width = UInt64(c.utf8.count)
    +            let cost = units(c)
    +            if count >= policy.maxSourceScalars || bytes + width > policy.maxPrefixUTF8Bytes
    +                || display + cost > policy.maxEscapedDisplayScalars
    +            {
    +                return .init(
    +                    source: source, shortened: true, scalars: count, displayScalars: display,
    +                    reservation: bytes)
    +            }
    +            for _ in c.utf8 {
    +                try meter.work()
    +                try meter.retain(1)
    +            }
    +            source.unicodeScalars.append(c)
    +            count += 1
    +            bytes += width
    +            display += cost
             }
    -      } else if c == "\"" || c == "\\" {
    -        try output.appendByte(92, meter: &meter)
    -        for b in c.utf8 { try output.escaped(b, meter: &meter) }
    -      } else {
    -        for b in c.utf8 { try output.escaped(b, meter: &meter) }
    -      }
    +        return .init(
    +            source: source, shortened: false, scalars: count, displayScalars: display,
    +            reservation: bytes)
         }
    -    if quoted { try output.escaped(34, meter: &meter) }
    -  }
    -  package static func equal(
    -    _ lhs: [JSONLocationComponent], _ rhs: [JSONLocationComponent], meter: inout JSONWorkMeter
    -  ) throws -> Bool {
    -    try meter.work()
    -    guard lhs.count == rhs.count else { return false }
    -    for (a, b) in zip(lhs, rhs) {
    -      try meter.work()
    -      switch (a, b) {
    -      case (.index(let x), .index(let y)): if x != y { return false }
    -      case (.key(let x), .key(let y)):
    -        var l = x.string.utf8.makeIterator()
    -        var r = y.string.utf8.makeIterator()
    -        while true {
    -          try meter.work()
    -          let a = l.next()
    -          let b = r.next()
    -          if a != b { return false }
    -          if a == nil { break }
    +    package static func text(
    +        _ text: String, quoted: Bool, output: inout JSONHTMLDocumentBuffer,
    +        meter: inout JSONWorkMeter
    +    ) throws {
    +        if quoted { try output.escaped(34, meter: &meter) }
    +        func escape(_ value: UInt32, _ out: inout JSONHTMLDocumentBuffer, _ m: inout JSONWorkMeter)
    +            throws
    +        {
    +            try out.append("\\u", meter: &m)
    +            for shift in [12, 8, 4, 0] {
    +                try m.work()
    +                try out.appendByte(hex[Int(value >> shift & 15)], meter: &m)
    +            }
             }
    -      default: return false
    -      }
    -    }
    -    return true
    -  }
    -}
    -package struct JSONHTMLColor {
    -  package let r: UInt8, g: UInt8, b: UInt8, a: Double
    -  package let derived: Bool
    -  package func emit(
    -    ordinal: UInt64, namespace: String, label: JSONHTMLStringPrefix?,
    -    output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter
    -  ) throws {
    -    guard a.isFinite, a >= 0, a <= 1 else { throw JSONHTMLFailure.invalidConfiguration }
    -    try meter.retain(64)
    -    try output.addDOM(18, meter: &meter)
    -    try output.append(
    -      "",
    -      meter: &meter)
    -    if !derived, let label {
    -      try output.escaped(label.source, meter: &meter)
    -      if label.shortened { try output.append(" (preview)", meter: &meter) }
    -    } else {
    -      try output.append("Derived #", meter: &meter)
    -      for v in [r, g, b, UInt8((a * 255).rounded())] {
    -        for n in [v >> 4, v & 15] {
    -          try meter.work()
    -          try output.appendByte(n < 10 ? n + 48 : n + 87, meter: &meter)
    +        for c in text.unicodeScalars {
    +            try meter.work()
    +            if protected(c) {
    +                if c.value <= 0xffff {
    +                    try escape(c.value, &output, &meter)
    +                } else {
    +                    let v = c.value - 0x10000
    +                    try escape(0xd800 + (v >> 10), &output, &meter)
    +                    try escape(0xdc00 + (v & 1023), &output, &meter)
    +                }
    +            } else if c == "\"" || c == "\\" {
    +                try output.appendByte(92, meter: &meter)
    +                for b in c.utf8 { try output.escaped(b, meter: &meter) }
    +            } else {
    +                for b in c.utf8 { try output.escaped(b, meter: &meter) }
    +            }
             }
    -      }
    +        if quoted { try output.escaped(34, meter: &meter) }
         }
    -    try output.append("", meter: &meter)
    -    try output.append(derived ? "Derived sRGB channels: " : "sRGB channels: ", meter: &meter)
    -    for (i, v) in [r, g, b].enumerated() {
    -      if i > 0 { try output.append(", ", meter: &meter) }
    -      try output.decimal(UInt64(v), meter: &meter)
    -    }
    -    try output.append("; alpha ", meter: &meter)
    -    try output.append(String(a), meter: &meter)
    -    try output.append("", meter: &meter)
    -    try meter.release(64)
    -  }
    -  package static func parse(
    -    _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter
    -  ) throws -> Self? {
    -    var bytes: [UInt8] = []
    -    for b in source.utf8 {
    -      try meter.work()
    -      guard UInt64(bytes.count) < policy.maxColorCandidateBytes, b >= 32, b < 127 else {
    -        try meter.release(UInt64(bytes.count))
    -        return nil
    -      }
    -      try meter.retain(1)
    -      bytes.append(b)
    -    }
    -    try meter.retain(128)
    -    let result = try parseBytes(bytes, meter: &meter)
    -    try meter.release(128 + UInt64(bytes.count))
    -    return result
    -  }
    -  private static func parseBytes(_ bytes: [UInt8], meter: inout JSONWorkMeter) throws -> Self? {
    -    func nibble(_ x: UInt8) -> UInt8? {
    -      switch x {
    -      case 48...57: return x - 48
    -      case 65...70: return x - 55
    -      case 97...102: return x - 87
    -      default: return nil
    -      }
    -    }
    -    if bytes.first == 35 {
    -      guard [4, 5, 7, 9].contains(bytes.count) else { return nil }
    -      var values: [UInt8] = []
    -      let short = bytes.count < 6
    -      var i = 1
    -      while i < bytes.count {
    +    package static func equal(
    +        _ lhs: [JSONLocationComponent], _ rhs: [JSONLocationComponent], meter: inout JSONWorkMeter
    +    ) throws -> Bool {
             try meter.work()
    -        guard let n = nibble(bytes[i]) else { return nil }
    -        if short {
    -          values.append(n * 17)
    -          i += 1
    +        guard lhs.count == rhs.count else { return false }
    +        for (a, b) in zip(lhs, rhs) {
    +            try meter.work()
    +            switch (a, b) {
    +            case (.index(let x), .index(let y)): if x != y { return false }
    +            case (.key(let x), .key(let y)):
    +                var l = x.string.utf8.makeIterator()
    +                var r = y.string.utf8.makeIterator()
    +                while true {
    +                    try meter.work()
    +                    let a = l.next()
    +                    let b = r.next()
    +                    if a != b { return false }
    +                    if a == nil { break }
    +                }
    +            default: return false
    +            }
    +        }
    +        return true
    +    }
    +}
    +package struct JSONHTMLColor {
    +    package let r: UInt8, g: UInt8, b: UInt8, a: Double
    +    package let derived: Bool
    +    package func emit(
    +        ordinal: UInt64, namespace: String, label: JSONHTMLStringPrefix?,
    +        output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter
    +    ) throws {
    +        guard a.isFinite, a >= 0, a <= 1 else { throw JSONHTMLFailure.invalidConfiguration }
    +        try meter.retain(64)
    +        try output.addDOM(18, meter: &meter)
    +        try output.append(
    +            "",
    +            meter: &meter)
    +        if !derived, let label {
    +            try output.escaped(label.source, meter: &meter)
    +            if label.shortened { try output.append(" (preview)", meter: &meter) }
             } else {
    -          guard let m = nibble(bytes[i + 1]) else { return nil }
    -          values.append(n * 16 + m)
    -          i += 2
    +            try output.append("Derived #", meter: &meter)
    +            for v in [r, g, b, UInt8((a * 255).rounded())] {
    +                for n in [v >> 4, v & 15] {
    +                    try meter.work()
    +                    try output.appendByte(n < 10 ? n + 48 : n + 87, meter: &meter)
    +                }
    +            }
             }
    -      }
    -      return .init(
    -        r: values[0], g: values[1], b: values[2],
    -        a: values.count == 4 ? Double(values[3]) / 255 : 1, derived: false)
    +        try output.append("", meter: &meter)
    +        try output.append(derived ? "Derived sRGB channels: " : "sRGB channels: ", meter: &meter)
    +        for (i, v) in [r, g, b].enumerated() {
    +            if i > 0 { try output.append(", ", meter: &meter) }
    +            try output.decimal(UInt64(v), meter: &meter)
    +        }
    +        try output.append("; alpha ", meter: &meter)
    +        try output.append(String(a), meter: &meter)
    +        try output.append("", meter: &meter)
    +        try meter.release(64)
         }
    -    let rgba = bytes.starts(with: Array("rgba(".utf8))
    -    let rgb = bytes.starts(with: Array("rgb(".utf8))
    -    guard rgba || rgb else { return nil }
    -    var i = rgba ? 5 : 4
    -    var values: [Double] = []
    -    for channel in 0..<(rgba ? 4 : 3) {
    -      while i < bytes.count && bytes[i] == 32 {
    -        try meter.work()
    -        i += 1
    -      }
    -      var value: UInt64 = 0
    -      var digits = 0
    -      while i < bytes.count && (48...57).contains(bytes[i]) {
    -        try meter.work()
    -        guard value <= 255 else { return nil }
    -        value = value * 10 + UInt64(bytes[i] - 48)
    -        digits += 1
    -        i += 1
    -      }
    -      guard digits > 0 || (channel == 3 && i < bytes.count && bytes[i] == 46) else { return nil }
    -      var projected = Double(value)
    -      if channel == 3 && i < bytes.count && bytes[i] == 46 {
    -        i += 1
    -        let start = i
    -        var factor = 0.1
    -        while i < bytes.count && (48...57).contains(bytes[i]) {
    -          try meter.work()
    -          if value == 1 && bytes[i] != 48 { return nil }
    -          projected += Double(bytes[i] - 48) * factor
    -          factor /= 10
    -          i += 1
    +    package static func parse(
    +        _ source: String, policy: JSONHTMLPreviewPolicy, meter: inout JSONWorkMeter
    +    ) throws -> Self? {
    +        var bytes: [UInt8] = []
    +        for b in source.utf8 {
    +            try meter.work()
    +            guard UInt64(bytes.count) < policy.maxColorCandidateBytes, b >= 32, b < 127 else {
    +                try meter.release(UInt64(bytes.count))
    +                return nil
    +            }
    +            try meter.retain(1)
    +            bytes.append(b)
             }
    -        guard i > start else { return nil }
    -      }
    -      guard projected <= Double(channel == 3 ? 1 : 255) else { return nil }
    -      values.append(projected)
    -      while i < bytes.count && bytes[i] == 32 {
    -        try meter.work()
    -        i += 1
    -      }
    -      let separator: UInt8 = channel == (rgba ? 3 : 2) ? 41 : 44
    -      guard i < bytes.count && bytes[i] == separator else { return nil }
    -      i += 1
    +        try meter.retain(128)
    +        let result = try parseBytes(bytes, meter: &meter)
    +        try meter.release(128 + UInt64(bytes.count))
    +        return result
         }
    -    guard i == bytes.count else { return nil }
    -    return .init(
    -      r: UInt8(values[0]), g: UInt8(values[1]), b: UInt8(values[2]), a: rgba ? values[3] : 1,
    -      derived: true)
    -  }
    -  package static func hinted(
    -    _ value: JSON, units: JSONHTMLColorUnits, order: JSONHTMLColorOrder, meter: inout JSONWorkMeter
    -  ) throws -> Self? {
    -    try meter.retain(512)
    -    let result = try hintedBody(value, units: units, order: order, meter: &meter)
    -    try meter.release(512)
    -    return result
    -  }
    -  private static func hintedBody(
    -    _ value: JSON, units: JSONHTMLColorUnits, order: JSONHTMLColorOrder, meter: inout JSONWorkMeter
    -  ) throws -> Self? {
    -    var channels: [JSON] = []
    -    switch value {
    -    case .array(let v):
    -      guard v.count <= 4 else { return nil }
    -      channels = v
    -    case .object(let o):
    -      for key in order == .rgb ? ["r", "g", "b"] : ["r", "g", "b", "a"] {
    -        try meter.work()
    -        guard let v = try o.lookup(key, meter: &meter) else { return nil }
    -        channels.append(v)
    -      }
    -      guard o.count == channels.count else { return nil }
    -    default: return nil
    +    private static func parseBytes(_ bytes: [UInt8], meter: inout JSONWorkMeter) throws -> Self? {
    +        func nibble(_ x: UInt8) -> UInt8? {
    +            switch x {
    +            case 48...57: return x - 48
    +            case 65...70: return x - 55
    +            case 97...102: return x - 87
    +            default: return nil
    +            }
    +        }
    +        if bytes.first == 35 {
    +            guard [4, 5, 7, 9].contains(bytes.count) else { return nil }
    +            var values: [UInt8] = []
    +            let short = bytes.count < 6
    +            var i = 1
    +            while i < bytes.count {
    +                try meter.work()
    +                guard let n = nibble(bytes[i]) else { return nil }
    +                if short {
    +                    values.append(n * 17)
    +                    i += 1
    +                } else {
    +                    guard let m = nibble(bytes[i + 1]) else { return nil }
    +                    values.append(n * 16 + m)
    +                    i += 2
    +                }
    +            }
    +            return .init(
    +                r: values[0], g: values[1], b: values[2],
    +                a: values.count == 4 ? Double(values[3]) / 255 : 1, derived: false)
    +        }
    +        let rgba = bytes.starts(with: Array("rgba(".utf8))
    +        let rgb = bytes.starts(with: Array("rgb(".utf8))
    +        guard rgba || rgb else { return nil }
    +        var i = rgba ? 5 : 4
    +        var values: [Double] = []
    +        for channel in 0..<(rgba ? 4 : 3) {
    +            while i < bytes.count && bytes[i] == 32 {
    +                try meter.work()
    +                i += 1
    +            }
    +            var value: UInt64 = 0
    +            var digits = 0
    +            while i < bytes.count && (48...57).contains(bytes[i]) {
    +                try meter.work()
    +                guard value <= 255 else { return nil }
    +                value = value * 10 + UInt64(bytes[i] - 48)
    +                digits += 1
    +                i += 1
    +            }
    +            guard digits > 0 || (channel == 3 && i < bytes.count && bytes[i] == 46) else {
    +                return nil
    +            }
    +            var projected = Double(value)
    +            if channel == 3 && i < bytes.count && bytes[i] == 46 {
    +                i += 1
    +                let start = i
    +                var factor = 0.1
    +                while i < bytes.count && (48...57).contains(bytes[i]) {
    +                    try meter.work()
    +                    if value == 1 && bytes[i] != 48 { return nil }
    +                    projected += Double(bytes[i] - 48) * factor
    +                    factor /= 10
    +                    i += 1
    +                }
    +                guard i > start else { return nil }
    +            }
    +            guard projected <= Double(channel == 3 ? 1 : 255) else { return nil }
    +            values.append(projected)
    +            while i < bytes.count && bytes[i] == 32 {
    +                try meter.work()
    +                i += 1
    +            }
    +            let separator: UInt8 = channel == (rgba ? 3 : 2) ? 41 : 44
    +            guard i < bytes.count && bytes[i] == separator else { return nil }
    +            i += 1
    +        }
    +        guard i == bytes.count else { return nil }
    +        return .init(
    +            r: UInt8(values[0]), g: UInt8(values[1]), b: UInt8(values[2]), a: rgba ? values[3] : 1,
    +            derived: true)
         }
    -    guard channels.count == (order == .rgb ? 3 : 4) else { return nil }
    -    var projected: [Double] = []
    -    for (i, c) in channels.enumerated() {
    -      try meter.work()
    -      guard case .number(let n) = c else { return nil }
    -      if units == .integer8 && i < 3 {
    -        guard n.components.exponent >= 0 else { return nil }
    -        // Range/type rejection is local; a real numeric meter failure must propagate.
    -        guard try n.compare(to: .init(integer: 0), meter: &meter) != .orderedAscending,
    -          try n.compare(to: .init(integer: 255), meter: &meter) != .orderedDescending
    -        else { return nil }
    -        projected.append(Double(try n.integer(as: UInt8.self, meter: &meter)))
    -      } else {
    -        guard try n.compare(to: .init(integer: 0), meter: &meter) != .orderedAscending,
    -          try n.compare(to: .init(integer: 1), meter: &meter) != .orderedDescending
    -        else { return nil }
    -        projected.append(try n.doubleValue(policy: .rounded, meter: &meter))
    -      }
    +    package static func hinted(
    +        _ value: JSON, units: JSONHTMLColorUnits, order: JSONHTMLColorOrder,
    +        meter: inout JSONWorkMeter
    +    ) throws -> Self? {
    +        try meter.retain(512)
    +        let result = try hintedBody(value, units: units, order: order, meter: &meter)
    +        try meter.release(512)
    +        return result
         }
    -    func byte(_ i: Int) -> UInt8 {
    -      UInt8(units == .normalized ? (projected[i] * 255).rounded() : projected[i])
    +    private static func hintedBody(
    +        _ value: JSON, units: JSONHTMLColorUnits, order: JSONHTMLColorOrder,
    +        meter: inout JSONWorkMeter
    +    ) throws -> Self? {
    +        var channels: [JSON] = []
    +        switch value {
    +        case .array(let v):
    +            guard v.count <= 4 else { return nil }
    +            channels = v
    +        case .object(let o):
    +            for key in order == .rgb ? ["r", "g", "b"] : ["r", "g", "b", "a"] {
    +                try meter.work()
    +                guard let v = try o.lookup(key, meter: &meter) else { return nil }
    +                channels.append(v)
    +            }
    +            guard o.count == channels.count else { return nil }
    +        default: return nil
    +        }
    +        guard channels.count == (order == .rgb ? 3 : 4) else { return nil }
    +        var projected: [Double] = []
    +        for (i, c) in channels.enumerated() {
    +            try meter.work()
    +            guard case .number(let n) = c else { return nil }
    +            if units == .integer8 && i < 3 {
    +                guard n.components.exponent >= 0 else { return nil }
    +                // Range/type rejection is local; a real numeric meter failure must propagate.
    +                guard try n.compare(to: .init(integer: 0), meter: &meter) != .orderedAscending,
    +                    try n.compare(to: .init(integer: 255), meter: &meter) != .orderedDescending
    +                else { return nil }
    +                projected.append(Double(try n.integer(as: UInt8.self, meter: &meter)))
    +            } else {
    +                guard try n.compare(to: .init(integer: 0), meter: &meter) != .orderedAscending,
    +                    try n.compare(to: .init(integer: 1), meter: &meter) != .orderedDescending
    +                else { return nil }
    +                projected.append(try n.doubleValue(policy: .rounded, meter: &meter))
    +            }
    +        }
    +        func byte(_ i: Int) -> UInt8 {
    +            UInt8(units == .normalized ? (projected[i] * 255).rounded() : projected[i])
    +        }
    +        return .init(
    +            r: byte(0), g: byte(1), b: byte(2), a: projected.count == 4 ? projected[3] : 1,
    +            derived: true)
         }
    -    return .init(
    -      r: byte(0), g: byte(1), b: byte(2), a: projected.count == 4 ? projected[3] : 1, derived: true)
    -  }
     }
    diff --git a/Sources/JSONHTML/JSONHTMLRaster.swift b/Sources/JSONHTML/JSONHTMLRaster.swift
    index e7c5bef..8e1ad15 100644
    --- a/Sources/JSONHTML/JSONHTMLRaster.swift
    +++ b/Sources/JSONHTML/JSONHTMLRaster.swift
    @@ -2,268 +2,277 @@
     import JSON
     
     package struct JSONHTMLRaster {
    -  package let type: JSONHTMLRasterMediaType
    -  package let bytes: [UInt8]
    -  package let width: UInt32, height: UInt32
    -  package let progressive: Bool
    -  package var reservation: UInt64 { UInt64(bytes.count) }
    -  package func emit(
    -    alt: String? = nil, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter
    -  ) throws {
    -    try output.addDOM(6, meter: &meter)
    -    try output.append("
    \"",
    ", meter: &meter) - try output.append(type.rawValue, meter: &meter) - try output.append(" · ", meter: &meter) - try output.decimal(UInt64(width), meter: &meter) - try output.append(" × ", meter: &meter) - try output.decimal(UInt64(height), meter: &meter) - try output.append( - " pixels · native header admitted; raster decoding is browser work
    ", - meter: &meter) - } + package let type: JSONHTMLRasterMediaType + package let bytes: [UInt8] + package let width: UInt32, height: UInt32 + package let progressive: Bool + package var reservation: UInt64 { UInt64(bytes.count) } + package func emit( + alt: String? = nil, output: inout JSONHTMLDocumentBuffer, meter: inout JSONWorkMeter + ) throws { + try output.addDOM(6, meter: &meter) + try output.append("
    \"",
    ", meter: &meter) + try output.append(type.rawValue, meter: &meter) + try output.append(" · ", meter: &meter) + try output.decimal(UInt64(width), meter: &meter) + try output.append(" × ", meter: &meter) + try output.decimal(UInt64(height), meter: &meter) + try output.append( + " pixels · native header admitted; raster decoding is browser work
    ", + meter: &meter) + } } package struct JSONHTMLMediaLedger { - package var bytes: UInt64 = 0 - package var images: UInt64 = 0 + package var bytes: UInt64 = 0 + package var images: UInt64 = 0 } package enum JSONHTMLRasterAdmission { - private static let pngDepths: [UInt8: [UInt8]] = [ - 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16], - ] - package static func decode( - _ source: String, type: JSONHTMLRasterMediaType?, policy: JSONHTMLPreviewPolicy, - ledger: inout JSONHTMLMediaLedger, meter: inout JSONWorkMeter - ) throws -> JSONHTMLRaster? { - let retained = meter.used(.retainedBytes) - do { - var iterator = source.utf8.makeIterator() - var declared = type - var encoded: [UInt8] = [] - if declared == nil { - var header: [UInt8] = [] - while let b = iterator.next() { - try meter.work() - guard header.count < 32 else { throw JSONHTMLPreviewRejection.syntax } - try meter.retain(1) - header.append(b) - if b == 44 { break } - } - if header == Array("data:image/png;base64,".utf8) { - declared = .png - } else if header == Array("data:image/jpeg;base64,".utf8) { - declared = .jpeg - } else { - throw JSONHTMLPreviewRejection.syntax - } - try meter.release(UInt64(header.count)) - } - while let b = iterator.next() { - try meter.work() - guard UInt64(encoded.count) < policy.maxEncodedImageBytes else { - throw JSONHTMLPreviewRejection.eligibility - } - try meter.retain(1) - encoded.append(b) - } - guard !encoded.isEmpty, encoded.count % 4 == 0, ledger.images < policy.maxImagePreviews else { - throw JSONHTMLPreviewRejection.eligibility - } - let padding = encoded.last == 61 ? (encoded[encoded.count - 2] == 61 ? 2 : 1) : 0 - let size = UInt64(encoded.count / 4) * 3 - UInt64(padding) - guard size <= policy.maxDecodedImageBytes, - size <= policy.maxAggregateDecodedImageBytes - ledger.bytes - else { throw JSONHTMLPreviewRejection.eligibility } - let decoded = try canonical(encoded, maximum: size, meter: &meter) - try meter.release(UInt64(encoded.count)) - let geometry = try dimensions(decoded, type: declared!, policy: policy, meter: &meter) - let pixels = UInt64(geometry.0) * UInt64(geometry.1) - guard geometry.0 > 0, geometry.1 > 0, UInt64(geometry.0) <= policy.maxImageWidth, - UInt64(geometry.1) <= policy.maxImageHeight, pixels <= policy.maxImagePixels - else { throw JSONHTMLPreviewRejection.eligibility } - ledger.bytes += size - ledger.images += 1 - return .init( - type: declared!, bytes: decoded, width: geometry.0, height: geometry.1, - progressive: geometry.2) - } catch is JSONHTMLPreviewRejection { - try meter.release(meter.used(.retainedBytes) - retained) - return nil - } catch { throw error } - } - package static func canonical(_ encoded: [UInt8], maximum: UInt64, meter: inout JSONWorkMeter) - throws -> [UInt8] - { - guard encoded.count % 4 == 0 else { throw JSONHTMLPreviewRejection.syntax } - func digit(_ b: UInt8) -> UInt32? { - switch b { - case 65...90: return UInt32(b - 65) - case 97...122: return UInt32(b - 71) - case 48...57: return UInt32(b + 4) - case 43: return 62 - case 47: return 63 - default: return nil - } + private static let pngDepths: [UInt8: [UInt8]] = [ + 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16], + ] + package static func decode( + _ source: String, type: JSONHTMLRasterMediaType?, policy: JSONHTMLPreviewPolicy, + ledger: inout JSONHTMLMediaLedger, meter: inout JSONWorkMeter + ) throws -> JSONHTMLRaster? { + let retained = meter.used(.retainedBytes) + do { + var iterator = source.utf8.makeIterator() + var declared = type + var encoded: [UInt8] = [] + if declared == nil { + var header: [UInt8] = [] + while let b = iterator.next() { + try meter.work() + guard header.count < 32 else { throw JSONHTMLPreviewRejection.syntax } + try meter.retain(1) + header.append(b) + if b == 44 { break } + } + if header == Array("data:image/png;base64,".utf8) { + declared = .png + } else if header == Array("data:image/jpeg;base64,".utf8) { + declared = .jpeg + } else { + throw JSONHTMLPreviewRejection.syntax + } + try meter.release(UInt64(header.count)) + } + while let b = iterator.next() { + try meter.work() + guard UInt64(encoded.count) < policy.maxEncodedImageBytes else { + throw JSONHTMLPreviewRejection.eligibility + } + try meter.retain(1) + encoded.append(b) + } + guard !encoded.isEmpty, encoded.count % 4 == 0, ledger.images < policy.maxImagePreviews + else { + throw JSONHTMLPreviewRejection.eligibility + } + let padding = encoded.last == 61 ? (encoded[encoded.count - 2] == 61 ? 2 : 1) : 0 + let size = UInt64(encoded.count / 4) * 3 - UInt64(padding) + guard size <= policy.maxDecodedImageBytes, + size <= policy.maxAggregateDecodedImageBytes - ledger.bytes + else { throw JSONHTMLPreviewRejection.eligibility } + let decoded = try canonical(encoded, maximum: size, meter: &meter) + try meter.release(UInt64(encoded.count)) + let geometry = try dimensions(decoded, type: declared!, policy: policy, meter: &meter) + let pixels = UInt64(geometry.0) * UInt64(geometry.1) + guard geometry.0 > 0, geometry.1 > 0, UInt64(geometry.0) <= policy.maxImageWidth, + UInt64(geometry.1) <= policy.maxImageHeight, pixels <= policy.maxImagePixels + else { throw JSONHTMLPreviewRejection.eligibility } + ledger.bytes += size + ledger.images += 1 + return .init( + type: declared!, bytes: decoded, width: geometry.0, height: geometry.1, + progressive: geometry.2) + } catch is JSONHTMLPreviewRejection { + try meter.release(meter.used(.retainedBytes) - retained) + return nil + } catch { throw error } } - var bytes: [UInt8] = [] - var i = 0 - while i < encoded.count { - try meter.work(4) - guard let a = digit(encoded[i]), let b = digit(encoded[i + 1]) else { - throw JSONHTMLPreviewRejection.syntax - } - let c = encoded[i + 2] - let d = encoded[i + 3] - let last = i + 4 == encoded.count - var value = a << 18 | b << 12 - let emitted: Int - if c == 61 { - guard last, d == 61, b & 15 == 0 else { throw JSONHTMLPreviewRejection.syntax } - emitted = 1 - } else { - guard let cc = digit(c) else { throw JSONHTMLPreviewRejection.syntax } - value |= cc << 6 - if d == 61 { - guard last, cc & 3 == 0 else { throw JSONHTMLPreviewRejection.syntax } - emitted = 2 - } else { - guard let dd = digit(d) else { throw JSONHTMLPreviewRejection.syntax } - value |= dd - emitted = 3 + package static func canonical(_ encoded: [UInt8], maximum: UInt64, meter: inout JSONWorkMeter) + throws -> [UInt8] + { + guard encoded.count % 4 == 0 else { throw JSONHTMLPreviewRejection.syntax } + func digit(_ b: UInt8) -> UInt32? { + switch b { + case 65...90: return UInt32(b - 65) + case 97...122: return UInt32(b - 71) + case 48...57: return UInt32(b + 4) + case 43: return 62 + case 47: return 63 + default: return nil + } } - } - for n in 0..> UInt32(16 - 8 * n) & 255)) - } - i += 4 - } - return bytes - } - private static func dimensions( - _ data: [UInt8], type: JSONHTMLRasterMediaType, policy: JSONHTMLPreviewPolicy, - meter: inout JSONWorkMeter - ) throws -> (UInt32, UInt32, Bool) { - var scanned: UInt64 = 0 - func read(_ i: Int, _ m: inout JSONWorkMeter) throws -> UInt8 { - try m.work() - guard i >= 0, i < data.count, scanned < policy.maxImageHeaderBytes else { - throw JSONHTMLPreviewRejection.eligibility - } - scanned += 1 - return data[i] - } - func u32(_ i: Int, _ m: inout JSONWorkMeter) throws -> UInt32 { - var n: UInt32 = 0 - for x in i..<(i + 4) { n = n << 8 | UInt32(try read(x, &m)) } - return n - } - if type == .png { - guard data.count >= 45 else { throw JSONHTMLPreviewRejection.syntax } - for (i, b) in [UInt8(137), 80, 78, 71, 13, 10, 26, 10].enumerated() { - guard try read(i, &meter) == b else { throw JSONHTMLPreviewRejection.syntax } - } - guard try u32(8, &meter) == 13 else { throw JSONHTMLPreviewRejection.syntax } - for (i, b) in Array("IHDR".utf8).enumerated() { - guard try read(i + 12, &meter) == b else { throw JSONHTMLPreviewRejection.syntax } - } - let width = try u32(16, &meter) - let height = try u32(20, &meter) - let depth = try read(24, &meter) - let color = try read(25, &meter) - guard pngDepths[color]?.contains(depth) == true, try read(26, &meter) == 0, - try read(27, &meter) == 0, try read(28, &meter) <= 1 - else { throw JSONHTMLPreviewRejection.syntax } - // Owned IHDR CRC check; compressed raster validation is deliberately separate. - var crc: UInt32 = 0xffff_ffff - for i in 12..<29 { - crc ^= UInt32(try read(i, &meter)) - for _ in 0..<8 { - try meter.work() - crc = crc & 1 == 1 ? (crc >> 1) ^ 0xedb8_8320 : crc >> 1 - } - } - guard crc ^ 0xffff_ffff == (try u32(29, &meter)) else { - throw JSONHTMLPreviewRejection.syntax - } - var at = 33 - var seenIDAT = false - var ended = false - while at < data.count { - guard data.count - at >= 12 else { throw JSONHTMLPreviewRejection.syntax } - let length = UInt64(try u32(at, &meter)) - guard length <= UInt64(data.count - at) - 12 else { throw JSONHTMLPreviewRejection.syntax } - let tag = try u32(at + 4, &meter) - if tag == 0x6163_544c { throw JSONHTMLPreviewRejection.syntax } // acTL: no animation claim. - if tag == 0x4948_4452 { throw JSONHTMLPreviewRejection.syntax } - if tag == 0x4944_4154 { seenIDAT = true } - if tag == 0x4945_4e44 { - guard length == 0, seenIDAT, at + 12 == data.count else { - throw JSONHTMLPreviewRejection.syntax - } - ended = true - break + var bytes: [UInt8] = [] + var i = 0 + while i < encoded.count { + try meter.work(4) + guard let a = digit(encoded[i]), let b = digit(encoded[i + 1]) else { + throw JSONHTMLPreviewRejection.syntax + } + let c = encoded[i + 2] + let d = encoded[i + 3] + let last = i + 4 == encoded.count + var value = a << 18 | b << 12 + let emitted: Int + if c == 61 { + guard last, d == 61, b & 15 == 0 else { throw JSONHTMLPreviewRejection.syntax } + emitted = 1 + } else { + guard let cc = digit(c) else { throw JSONHTMLPreviewRejection.syntax } + value |= cc << 6 + if d == 61 { + guard last, cc & 3 == 0 else { throw JSONHTMLPreviewRejection.syntax } + emitted = 2 + } else { + guard let dd = digit(d) else { throw JSONHTMLPreviewRejection.syntax } + value |= dd + emitted = 3 + } + } + for n in 0..> UInt32(16 - 8 * n) & 255)) + } + i += 4 } - at += Int(length) + 12 - } - guard ended else { throw JSONHTMLPreviewRejection.syntax } - return (width, height, false) + return bytes } - guard data.count >= 4, try read(0, &meter) == 255, try read(1, &meter) == 216, - data[data.count - 2] == 255, data.last == 217 - else { throw JSONHTMLPreviewRejection.syntax } - var at = 2 - while at < data.count { - guard try read(at, &meter) == 255 else { throw JSONHTMLPreviewRejection.syntax } - at += 1 - while try at < data.count && read(at, &meter) == 255 { at += 1 } - let marker = try read(at, &meter) - at += 1 - guard marker != 0, marker != 0xd9, marker != 0xda else { - throw JSONHTMLPreviewRejection.syntax - } - if marker == 0x01 || (0xd0...0xd7).contains(marker) { continue } - let length = Int(try read(at, &meter)) * 256 + Int(try read(at + 1, &meter)) - guard length >= 2, length <= data.count - at else { throw JSONHTMLPreviewRejection.syntax } - if marker == 0xc0 || marker == 0xc2 { - guard length >= 8, try read(at + 2, &meter) == 8 else { - throw JSONHTMLPreviewRejection.syntax + private static func dimensions( + _ data: [UInt8], type: JSONHTMLRasterMediaType, policy: JSONHTMLPreviewPolicy, + meter: inout JSONWorkMeter + ) throws -> (UInt32, UInt32, Bool) { + var scanned: UInt64 = 0 + func read(_ i: Int, _ m: inout JSONWorkMeter) throws -> UInt8 { + try m.work() + guard i >= 0, i < data.count, scanned < policy.maxImageHeaderBytes else { + throw JSONHTMLPreviewRejection.eligibility + } + scanned += 1 + return data[i] + } + func u32(_ i: Int, _ m: inout JSONWorkMeter) throws -> UInt32 { + var n: UInt32 = 0 + for x in i..<(i + 4) { n = n << 8 | UInt32(try read(x, &m)) } + return n } - let height = UInt32(try read(at + 3, &meter)) * 256 + UInt32(try read(at + 4, &meter)) - let width = UInt32(try read(at + 5, &meter)) * 256 + UInt32(try read(at + 6, &meter)) - let components = Int(try read(at + 7, &meter)) - guard [1, 3, 4].contains(components), length == 8 + 3 * components else { - throw JSONHTMLPreviewRejection.syntax + if type == .png { + guard data.count >= 45 else { throw JSONHTMLPreviewRejection.syntax } + for (i, b) in [UInt8(137), 80, 78, 71, 13, 10, 26, 10].enumerated() { + guard try read(i, &meter) == b else { throw JSONHTMLPreviewRejection.syntax } + } + guard try u32(8, &meter) == 13 else { throw JSONHTMLPreviewRejection.syntax } + for (i, b) in Array("IHDR".utf8).enumerated() { + guard try read(i + 12, &meter) == b else { throw JSONHTMLPreviewRejection.syntax } + } + let width = try u32(16, &meter) + let height = try u32(20, &meter) + let depth = try read(24, &meter) + let color = try read(25, &meter) + guard pngDepths[color]?.contains(depth) == true, try read(26, &meter) == 0, + try read(27, &meter) == 0, try read(28, &meter) <= 1 + else { throw JSONHTMLPreviewRejection.syntax } + // Owned IHDR CRC check; compressed raster validation is deliberately separate. + var crc: UInt32 = 0xffff_ffff + for i in 12..<29 { + crc ^= UInt32(try read(i, &meter)) + for _ in 0..<8 { + try meter.work() + crc = crc & 1 == 1 ? (crc >> 1) ^ 0xedb8_8320 : crc >> 1 + } + } + guard crc ^ 0xffff_ffff == (try u32(29, &meter)) else { + throw JSONHTMLPreviewRejection.syntax + } + var at = 33 + var seenIDAT = false + var ended = false + while at < data.count { + guard data.count - at >= 12 else { throw JSONHTMLPreviewRejection.syntax } + let length = UInt64(try u32(at, &meter)) + guard length <= UInt64(data.count - at) - 12 else { + throw JSONHTMLPreviewRejection.syntax + } + let tag = try u32(at + 4, &meter) + if tag == 0x6163_544c { throw JSONHTMLPreviewRejection.syntax } // acTL: no animation claim. + if tag == 0x4948_4452 { throw JSONHTMLPreviewRejection.syntax } + if tag == 0x4944_4154 { seenIDAT = true } + if tag == 0x4945_4e44 { + guard length == 0, seenIDAT, at + 12 == data.count else { + throw JSONHTMLPreviewRejection.syntax + } + ended = true + break + } + at += Int(length) + 12 + } + guard ended else { throw JSONHTMLPreviewRejection.syntax } + return (width, height, false) } - for n in 0..> 4), (1...4).contains(sampling & 15), - try read(at + 10 + n * 3, &meter) <= 3 - else { throw JSONHTMLPreviewRejection.syntax } + guard data.count >= 4, try read(0, &meter) == 255, try read(1, &meter) == 216, + data[data.count - 2] == 255, data.last == 217 + else { throw JSONHTMLPreviewRejection.syntax } + var at = 2 + while at < data.count { + guard try read(at, &meter) == 255 else { throw JSONHTMLPreviewRejection.syntax } + at += 1 + while try at < data.count && read(at, &meter) == 255 { at += 1 } + let marker = try read(at, &meter) + at += 1 + guard marker != 0, marker != 0xd9, marker != 0xda else { + throw JSONHTMLPreviewRejection.syntax + } + if marker == 0x01 || (0xd0...0xd7).contains(marker) { continue } + let length = Int(try read(at, &meter)) * 256 + Int(try read(at + 1, &meter)) + guard length >= 2, length <= data.count - at else { + throw JSONHTMLPreviewRejection.syntax + } + if marker == 0xc0 || marker == 0xc2 { + guard length >= 8, try read(at + 2, &meter) == 8 else { + throw JSONHTMLPreviewRejection.syntax + } + let height = + UInt32(try read(at + 3, &meter)) * 256 + UInt32(try read(at + 4, &meter)) + let width = + UInt32(try read(at + 5, &meter)) * 256 + UInt32(try read(at + 6, &meter)) + let components = Int(try read(at + 7, &meter)) + guard [1, 3, 4].contains(components), length == 8 + 3 * components else { + throw JSONHTMLPreviewRejection.syntax + } + for n in 0..> 4), (1...4).contains(sampling & 15), + try read(at + 10 + n * 3, &meter) <= 3 + else { throw JSONHTMLPreviewRejection.syntax } + } + return (width, height, marker == 0xc2) + } + // Count skipped header bytes as scan work before advancing a bounded segment. + guard UInt64(length) <= policy.maxImageHeaderBytes - scanned else { + throw JSONHTMLPreviewRejection.eligibility + } + try meter.work(UInt64(length)) + scanned += UInt64(length) + at += length } - return (width, height, marker == 0xc2) - } - // Count skipped header bytes as scan work before advancing a bounded segment. - guard UInt64(length) <= policy.maxImageHeaderBytes - scanned else { - throw JSONHTMLPreviewRejection.eligibility - } - try meter.work(UInt64(length)) - scanned += UInt64(length) - at += length + throw JSONHTMLPreviewRejection.syntax } - throw JSONHTMLPreviewRejection.syntax - } } diff --git a/Sources/JSONHTML/JSONHTMLStructured.swift b/Sources/JSONHTML/JSONHTMLStructured.swift index 1670568..b75a20f 100644 --- a/Sources/JSONHTML/JSONHTMLStructured.swift +++ b/Sources/JSONHTML/JSONHTMLStructured.swift @@ -2,467 +2,506 @@ import JSON private enum StructuredStep { - case node(JSON, JSONLocationComponent?, Int) - case end(String, Bool) - case objectMember(JSON, JSONKey, Int) - case arrayMember(JSON, Int, Int, Bool) + case node(JSON, JSONLocationComponent?, Int) + case end(String, Bool) + case objectMember(JSON, JSONKey, Int) + case arrayMember(JSON, Int, Int, Bool, Bool) } private struct StructuredOutline { - let ordinal: UInt64 - let component: JSONLocationComponent? - let depth: Int + let ordinal: UInt64 + let component: JSONLocationComponent? + let depth: Int } extension JSONHTMLRenderer { - package func renderRichDocument( - _ value: JSON, annotations: [JSONHTMLBoundAnnotation], meter: inout JSONWorkMeter - ) throws -> JSONHTMLDocumentBuffer { - var planning = options - if options.layout == .syntax { - // An explicit syntax invocation never invokes the inline/media pipeline. - planning.inlineFormatting = .off - planning.colorPreviews = false - planning.embeddedImagePreviews = false - planning.previewHints = [] - planning.effectivePreviewHints = [] - } - let plan = try JSONHTMLPreviewPlan.make(value, options: planning, meter: &meter) - var output = JSONHTMLDocumentBuffer(limits: options.limits) - output.setAnnotations(UInt64(annotations.count)) - output.setPreviews( - shortened: plan.shortened && options.valueProfile == .inspection - && options.layout == .structured, issues: UInt64(plan.issues.count), - images: plan.media.images, imageBytes: plan.media.bytes) - let interactive = options.presentation == .interactive - try output.addDOM(110, meter: &meter) - try output.append( - " 0 { try output.append("img-src data:; ", meter: &meter) } - try output.append( - "base-uri 'none'; form-action 'none'; object-src 'none'\">", meter: &meter) - try output.escaped(options.title, meter: &meter) - try output.append("", meter: &meter) - if interactive { - try output.append("", meter: &meter) - } - try output.append("\n", meter: &meter) - try meter.checkpoint() - return output - } - private var writingOptions: JSONWritingOptions { - .init( - limits: options.limits.core, indentation: options.indentation, numberStyle: .preserved, - indentationUnit: options.indentationUnit) - } - private func scalarFlow(_ values: [JSON], meter: inout JSONWorkMeter) throws -> Bool { - guard values.count <= 10 else { return false } - for value in values { - try meter.work() - var length: UInt64 = 0 - switch value { - case .array, .object: return false - case .string(let s): - length = 2 - for scalar in s.unicodeScalars { - try meter.work() - length += JSONHTMLDisplay.units(scalar) - if length > 60 { return false } - } - default: - var cursor = JSONWritingCursor(value: value, options: writingOptions) - while (try cursor.next(meter: &meter)) != nil { - length += 1 - if length > 60 { return false } - } - } - if length > 60 { return false } - } - return true - } - private func structuredBody( - _ value: JSON, plan: JSONHTMLPreviewPlan, profile: JSONHTMLValueProfile, namespace: String, - interactive: Bool, countNodes: Bool, annotations: [JSONHTMLBoundAnnotation], - outline: inout [StructuredOutline], output: inout JSONHTMLDocumentBuffer, - meter: inout JSONWorkMeter - ) throws { - var stack: [StructuredStep] = [] - var path: [JSONLocationComponent] = [] - var ordinal: UInt64 = 0 - func push(_ step: StructuredStep, _ m: inout JSONWorkMeter) throws { - try m.retain(JSONLogicalWeights.frame) - stack.append(step) - } - try push(.node(value, nil, 0), &meter) - while let step = stack.popLast() { - try meter.release(JSONLogicalWeights.frame) - try meter.work() - try meter.retain(256) - switch step { - case .end(let markup, let pop): - try output.append(markup, meter: &meter) - if pop { - path.removeLast() - try meter.release(JSONLogicalWeights.frame) + package func renderRichDocument( + _ value: JSON, annotations: [JSONHTMLBoundAnnotation], meter: inout JSONWorkMeter + ) throws -> JSONHTMLDocumentBuffer { + var planning = options + if options.layout == .syntax { + // An explicit syntax invocation never invokes the inline/media pipeline. + planning.inlineFormatting = .off + planning.colorPreviews = false + planning.embeddedImagePreviews = false + planning.previewHints = [] + planning.effectivePreviewHints = [] } - case .objectMember(let child, let key, let depth): - try output.addDOM(6, meter: &meter) + let plan = try JSONHTMLPreviewPlan.make(value, options: planning, meter: &meter) + var output = JSONHTMLDocumentBuffer(limits: options.limits) + output.setAnnotations(UInt64(annotations.count)) + output.setPreviews( + shortened: plan.shortened && options.valueProfile == .inspection + && options.layout == .structured, issues: UInt64(plan.issues.count), + images: plan.media.images, imageBytes: plan.media.bytes) + let interactive = options.presentation == .interactive + try output.addDOM(110, meter: &meter) try output.append( - "
    ", meter: &meter) - try JSONHTMLDisplay.text(key.string, quoted: true, output: &output, meter: &meter) - if key.string.isEmpty { - try output.append(" Empty key", meter: &meter) + "
    ", meter: &meter) - try push(.end("
    ", false), &meter) - try push(.node(child, .key(key), depth), &meter) - case .arrayMember(let child, let index, let depth, let flow): - try output.addDOM(6, meter: &meter) try output.append( - flow - ? "
  • Index " - : "
  • #", meter: &meter) - try output.decimal(UInt64(index), meter: &meter) - try output.append("", meter: &meter) - try push(.end("
  • ", false), &meter) - try push(.node(child, .index(index), depth), &meter) - case .node(let node, let component, let depth): - if let component { - try meter.retain(JSONLogicalWeights.frame) - path.append(component) - } - let id = ordinal - ordinal = try JSONHTMLDocumentBuffer.adding( - .renderedJSONNodes, ordinal, 1, options.limits.maxRenderedJSONNodes) - if countNodes { try output.addNode(meter: &meter) } - let container: Bool - let empty: Bool - let kind: String - switch node { - case .object(let o): - container = true - empty = o.isEmpty - kind = "object" - case .array(let a): - container = true - empty = a.isEmpty - kind = "array" - case .string: - container = false - empty = false - kind = "string" - case .number: - container = false - empty = false - kind = "number" - case .boolean: - container = false - empty = false - kind = "bool" - case .null: - container = false - empty = false - kind = "null" - } - if interactive, container, let maxDepth = options.outlineDepth, depth <= maxDepth { - try output.addOutline(meter: &meter) - try meter.retain(40) - outline.append(.init(ordinal: id, component: component, depth: depth)) - } - try output.addDOM(24, meter: &meter) + "; style-src 'sha256-" + JSONHTMLInteractiveAssets.styleHash + "'; ", meter: &meter) + if plan.media.images > 0 { try output.append("img-src data:; ", meter: &meter) } + try output.append( + "base-uri 'none'; form-action 'none'; object-src 'none'\">", meter: &meter) + try output.escaped(options.title, meter: &meter) + try output.append("

    ", meter: &meter) + try output.escaped(options.title, meter: &meter) + try output.append("

    ", meter: &meter) try output.append( - container && !empty - ? (interactive - ? "

    ", meter: &meter) + let copy = try nativeBytes(value, options: writingOptions, meter: &meter) + output.setCopyBytes(UInt64(copy.count)) + try output.append( + "

    Download exact JSON (UTF-8; works without JavaScript)

    ", meter: &meter) if interactive { - let pointer = try pointerBytes(path, meter: &meter) - try output.append(" data-pointer=\"", meter: &meter) - try output.base64(pointer, meter: &meter) - for _ in pointer { - try meter.work() - try meter.retain(1) - } - let text = String(decoding: pointer, as: UTF8.self) - let quoted = try nativeBytes(.string(text), options: writingOptions, meter: &meter) - try meter.release(UInt64(pointer.count)) - try output.append("\" data-pointer-escaped=\"", meter: &meter) - try output.base64(quoted, meter: &meter) - try meter.release(UInt64(quoted.count)) - try meter.release(UInt64(pointer.count)) - try output.append("\"", meter: &meter) + try output.append( + "

      ", meter: &meter) + } + try meter.release(UInt64(copy.count)) + var outline: [StructuredOutline] = [] + if options.layout == .structured || interactive { + try output.append( + "
      ", + meter: &meter) + try structuredBody( + value, plan: plan, profile: options.valueProfile, namespace: "jh-n-", + interactive: interactive, countNodes: true, annotations: annotations, + outline: &outline, + output: &output, meter: &meter) + try output.append("
      ", meter: &meter) + if interactive, options.outlineDepth != nil { + try output.append( + "", meter: &meter) + } + try output.append("
      ", meter: &meter) } - if interactive && container && !empty && depth < options.initiallyExpandedDepth { - try output.append(" open", meter: &meter) + if interactive || options.layout == .syntax { + try output.append( + "

      Exact JSON source

      ",
      +                meter: &meter)
      +            try highlighted(value, options: writingOptions, output: &output, meter: &meter)
      +            try output.append("
      ", meter: &meter) + } else { + try output.append( + "
      Exact JSON source (complete)
      ",
      +                meter: &meter)
      +            try highlighted(value, options: writingOptions, output: &output, meter: &meter)
      +            try output.append("
      ", meter: &meter) + } + if !plan.issues.isEmpty { + try output.addDOM(UInt64(plan.issues.count) * 4 + 4, meter: &meter) + try output.append( + "

      Preview issues

        ", + meter: &meter) + for issue in plan.issues { + try meter.work() + try output.append("
      • ", meter: &meter) + let pointer = try pointerBytes(issue.path, meter: &meter) + try output.escaped(String(decoding: pointer, as: UTF8.self), meter: &meter) + try meter.release(UInt64(pointer.count)) + try output.append(": ", meter: &meter) + try output.escaped(issue.message, meter: &meter) + try output.append("
      • ", meter: &meter) + } + try output.append("
      ", meter: &meter) } - try annotationDescription(at: id, in: annotations, output: &output, meter: &meter) + try annotationSection(annotations, output: &output, meter: &meter) + // Printing is a separately charged representation of the same immutable plan/root. try output.append( - container && !empty - ? (interactive ? ">" : ">
      ") : ">", - meter: &meter) - if !container { - try output.append("", meter: &meter) - try output.append( - kind == "bool" - ? "Boolean" : kind == "null" ? "Null" : kind == "string" ? "String" : "Number", + "
      ", meter: &meter) - try output.append("", meter: &meter) - } - if container { - try output.append("", meter: &meter) - try output.append(empty ? "Empty " + kind : kind, meter: &meter) - switch node { - case .object(let o): - try output.append(" · ", meter: &meter) - try output.decimal(UInt64(o.count), meter: &meter) - try output.append(" members", meter: &meter) - case .array(let a): - try output.append(" · ", meter: &meter) - try output.decimal(UInt64(a.count), meter: &meter) - try output.append(" items", meter: &meter) - default: break - } - try output.append("", meter: &meter) + if options.layout == .structured { + var ignored: [StructuredOutline] = [] + try structuredBody( + value, plan: plan, profile: options.printValueProfile, namespace: "jh-p-", + interactive: false, countNodes: false, annotations: [], outline: &ignored, + output: &output, + meter: &meter) + } else { + try output.append("
      ", meter: &meter)
      +            try highlighted(value, options: writingOptions, output: &output, meter: &meter)
      +            try output.append("
      ", meter: &meter) } + try meter.release(plan.reservation) + try output.append("
      ", meter: &meter) if interactive { - try output.append( - "", meter: &meter) + try output.append("", meter: &meter) } - let preview = plan.nodes[Int(id)] - if case .color(let color) = preview.primary { - try color.emit( - ordinal: id, namespace: namespace, label: preview.prefix, output: &output, meter: &meter - ) + try output.append("\n", meter: &meter) + try meter.checkpoint() + return output + } + private var writingOptions: JSONWritingOptions { + .init( + limits: options.limits.core, indentation: options.indentation, numberStyle: .preserved, + indentationUnit: options.indentationUnit) + } + private func scalarFlow(_ values: [JSON], meter: inout JSONWorkMeter) throws -> Bool { + guard values.count <= 10 else { return false } + for value in values { + try meter.work() + var length: UInt64 = 0 + switch value { + case .array, .object: return false + case .string(let s): + length = 2 + for scalar in s.unicodeScalars { + try meter.work() + length += JSONHTMLDisplay.units(scalar) + if length > 60 { return false } + } + default: + var cursor = JSONWritingCursor(value: value, options: writingOptions) + while (try cursor.next(meter: &meter)) != nil { + length += 1 + if length > 60 { return false } + } + } + if length > 60 { return false } } - if case .image(let image) = preview.primary { - try image.emit(output: &output, meter: &meter) + return true + } + private func structuredBody( + _ value: JSON, plan: JSONHTMLPreviewPlan, profile: JSONHTMLValueProfile, namespace: String, + interactive: Bool, countNodes: Bool, annotations: [JSONHTMLBoundAnnotation], + outline: inout [StructuredOutline], output: inout JSONHTMLDocumentBuffer, + meter: inout JSONWorkMeter + ) throws { + var stack: [StructuredStep] = [] + var path: [JSONLocationComponent] = [] + var ordinal: UInt64 = 0 + func push(_ step: StructuredStep, _ m: inout JSONWorkMeter) throws { + try m.retain(JSONLogicalWeights.frame) + stack.append(step) } - switch node { - case .object(let members) where !members.isEmpty: - try output.append( - interactive - ? "
      " : "
      ", - meter: &meter) - try push( - .end(interactive ? "
      " : "
      ", component != nil), &meter) - for member in members.reversed() { + try push(.node(value, nil, 0), &meter) + while let step = stack.popLast() { + try meter.release(JSONLogicalWeights.frame) try meter.work() - try push(.objectMember(member.value, member.key, depth + 1), &meter) - } - case .array(let values) where !values.isEmpty: - let flow = try scalarFlow(values, meter: &meter) - try output.append(interactive ? "" : "", meter: &meter) - try output.append( - flow ? "
        " : "
          ", meter: &meter) - try push( - .end(interactive ? "
        " : "
      ", component != nil), &meter) - for index in values.indices.reversed() { - try meter.work() - try push(.arrayMember(values[index], index, depth + 1, flow), &meter) - } - case .string(let string): - let prefix = preview.prefix! - let usePrefix = profile == .inspection - try output.append("", meter: &meter) - if case .inline(let inline) = preview.primary, inline.formatted { - try output.append("Inline preview: ", meter: &meter) - try inline.emit(output: &output, meter: &meter) - } else { - try JSONHTMLDisplay.text( - usePrefix ? prefix.source : string, quoted: true, output: &output, meter: &meter) - } - try output.append("", meter: &meter) - if prefix.shortened - && (usePrefix - || { - if case .inline = preview.primary { return true } - return false - }()) - { - try output.addDOM(4, meter: &meter) - try output.append( - "… String preview · first ", meter: &meter) - try output.decimal(prefix.scalars, meter: &meter) - try output.append(" decoded scalars (bounded display)", meter: &meter) - } - let needsOriginal: Bool - switch preview.primary { - case .literal: needsOriginal = usePrefix && prefix.shortened - default: needsOriginal = true - } - if needsOriginal { - try output.addDOM(6, meter: &meter) - if profile == .complete { - try output.append( - "
      Original value (complete)", - meter: &meter) - } else { - try output.append( - "
      Original value (complete)", - meter: &meter) + try meter.retain(256) + switch step { + case .end(let markup, let pop): + try output.append(markup, meter: &meter) + if pop { + path.removeLast() + try meter.release(JSONLogicalWeights.frame) + } + case .objectMember(let child, let key, let depth): + try output.addDOM(6, meter: &meter) + try output.append( + "
      ", + meter: &meter) + try JSONHTMLDisplay.text(key.string, quoted: true, output: &output, meter: &meter) + if key.string.isEmpty { + try output.append( + " Empty key", meter: &meter) + } + try output.append("
      ", meter: &meter) + try push(.end("
      ", false), &meter) + try push(.node(child, .key(key), depth), &meter) + case .arrayMember(let child, let index, let depth, let flow, let records): + try output.addDOM(6, meter: &meter) + try output.append( + flow + ? "
    • Index " + : records + ? "
    • #" + : "
    • #", + meter: &meter) + try output.decimal(UInt64(index), meter: &meter) + try output.append("", meter: &meter) + try push(.end("
    • ", false), &meter) + try push(.node(child, .index(index), depth), &meter) + case .node(let node, let component, let depth): + if let component { + try meter.retain(JSONLogicalWeights.frame) + path.append(component) + } + let id = ordinal + ordinal = try JSONHTMLDocumentBuffer.adding( + .renderedJSONNodes, ordinal, 1, options.limits.maxRenderedJSONNodes) + if countNodes { try output.addNode(meter: &meter) } + let container: Bool + let empty: Bool + let kind: String + switch node { + case .object(let o): + container = true + empty = o.isEmpty + kind = "object" + case .array(let a): + container = true + empty = a.isEmpty + kind = "array" + case .string: + container = false + empty = false + kind = "string" + case .number: + container = false + empty = false + kind = "number" + case .boolean: + container = false + empty = false + kind = "bool" + case .null: + container = false + empty = false + kind = "null" + } + if interactive, container, let maxDepth = options.outlineDepth, depth <= maxDepth { + try output.addOutline(meter: &meter) + try meter.retain(40) + outline.append(.init(ordinal: id, component: component, depth: depth)) + } + try output.addDOM(24, meter: &meter) + try output.append( + container && !empty + ? (interactive + ? "
      " : ">
      ") + : ">", + meter: &meter) + if !container { + try output.append("", meter: &meter) + try output.append( + kind == "bool" + ? "Boolean" + : kind == "null" ? "Null" : kind == "string" ? "String" : "Number", + meter: &meter) + try output.append("", meter: &meter) + } + if container { + try output.append("", meter: &meter) + try output.append(empty ? "Empty " + kind : kind, meter: &meter) + switch node { + case .object(let o): + try output.append(" · ", meter: &meter) + try output.decimal(UInt64(o.count), meter: &meter) + try output.append(" members", meter: &meter) + case .array(let a): + try output.append(" · ", meter: &meter) + try output.decimal(UInt64(a.count), meter: &meter) + try output.append(" items", meter: &meter) + default: break + } + try output.append("", meter: &meter) + } + if interactive { + try output.append( + "", meter: &meter) + } + let preview = plan.nodes[Int(id)] + if case .color(let color) = preview.primary { + try color.emit( + ordinal: id, namespace: namespace, label: preview.prefix, output: &output, + meter: &meter + ) + } + if case .image(let image) = preview.primary { + try image.emit(output: &output, meter: &meter) + } + switch node { + case .object(let members) where !members.isEmpty: + try output.append( + interactive + ? "
      " + : "
      ", + meter: &meter) + try push( + .end(interactive ? "
      " : "", component != nil), + &meter) + for member in members.reversed() { + try meter.work() + try push(.objectMember(member.value, member.key, depth + 1), &meter) + } + case .array(let values) where !values.isEmpty: + let flow = try scalarFlow(values, meter: &meter) + var records = false + for child in values { + try meter.work() + if case .object = child { + records = true + break + } + if case .array = child { + records = true + break + } + } + try output.append(interactive ? "" : "", meter: &meter) + try output.append( + flow ? "
        " : "
          ", + meter: &meter) + try push( + .end(interactive ? "
      " : "", component != nil), + &meter) + for index in values.indices.reversed() { + try meter.work() + try push( + .arrayMember(values[index], index, depth + 1, flow, records), &meter) + } + case .string(let string): + let prefix = preview.prefix! + let usePrefix = profile == .inspection + try output.append("", meter: &meter) + if case .inline(let inline) = preview.primary, inline.formatted { + try output.append( + "Inline preview: ", meter: &meter) + try inline.emit(output: &output, meter: &meter) + } else { + try JSONHTMLDisplay.text( + usePrefix ? prefix.source : string, quoted: true, output: &output, + meter: &meter) + } + try output.append("", meter: &meter) + if prefix.shortened + && (usePrefix + || { + if case .inline = preview.primary { return true } + return false + }()) + { + try output.addDOM(4, meter: &meter) + try output.append( + "… String preview · first ", meter: &meter + ) + try output.decimal(prefix.scalars, meter: &meter) + try output.append( + " decoded scalars (bounded display)", meter: &meter) + } + let needsOriginal: Bool + switch preview.primary { + case .literal: needsOriginal = usePrefix && prefix.shortened + default: needsOriginal = true + } + if needsOriginal { + try output.addDOM(6, meter: &meter) + if profile == .complete { + try output.append( + "
      Original value (complete)", + meter: &meter) + } else { + try output.append( + "
      Original value (complete)", + meter: &meter) + } + try JSONHTMLDisplay.text( + string, quoted: true, output: &output, meter: &meter) + try output.append( + profile == .complete ? "
      " : "", + meter: &meter) + } + try push(.end("
      ", component != nil), &meter) + default: + try output.append("", meter: &meter) + if empty { + try output.append(kind == "object" ? "{}" : "[]", meter: &meter) + } else { + try highlighted( + node, options: writingOptions, output: &output, meter: &meter) + } + try output.append("", meter: &meter) + try push(.end("", component != nil), &meter) + } } - try JSONHTMLDisplay.text(string, quoted: true, output: &output, meter: &meter) - try output.append( - profile == .complete ? "" : "
      ", meter: &meter) - } - try push(.end("", component != nil), &meter) - default: - try output.append("", meter: &meter) - if empty { - try output.append(kind == "object" ? "{}" : "[]", meter: &meter) - } else { - try highlighted(node, options: writingOptions, output: &output, meter: &meter) - } - try output.append("", meter: &meter) - try push(.end("", component != nil), &meter) + try meter.release(256) } - } - try meter.release(256) } - } } diff --git a/Tests/JSONCLITests/HTMLPreviewTests.swift b/Tests/JSONCLITests/HTMLPreviewTests.swift index 364e874..50c7ade 100644 --- a/Tests/JSONCLITests/HTMLPreviewTests.swift +++ b/Tests/JSONCLITests/HTMLPreviewTests.swift @@ -7,57 +7,60 @@ import Testing @testable import JSONFilterRuntime @Test(arguments: [ - ["render", "--layout", "bad"], ["render", "--string-preview", "-1"], - ["render", "--string-preview", "4611686018427387904"], - ["render", "--string-preview", "0", "--full-values"], - ["render", "--inline-formatting", "markdown"], ["render", "--value-previews", "remote"], - ["render", "--format", "text", "--layout", "structured"], - ["render", "--format", "markdown", "--full-values"], - ["render", "--format", "text", "--string-preview", "0"], - ["render", "--format", "text", "--inline-formatting", "off"], - ["render", "--format", "text", "--value-previews", "off"], + ["render", "--layout", "bad"], ["render", "--string-preview", "-1"], + ["render", "--string-preview", "4611686018427387904"], + ["render", "--string-preview", "0", "--full-values"], + ["render", "--inline-formatting", "markdown"], ["render", "--value-previews", "remote"], + ["render", "--format", "text", "--layout", "structured"], + ["render", "--format", "markdown", "--full-values"], + ["render", "--format", "text", "--string-preview", "0"], + ["render", "--format", "text", "--inline-formatting", "off"], + ["render", "--format", "text", "--value-previews", "off"], ]) func cliHTMLPreviewFlagsRejectUsageBeforeInput(argv: [String]) throws { - var context = try CLIJobContext() - do { - _ = try CLIArguments(argv, context: &context) - Issue.record("Expected usage failure") - } catch let error as CLIError { #expect(error.exit == 2) } - #expect(context.used(.totalInputBytes) == 0 && context.committedResults == 0) + var context = try CLIJobContext() + do { + _ = try CLIArguments(argv, context: &context) + Issue.record("Expected usage failure") + } catch let error as CLIError { #expect(error.exit == 2) } + #expect(context.used(.totalInputBytes) == 0 && context.committedResults == 0) } @Test func cliHTMLPreviewZeroCompleteAndSyntaxShareAtomicOutput() throws { - let root = FileManager.default.temporaryDirectory.appendingPathComponent( - "html-preview-" + UUID().uuidString) - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) - defer { try? FileManager.default.removeItem(at: root) } - let input = root.appendingPathComponent("input.json") - let output = root.appendingPathComponent("output.html") - try Data("\"**bold** $x_i^2$ TAIL\"".utf8).write(to: input) - for flags in [ - ["--string-preview", "0"], ["--full-values"], ["--layout", "syntax"], - ["--inline-formatting", "off", "--value-previews", "off"], - ] { - var context = try CLIJobContext() + let root = FileManager.default.temporaryDirectory.appendingPathComponent( + "html-preview-" + UUID().uuidString) + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: false) + defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("input.json") + let output = root.appendingPathComponent("output.html") + try Data("\"**bold** $x_i^2$ TAIL\"".utf8).write(to: input) + for flags in [ + ["--string-preview", "0"], ["--full-values"], ["--layout", "syntax"], + ["--inline-formatting", "off", "--value-previews", "off"], + ] { + var context = try CLIJobContext() + let args = try CLIArguments( + ["render", input.path, "--output", output.path, "--overwrite"] + flags, + context: &context) + #expect(try CLIEngine.execute(args, context: &context) == 0) + let html = String(decoding: try Data(contentsOf: output), as: UTF8.self) + #expect(!html.contains(""}"# + let source = + #"{"é":9007199254740993,"é":-0.00e+2,"empty":[],"end":"LAST-CONTENT-SENTINEL😀"}"# let value = try JSONParser().parse(source) - let report = try JSONExportDocument.prepareWithReport(value, options: .init(filenameStem: "Exact-JSON")) + let report = try JSONExportDocument.prepareWithReport( + value, options: .init(filenameStem: "Exact-JSON")) let item = report.result try check("available-four", item.availableFormats == [.json, .plainText, .markdown, .html]) try check("advertised-four", item.advertisedFormats == item.availableFormats) try check("one-root-admission", report.operation[.nodes] == 5) - try check("aggregate-artifact-sum", report.preparedBytes == report.representationBytes.values.reduce(0, +)) - try check("exact-native-roundtrip", try JSONWriter(options: .init(numberStyle: .preserved)).write(JSONParser().parse(item.data(for: .json))) == Data(source.utf8)) + try check( + "aggregate-artifact-sum", + report.preparedBytes == report.representationBytes.values.reduce(0, +)) + try check( + "exact-native-roundtrip", + try JSONWriter(options: .init(numberStyle: .preserved)).write( + JSONParser().parse(item.data(for: .json))) == Data(source.utf8)) try check("pretty-plain-body", try item.data(for: .json) == item.data(for: .plainText)) var samples: [String: Int] = [:] for format in [JSONExportFormat.json, .plainText, .markdown, .html] { let bytes = try item.data(for: format) let name = "Exact-JSON." + format.filenameExtension - try bytes.write(to: output.appendingPathComponent(name)); samples[name] = bytes.count + try bytes.write(to: output.appendingPathComponent(name)) + samples[name] = bytes.count let selected = try item.selecting([format]) - try check("selection-" + format.rawValue, selected.advertisedFormats == [format] && selected.availableFormats == item.availableFormats) + try check( + "selection-" + format.rawValue, + selected.advertisedFormats == [format] + && selected.availableFormats == item.availableFormats) } #if canImport(CoreTransferable) && canImport(UniformTypeIdentifiers) - try check("direct-native-import-only", JSON.importedContentTypes() == [.json]) - try check("prepared-export-only", JSONExportDocument.importedContentTypes().isEmpty) - for (format, type) in [(JSONExportFormat.json, UTType.json), (.html, .html), (.plainText, .utf8PlainText), (.markdown, .markdown)] { - let selected = try item.selecting([format]) - try check("instance-discovery-" + format.rawValue, selected.exportedContentTypes() == [type]) - try check("transfer-bytes-" + format.rawValue, try await selected.exported(as: type) == item.data(for: format)) - } - let fileDirectory = output.appendingPathComponent("SystemExport", isDirectory: true) - try FileManager.default.createDirectory(at: fileDirectory, withIntermediateDirectories: true) - let path = try await item.selecting([.json]).export(to: fileDirectory, contentType: .json) - try check("system-filename-and-file", path.lastPathComponent == "Exact-JSON.json" && Data(contentsOf: path) == item.data(for: .json)) - let imported = try await JSON(importing: path, contentType: .json) - try check("system-file-strict-import", try JSONWriter(options: .init(numberStyle: .preserved)).write(imported) == Data(source.utf8)) + try check("direct-native-import-only", JSON.importedContentTypes() == [.json]) + try check("prepared-export-only", JSONExportDocument.importedContentTypes().isEmpty) + for (format, type) in [ + (JSONExportFormat.json, UTType.json), (.html, .html), (.plainText, .utf8PlainText), + (.markdown, .markdown), + ] { + let selected = try item.selecting([format]) + try check( + "instance-discovery-" + format.rawValue, + selected.exportedContentTypes() == [type]) + try check( + "transfer-bytes-" + format.rawValue, + try await selected.exported(as: type) == item.data(for: format)) + } + let fileDirectory = output.appendingPathComponent("SystemExport", isDirectory: true) + try FileManager.default.createDirectory( + at: fileDirectory, withIntermediateDirectories: true) + let path = try await item.selecting([.json]).export( + to: fileDirectory, contentType: .json) + try check( + "system-filename-and-file", + path.lastPathComponent == "Exact-JSON.json" + && Data(contentsOf: path) == item.data(for: .json)) + let imported = try await JSON(importing: path, contentType: .json) + try check( + "system-file-strict-import", + try JSONWriter(options: .init(numberStyle: .preserved)).write(imported) + == Data(source.utf8)) #endif - let rich = try JSONExportDocument.prepareWithReport(value, options: .init(formats: [.json, .html, .rtf, .plainText, .markdown], filenameStem: "Exact-JSON")) - try check("rtf-explicit-five", rich.result.availableFormats == [.json, .html, .rtf, .plainText, .markdown]) + let rich = try JSONExportDocument.prepareWithReport( + value, + options: .init( + formats: [.json, .html, .rtf, .plainText, .markdown], filenameStem: "Exact-JSON")) + try check( + "rtf-explicit-five", + rich.result.availableFormats == [.json, .html, .rtf, .plainText, .markdown]) try check("rtf-one-admission", rich.operation[.nodes] == 5) try check("rtf-cumulative-work", rich.operation[.workUnits] > report.operation[.workUnits]) - try check("rtf-aggregate", rich.preparedBytes == rich.representationBytes.values.reduce(0, +)) + try check( + "rtf-aggregate", rich.preparedBytes == rich.representationBytes.values.reduce(0, +)) let rtfBytes = try rich.result.data(for: .rtf) - try rtfBytes.write(to: output.appendingPathComponent("Exact-JSON.rtf")); samples["Exact-JSON.rtf"] = rtfBytes.count + try rtfBytes.write(to: output.appendingPathComponent("Exact-JSON.rtf")) + samples["Exact-JSON.rtf"] = rtfBytes.count #if canImport(AppKit) || canImport(UIKit) - let decoded = try NSAttributedString(data: rtfBytes, options: [.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) - try check("rtf-native-text", try Data(decoded.string.utf8) == rich.result.data(for: .plainText) && decoded.length == decoded.string.utf16.count) - let key = (decoded.string as NSString).range(of: "\"é\"") - let number = (decoded.string as NSString).range(of: "9007199254740993") - #if canImport(AppKit) - let font = decoded.attribute(.font, at: key.location, effectiveRange: nil) as? NSFont - #else - let font = decoded.attribute(.font, at: key.location, effectiveRange: nil) as? UIFont - #endif - let keyColor = decoded.attribute(.foregroundColor, at: key.location, effectiveRange: nil) as? NSObject - let numberColor = decoded.attribute(.foregroundColor, at: number.location, effectiveRange: nil) as? NSObject - try check("rtf-native-styles", font?.fontName.contains("Menlo") == true && font?.pointSize == 11 && keyColor != nil && numberColor != nil && keyColor != numberColor) + let decoded = try NSAttributedString( + data: rtfBytes, options: [.documentType: NSAttributedString.DocumentType.rtf], + documentAttributes: nil) + try check( + "rtf-native-text", + try Data(decoded.string.utf8) == rich.result.data(for: .plainText) + && decoded.length == decoded.string.utf16.count) + let key = (decoded.string as NSString).range(of: "\"é\"") + let number = (decoded.string as NSString).range(of: "9007199254740993") + #if canImport(AppKit) + let font = + decoded.attribute(.font, at: key.location, effectiveRange: nil) as? NSFont + #else + let font = + decoded.attribute(.font, at: key.location, effectiveRange: nil) as? UIFont + #endif + let keyColor = + decoded.attribute(.foregroundColor, at: key.location, effectiveRange: nil) + as? NSObject + let numberColor = + decoded.attribute(.foregroundColor, at: number.location, effectiveRange: nil) + as? NSObject + try check( + "rtf-native-styles", + font?.fontName.contains("Menlo") == true && font?.pointSize == 11 && keyColor != nil + && numberColor != nil && keyColor != numberColor) #endif let richOnly = try rich.result.selecting([.rtf]) - try check("rtf-selection", richOnly.advertisedFormats == [.rtf] && richOnly.availableFormats.count == 5) + try check( + "rtf-selection", + richOnly.advertisedFormats == [.rtf] && richOnly.availableFormats.count == 5) #if canImport(CoreTransferable) && canImport(UniformTypeIdentifiers) - var providerNamesMatch = true - for (format, type) in [(JSONExportFormat.json, UTType.json), (.html, .html), - (.rtf, .rtf), (.plainText, .utf8PlainText), (.markdown, .markdown)] { - let selected = try rich.result.selecting([format]) - let provider = NSItemProvider() - provider.register(selected) - providerNamesMatch = providerNamesMatch && provider.registeredTypeIdentifiers == [type.identifier] - && provider.suggestedName == "Exact-JSON." + format.filenameExtension - } - try check("provider-selected-types-and-filenames", providerNamesMatch) + var providerNamesMatch = true + for (format, type) in [ + (JSONExportFormat.json, UTType.json), (.html, .html), + (.rtf, .rtf), (.plainText, .utf8PlainText), (.markdown, .markdown), + ] { + let selected = try rich.result.selecting([format]) + let provider = NSItemProvider() + provider.register(selected) + providerNamesMatch = + providerNamesMatch && provider.registeredTypeIdentifiers == [type.identifier] + && provider.suggestedName == "Exact-JSON." + format.filenameExtension + } + try check("provider-selected-types-and-filenames", providerNamesMatch) #endif #if canImport(CoreTransferable) && canImport(UniformTypeIdentifiers) - try check("rtf-content-type", richOnly.exportedContentTypes() == [.rtf]) - try check("rtf-transfer-bytes", try await richOnly.exported(as: .rtf) == rtfBytes) - let richPath = try await richOnly.export(to: fileDirectory, contentType: .rtf) - try check("rtf-system-filename", richPath.lastPathComponent == "Exact-JSON.rtf" && Data(contentsOf: richPath) == rtfBytes) + try check("rtf-content-type", richOnly.exportedContentTypes() == [.rtf]) + try check("rtf-transfer-bytes", try await richOnly.exported(as: .rtf) == rtfBytes) + let richPath = try await richOnly.export(to: fileDirectory, contentType: .rtf) + try check( + "rtf-system-filename", + richPath.lastPathComponent == "Exact-JSON.rtf" + && Data(contentsOf: richPath) == rtfBytes) #endif var pdfReports: [PDFObservation] = [] #if canImport(WebKit) && canImport(PDFKit) - #if canImport(AppKit) - _ = NSApplication.shared - #endif - let pdfReport = try await rich.result.preparingPDFWithReport() - try check("pdf-explicit-six", pdfReport.result.availableFormats.count == 6 && !rich.result.availableFormats.contains(.pdf)) - try check("pdf-native-unchanged", try pdfReport.result.data(for: .json) == rich.result.data(for: .json)) - try check("pdf-no-root-readmission", pdfReport.operation[.nodes] == 0) - try check("pdf-input-html", try pdfReport.inputHTMLBytes == UInt64(rich.result.data(for: .html).count)) - try check("pdf-cumulative-output", pdfReport.operation[.outputBytes] == pdfReport.capturedPDFBytes + pdfReport.outputPDFBytes) - let pdfOnly = try pdfReport.result.selecting([.pdf]) - #if canImport(CoreTransferable) && canImport(UniformTypeIdentifiers) - try check("pdf-content-type", pdfOnly.exportedContentTypes() == [.pdf]) - let pdfProvider = NSItemProvider() - pdfProvider.register(pdfOnly) - try check("pdf-provider-type-and-filename", pdfProvider.registeredTypeIdentifiers == [UTType.pdf.identifier] && pdfProvider.suggestedName == "Exact-JSON.pdf") - try check("pdf-transfer-bytes", try await pdfOnly.exported(as: .pdf) == pdfReport.result.data(for: .pdf)) - let pdfPath = try await pdfOnly.export(to: fileDirectory, contentType: .pdf) - try check("pdf-system-filename", try pdfPath.lastPathComponent == "Exact-JSON.pdf" && Data(contentsOf: pdfPath) == pdfReport.result.data(for: .pdf)) - #endif - try check("pdf-original-unavailable", !rich.result.advertisedFormats.contains(.pdf)) - let rows = (0..<150).map { JSON.string("ROW-\($0)-END 9007199254740993 é e\u{301} 😀 العربية https://example.test/wide/long/value") } - let pageItem = try JSONExportDocument.prepare(.array(rows + [.string("LAST-CONTENT-SENTINEL")]), options: .init(formats: [.html])) - for options in [JSONPDFOptions(), .init(paper: .usLetter), .init(layout: .continuous)] { - let rendered = try await pageItem.preparingPDFWithReport(options: options) - let bytes = try rendered.result.data(for: .pdf) - let pdf = try PDFDocument(data: bytes).unwrap() - let name = options.layout == .continuous ? "Continuous.pdf" : options.paper == .a4 ? "A4.pdf" : "Letter.pdf" - try bytes.write(to: output.appendingPathComponent(name)); samples[name] = bytes.count - if options.layout == .paginated { - let width = options.paper == .a4 ? 595.275590551 : 612.0 - let height = options.paper == .a4 ? 841.889763780 : 792.0 - try check("pdf-geometry-" + name, pdf.pageCount >= 3 && (0.. 2_000) + #if canImport(AppKit) + _ = NSApplication.shared + #endif + let pdfReport = try await rich.result.preparingPDFWithReport() + try check( + "pdf-explicit-six", + pdfReport.result.availableFormats.count == 6 + && !rich.result.availableFormats.contains(.pdf)) + try check( + "pdf-native-unchanged", + try pdfReport.result.data(for: .json) == rich.result.data(for: .json)) + try check("pdf-no-root-readmission", pdfReport.operation[.nodes] == 0) + try check( + "pdf-input-html", + try pdfReport.inputHTMLBytes == UInt64(rich.result.data(for: .html).count)) + try check( + "pdf-cumulative-output", + pdfReport.operation[.outputBytes] == pdfReport.capturedPDFBytes + + pdfReport.outputPDFBytes) + let pdfOnly = try pdfReport.result.selecting([.pdf]) + #if canImport(CoreTransferable) && canImport(UniformTypeIdentifiers) + try check("pdf-content-type", pdfOnly.exportedContentTypes() == [.pdf]) + let pdfProvider = NSItemProvider() + pdfProvider.register(pdfOnly) + try check( + "pdf-provider-type-and-filename", + pdfProvider.registeredTypeIdentifiers == [UTType.pdf.identifier] + && pdfProvider.suggestedName == "Exact-JSON.pdf") + try check( + "pdf-transfer-bytes", + try await pdfOnly.exported(as: .pdf) == pdfReport.result.data(for: .pdf)) + let pdfPath = try await pdfOnly.export(to: fileDirectory, contentType: .pdf) + try check( + "pdf-system-filename", + try pdfPath.lastPathComponent == "Exact-JSON.pdf" + && Data(contentsOf: pdfPath) == pdfReport.result.data(for: .pdf)) + #endif + try check("pdf-original-unavailable", !rich.result.advertisedFormats.contains(.pdf)) + let rows = (0..<150).map { + JSON.string( + "ROW-\($0)-END 9007199254740993 é e\u{301} 😀 العربية https://example.test/wide/long/value" + ) } - try check("pdf-last-page-" + name, pdf.page(at: pdf.pageCount - 1)?.string?.contains("LAST-CONTENT-SENTINEL") == true) - let elapsed = rendered.elapsed.components - pdfReports.append(.init(layout: options.layout.rawValue, paper: options.paper.rawValue, - pages: rendered.pageCount, bytes: rendered.outputPDFBytes, capturedBytes: rendered.capturedPDFBytes, - work: rendered.operation[.workUnits], peakLogicalBytes: rendered.operation.peakLogicalRetainedBytes, - elapsedMilliseconds: Double(elapsed.seconds) * 1_000 + Double(elapsed.attoseconds) / 1e15, backend: rendered.backend)) - } - let cancelled = Task { @MainActor in try await rich.result.preparingPDF() } - cancelled.cancel() - var cancellationRejected = false - do { _ = try await cancelled.value } catch is CancellationError { cancellationRejected = true } - try check("pdf-pre-cancellation", cancellationRejected && !rich.result.availableFormats.contains(.pdf)) + let pageItem = try JSONExportDocument.prepare( + .array(rows + [.string("LAST-CONTENT-SENTINEL")]), options: .init(formats: [.html])) + for options in [JSONPDFOptions(), .init(paper: .usLetter), .init(layout: .continuous)] { + let rendered = try await pageItem.preparingPDFWithReport(options: options) + let bytes = try rendered.result.data(for: .pdf) + let pdf = try PDFDocument(data: bytes).unwrap() + let name = + options.layout == .continuous + ? "Continuous.pdf" : options.paper == .a4 ? "A4.pdf" : "Letter.pdf" + try bytes.write(to: output.appendingPathComponent(name)) + samples[name] = bytes.count + if options.layout == .paginated { + let width = options.paper == .a4 ? 595.275590551 : 612.0 + let height = options.paper == .a4 ? 841.889763780 : 792.0 + try check( + "pdf-geometry-" + name, + pdf.pageCount >= 3 + && (0.. 2_000) + } + try check( + "pdf-last-page-" + name, + pdf.page(at: pdf.pageCount - 1)?.string?.filter({ !$0.isWhitespace }).contains( + "LAST-CONTENT-SENTINEL") == true) + let elapsed = rendered.elapsed.components + pdfReports.append( + .init( + layout: options.layout.rawValue, paper: options.paper.rawValue, + pages: rendered.pageCount, bytes: rendered.outputPDFBytes, + capturedBytes: rendered.capturedPDFBytes, + work: rendered.operation[.workUnits], + peakLogicalBytes: rendered.operation.peakLogicalRetainedBytes, + elapsedMilliseconds: Double(elapsed.seconds) * 1_000 + Double( + elapsed.attoseconds) / 1e15, backend: rendered.backend)) + } + let cancelled = Task { @MainActor in try await rich.result.preparingPDF() } + cancelled.cancel() + var cancellationRejected = false + do { _ = try await cancelled.value } catch is CancellationError { + cancellationRejected = true + } + try check( + "pdf-pre-cancellation", + cancellationRejected && !rich.result.availableFormats.contains(.pdf)) #endif var times: [Double] = [] let clock = ContinuousClock() for _ in 0..<7 { let start = clock.now let measured = try JSONExportDocument.prepareWithReport(value) - try check("measurement-complete-\(times.count)", measured.result.availableFormats.count == 4) + try check( + "measurement-complete-\(times.count)", measured.result.availableFormats.count == 4) let elapsed = start.duration(to: clock.now).components times.append(Double(elapsed.seconds) * 1_000 + Double(elapsed.attoseconds) / 1e15) } - let receipt = Receipt(status: "passed", cases: cases, samples: samples, timingsMilliseconds: times, + let receipt = Receipt( + status: "passed", cases: cases, samples: samples, timingsMilliseconds: times, preparedBytes: report.preparedBytes, workUnits: report.operation[.workUnits], peakLogicalRetainedBytes: report.operation.peakLogicalRetainedBytes, rtfPreparedBytes: rich.preparedBytes, rtfWorkUnits: rich.operation[.workUnits], rtfPeakLogicalRetainedBytes: rich.operation.peakLogicalRetainedBytes, - pdfReports: pdfReports, limitations: ["API/file transport consumer, not share-sheet, drag/drop or recipient UI qualification.", - "PDF native generation/text/geometry is independent of receiving-app UI and physical-device integration.", "Logical retention is not RSS; tiny sample timing does not prove throughput ceilings."]) - let encoder = JSONEncoder(); encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + pdfReports: pdfReports, + limitations: [ + "API/file transport consumer, not share-sheet, drag/drop or recipient UI qualification.", + "PDF native generation/text/geometry is independent of receiving-app UI and physical-device integration.", + "Logical retention is not RSS; tiny sample timing does not prove throughput ceilings.", + ]) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] try encoder.encode(receipt).write(to: output.appendingPathComponent("consumer.json")) - print("JSONHTML_EXPORT_CONSUMER passed cases=\(cases.count) artifacts=\(samples.count) measurements=\(times.count)") + print( + "JSONHTML_EXPORT_CONSUMER passed cases=\(cases.count) artifacts=\(samples.count) measurements=\(times.count)" + ) } } -private extension Optional { - func unwrap() throws -> Wrapped { guard let value = self else { throw CocoaError(.fileReadCorruptFile) }; return value } +extension Optional { + fileprivate func unwrap() throws -> Wrapped { + guard let value = self else { throw CocoaError(.fileReadCorruptFile) } + return value + } } From 76e4b262c4247ca3784b62b97bc21321e43d4044 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:06:44 +0200 Subject: [PATCH 47/59] fix: bound deep layout and use exact portable fixture paths --- .../StructuredPreviews/APIAndPolicy.md | 8 +++++++- Sources/JSONHTML/Assets/Interactive.css | 9 +++++++++ .../JSONHTML/JSONHTMLInteractiveAssets.swift | 11 +++++++++- Sources/JSONHTML/JSONPDFBackend.swift | 2 +- .../StructuredPreviewTests.swift | 20 +++++++++---------- Tools/JSONHTMLBrowser/browser.cjs | 5 +++++ 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md b/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md index ff7c77f..297a7b3 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md @@ -91,7 +91,7 @@ Prepared HTML retains immutable layout/value/print and font/media readiness poli Explicit asynchronous Apple PDF consumes that artifact, awaits fonts/image decoding, forces its print representation, and never regenerates JSONHTML. Syntax pagination retains CoreText visible ranges. Structured pagination collects native line/media -bounds one DOM step at a time, chooses integer whitespace cuts and composites native +bounds (including controlled swatches) one DOM step at a time, chooses integer whitespace cuts and composites native regions. Each capture checks and hides off-region indexed rows/cards one DOM node at a time; composition clips to the admitted region. This avoids SDK capture retaining neighboring selectable text. All scans and visibility calls charge the @@ -129,3 +129,9 @@ structured source-download anchor reuses exact native UTF-8 and works without JS its Base64 duplication is metered. `copyJSONBytes` describes the one raw payload, while emitted metadata/output counts each copy. Legacy static syntax retains its original code-only counters. + +The approved responsive transitions are 1200px (outer spacing/title), 920px +(full-width body and compact outline) and 580px (stacked key/value fields and +full-row search). At visual depth bucket 3, nested record borders and padding +stop accumulating. The complete 120-container alternating fixture is checked +at 320px; static source links share the themed accessible link color. diff --git a/Sources/JSONHTML/Assets/Interactive.css b/Sources/JSONHTML/Assets/Interactive.css index 4d3dce5..eaebdb1 100644 --- a/Sources/JSONHTML/Assets/Interactive.css +++ b/Sources/JSONHTML/Assets/Interactive.css @@ -18,3 +18,12 @@ @media print{.rich .source-download{display:none!important}} .rich .scalar-item{display:flex;align-items:flex-start;gap:.55rem;min-width:0;padding:.3rem 0;border-top:1px solid var(--line)}.rich .scalar-item>.index-label{flex:none;min-width:2.5rem;margin:0;padding-top:.2rem}.rich .scalar-item>.node{min-width:0;flex:1} + +/* Approved responsive transitions; all deep record rails stop at bucket 3. */ +.rich a{color:var(--num)} +.rich .depth-3 .record-card{padding:0;border:0;margin:0;border-radius:0} +.rich .depth-3 .indexed-list{gap:.2rem} +@media(max-width:1200px){.rich,.rich.interactive{padding:1.25rem}.rich .document-header h1{font-size:1.4rem}} +@media(max-width:920px){.rich .layout{grid-template-columns:minmax(0,1fr)}.rich .outline{position:static;max-height:12rem}.rich .document-header{display:block}} +@media(min-width:581px) and (max-width:700px){.rich .field-row{grid-template-columns:minmax(7rem,23%) minmax(0,1fr);gap:.6rem}} +@media(max-width:580px){.rich,.rich.interactive{padding:.8rem}.rich .field-row{grid-template-columns:minmax(0,1fr);gap:.3rem}.rich #jh-query{flex:1 1 100%;width:100%}} diff --git a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift index 56e4c67..bc719a4 100644 --- a/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift +++ b/Sources/JSONHTML/JSONHTMLInteractiveAssets.swift @@ -23,6 +23,15 @@ package enum JSONHTMLInteractiveAssets { @media print{.rich .source-download{display:none!important}} .rich .scalar-item{display:flex;align-items:flex-start;gap:.55rem;min-width:0;padding:.3rem 0;border-top:1px solid var(--line)}.rich .scalar-item>.index-label{flex:none;min-width:2.5rem;margin:0;padding-top:.2rem}.rich .scalar-item>.node{min-width:0;flex:1} + +/* Approved responsive transitions; all deep record rails stop at bucket 3. */ +.rich a{color:var(--num)} +.rich .depth-3 .record-card{padding:0;border:0;margin:0;border-radius:0} +.rich .depth-3 .indexed-list{gap:.2rem} +@media(max-width:1200px){.rich,.rich.interactive{padding:1.25rem}.rich .document-header h1{font-size:1.4rem}} +@media(max-width:920px){.rich .layout{grid-template-columns:minmax(0,1fr)}.rich .outline{position:static;max-height:12rem}.rich .document-header{display:block}} +@media(min-width:581px) and (max-width:700px){.rich .field-row{grid-template-columns:minmax(7rem,23%) minmax(0,1fr);gap:.6rem}} +@media(max-width:580px){.rich,.rich.interactive{padding:.8rem}.rich .field-row{grid-template-columns:minmax(0,1fr);gap:.3rem}.rich #jh-query{flex:1 1 100%;width:100%}} """# package static let script = #""" // SPDX-License-Identifier: Apache-2.0 @@ -231,6 +240,6 @@ package enum JSONHTMLInteractiveAssets { say('Interactive controls ready. Search scope: selected displayed previews or exact JSON source.'); })(); """# - package static let styleHash = "i9/4be/W5I4qf52IUKpWqBrCgmLKllGtm6fH0XgpMrI=" + package static let styleHash = "7DSyFzk9+PAtobOu58JQ6klUHFcpTNAffoo1DfByJvA=" package static let scriptHash = "0E/zm4efMpR/ae2G76+pX+c/zABUnOf3Bmlbu8CFcDc=" } diff --git a/Sources/JSONHTML/JSONPDFBackend.swift b/Sources/JSONHTML/JSONPDFBackend.swift index 5ed03dd..020e223 100644 --- a/Sources/JSONHTML/JSONPDFBackend.swift +++ b/Sources/JSONHTML/JSONPDFBackend.swift @@ -129,7 +129,7 @@ guard let page else { throw CancellationError() } do { _ = try await page.callJavaScript( - "const root=document.querySelector('.print-json'); window.__jhPDFScan={walker:document.createTreeWalker(root,NodeFilter.SHOW_TEXT),rects:null,index:0,media:root.querySelectorAll('img,math,.scalar-item,.record-card'),mediaIndex:0,height:height}; return 1;", + "const root=document.querySelector('.print-json'); window.__jhPDFScan={walker:document.createTreeWalker(root,NodeFilter.SHOW_TEXT),rects:null,index:0,media:root.querySelectorAll('img,math,.color-swatch,.scalar-item,.record-card'),mediaIndex:0,height:height}; return 1;", arguments: ["height": height], contentWorld: .defaultClient) } catch is CancellationError { throw CancellationError() } catch { throw JSONPDFFailure.layoutFailed diff --git a/Tests/JSONHTMLTests/StructuredPreviewTests.swift b/Tests/JSONHTMLTests/StructuredPreviewTests.swift index 204197d..ac2baa1 100644 --- a/Tests/JSONHTMLTests/StructuredPreviewTests.swift +++ b/Tests/JSONHTMLTests/StructuredPreviewTests.swift @@ -16,12 +16,14 @@ private struct Base64Gold: Decodable, Sendable { let valid: Bool } private struct GoldFile: Decodable { let cases: [T] } +private func previewFixtureURL(_ name: String, _ extensionName: String = "json") throws -> URL { + // WASI Foundation's Bundle named-resource search truncates some extensions. + // Use the exact pinned path under the generated SwiftPM resource directory. + let root = try #require(Bundle.module.resourceURL) + return URL(fileURLWithPath: root.path + "/Fixtures/Enrichment/" + name + "." + extensionName) +} private func gold(_ name: String, _: T.Type) throws -> T { - let url = try #require( - Bundle.module.url( - forResource: name, withExtension: "json", subdirectory: "Fixtures/Enrichment") - ) - return try JSONDecoder().decode(T.self, from: Data(contentsOf: url)) + return try JSONDecoder().decode(T.self, from: Data(contentsOf: previewFixtureURL(name))) } private let mathGold = try! gold("math-cases", GoldFile.self).cases private let inlineGold = try! gold("inline-cases", GoldFile.self).cases @@ -236,9 +238,7 @@ private func unwrapSingleRows(_ source: String) -> String { } } @Test func structuredAdmitsDeclaredMediaOnceAndRejectsMismatchedRemoteSVG() throws { - let mediaURL = try #require( - Bundle.module.url( - forResource: "media", withExtension: "json", subdirectory: "Fixtures/Enrichment")) + let mediaURL = try previewFixtureURL("media") guard case .object(let media) = try JSONParser().parse(Array(Data(contentsOf: mediaURL))) else { Issue.record("Fixture must be an object") return @@ -502,9 +502,7 @@ func structuredAllPreviewCeilingsRejectBeforeGrowthAndKeepInclusiveEdges(metric: #expect(meter.used(.retainedBytes) == 0) } } else { - let url = try #require( - Bundle.module.url( - forResource: "sample", withExtension: "png", subdirectory: "Fixtures/Enrichment")) + let url = try previewFixtureURL("sample", "png") let data = try Data(contentsOf: url) let encoded = data.base64EncodedString() let boundary: UInt64 = diff --git a/Tools/JSONHTMLBrowser/browser.cjs b/Tools/JSONHTMLBrowser/browser.cjs index 9a8a61d..341a3f3 100644 --- a/Tools/JSONHTMLBrowser/browser.cjs +++ b/Tools/JSONHTMLBrowser/browser.cjs @@ -209,6 +209,11 @@ async function main() { }, {viewport: {width: 390, height: 844}}); await run('complete-print', 'deep', 'http', true, async page => { assert.equal(await page.locator('#jh-tree details[open]').count(), 0); + await page.setViewportSize({width: 320, height: 900}); + await page.locator('[data-action="expand"]').click(); + assert.equal(await page.locator('#jh-tree details:not([open])').count(), 0); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'Expanded deep cards must stop accumulating indentation'); + await page.locator('[data-action="collapse"]').click(); await page.emulateMedia({media: 'print'}); assert.equal(await page.locator('#jh-tree').isVisible(), false); assert.equal(await page.locator('.print-json').isVisible(), true); From 8039cfb6fc61f4775c90e8fff70c4bc3de43d225 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:21:07 +0200 Subject: [PATCH 48/59] test: qualify immutable saves within WASI host capabilities --- Scripts/wasm_support.py | 6 +++--- Tests/JSONHTMLTests/StructuredPreviewTests.swift | 12 ++++++++++-- Tools/WasmAudit/README.md | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index cfab492..d977e91 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -8,7 +8,7 @@ SDK = 'swift-6.4.0-RELEASE_wasm' COUNTS = {'JSONTests': 107, 'JSONSchemaTests': 52, 'JSONRegexRuntimeTests': 12, 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 81} -GROUPS = {'core': 6, 'resources': 1, 'schema': 2, 'streaming': 3} +GROUPS = {'core': 7, 'resources': 1, 'schema': 2, 'streaming': 3} def main(): parser = argparse.ArgumentParser(description=__doc__) @@ -17,7 +17,7 @@ def main(): output = ROOT / args.output; output.mkdir(parents=True, exist_ok=True) result = {'schema_version': 1, 'status': 'running', 'target': 'wasm32-unknown-wasip1', 'sdk': SDK, 'commands': [], 'modules': [], 'root_counts': COUNTS, - 'wasmkit_scope': '13 public consumer functions, 49 caps, 20 rooted vectors, debug/release command parity', + 'wasmkit_scope': '14 public consumer functions, 49 caps, 20 rooted vectors, debug/release command parity', 'node_scope': 'complete core/HTML debug/release root suites, caps, public consumers and independent qualification; CLI runtime disposition is separate'} def save(): (output / 'wasi.json').write_text(json.dumps(result, indent=2) + '\n') @@ -88,7 +88,7 @@ def invoke(engine, path, mode='command', arguments=(), repo=True, resources=True modules[config, name] = binary(scratch, config, name) runner = binary(scratch, config, {'WasmAudit': 'WasmAuditTests', 'Qualification': 'LimitQualificationTests', 'MCPReadiness': 'MCPReadinessTests'}[name] + '-test-runner') for engine in ['node', 'wasmkit']: - text = run(engine + '-' + config + '-' + name + '-tests', invoke(engine, runner, 'tests'), tests({'WasmAudit':13, 'Qualification':1, 'MCPReadiness':3}[name])) + text = run(engine + '-' + config + '-' + name + '-tests', invoke(engine, runner, 'tests'), tests({'WasmAudit':14, 'Qualification':1, 'MCPReadiness':3}[name])) if name == 'Qualification': matrix = json.loads((ROOT / 'Documentation/Qualification/LimitsMatrix.json').read_text()) expected = {m['metric']: m['public_probe_outcome'] for m in matrix['metrics']} diff --git a/Tests/JSONHTMLTests/StructuredPreviewTests.swift b/Tests/JSONHTMLTests/StructuredPreviewTests.swift index ac2baa1..545b02e 100644 --- a/Tests/JSONHTMLTests/StructuredPreviewTests.swift +++ b/Tests/JSONHTMLTests/StructuredPreviewTests.swift @@ -399,8 +399,16 @@ private func unwrapSingleRows(_ source: String) -> String { @Test func structuredHTMLSavingUsesImmutableBytesAndNoMutatedDOMSerializer() throws { let renderer = JSONHTMLRenderer() let bytes = try renderer.render(.string("**bold** $x^2$")) - let file = FileManager.default.temporaryDirectory.appendingPathComponent( - UUID().uuidString + ".html") + #if os(WASI) + // The qualification host grants only resources and the repository, not /tmp. + let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent() + .deletingLastPathComponent().deletingLastPathComponent() + let file = root.appendingPathComponent( + ".build/structured-save-" + UUID().uuidString + ".html") + #else + let file = FileManager.default.temporaryDirectory.appendingPathComponent( + UUID().uuidString + ".html") + #endif defer { try? FileManager.default.removeItem(at: file) } #if os(WASI) try bytes.write(to: file) // WASI has no atomic temporary-file writer; the same immutable bytes are verified. diff --git a/Tools/WasmAudit/README.md b/Tools/WasmAudit/README.md index 2bf46f0..0709c10 100644 --- a/Tools/WasmAudit/README.md +++ b/Tools/WasmAudit/README.md @@ -27,7 +27,7 @@ wasmkit run --dir /absolute/path /absolute/path/WasmAudit.wasm schema ``` `missing-resources` intentionally runs without a preopen and requires a structured -meta-schema failure, with no trap. The 13 Swift Testing functions have the same +meta-schema failure, with no trap. The original 13 Swift Testing functions have the same public semantics on native and WASI. The broader root/corpus/49-cap/frozen consumer qualification runs through `python3 Scripts/ci.py --lane wasm`; source paths and private build outputs stay outside archives. `--repo` grants fixture access only @@ -38,6 +38,6 @@ cooperative cancellation, safety/measurement policy and unqualified host modes. The integrated JSONHTML consumer adds the fifth public product and a fourteenth Swift Testing function covering six exact HTML/export/budget/cancellation/backend -checks. Node runs all six root test bundles (including 49 portable HTML functions); +checks. Node runs all six root test bundles (including 81 portable HTML functions); Node and WasmKit run the 14-function public consumer. Apple transfer and PDF are not portable backend claims. See [receiving qualification](../../Documentation/Architecture/Release1.3/ReceivingQualification.md). From 7fe0226bae67747375c5a99a9a887814bd85d1de Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:40:58 +0200 Subject: [PATCH 49/59] test: run the separate six-case WASI HTML consumer mode --- Scripts/wasm_support.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index d977e91..1af373b 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -8,7 +8,7 @@ SDK = 'swift-6.4.0-RELEASE_wasm' COUNTS = {'JSONTests': 107, 'JSONSchemaTests': 52, 'JSONRegexRuntimeTests': 12, 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 81} -GROUPS = {'core': 7, 'resources': 1, 'schema': 2, 'streaming': 3} +GROUPS = {'core': 6, 'html': 6, 'resources': 1, 'schema': 2, 'streaming': 3} def main(): parser = argparse.ArgumentParser(description=__doc__) From d89be60b66e753bb5f4be1b414710ab8c6155376 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:55:04 +0200 Subject: [PATCH 50/59] docs: record structured preview qualification and remaining gates --- CHANGELOG.md | 5 ++ .../Architecture/Release1.3/Progress.json | 37 ++++++++++++ .../StructuredPreviews/PreviewPolicy.json | 12 +++- .../StructuredPreviews/Qualification.md | 58 +++++++++++++++++++ 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 605fb49..4d8a949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- Add structured JSONHTML layouts with finite string, color, declared raster, inline + and atomic MathML previews; preserve exact source, complete export/print and shared + CLI policy. Native, browser and WASI evidence is recorded with remaining assistive + and host qualification boundaries. + - Add typed caller diagnostics and coherent JSONDocument byte-span rendering to JSONHTML, with exact target identity, explicitly missing-parent attachment and finite cumulative annotation/path/message accounting. Rendering never asserts diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index 7c0fed7..ec22553 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -306,5 +306,42 @@ "Representative calibration only; no maximum-workload/RSS/opaque SDK instruction guarantees", "Formatted audible speech not part of the new interactive annotation check" ] + }, + "structured_preview_increment": { + "briefing_revision": "0.2", + "release_assignment": null, + "branch": "codex/jsonhtml-structured-previews", + "status": "Implementation reviewable; complete L5/P5 qualification remains open", + "native_library_head": "76e4b262c4247ca3784b62b97bc21321e43d4044", + "source_preserving_cli_base": "aded3c9930f580bd4fb902bba002ab75ed199a7b", + "native_functions": 372, + "native_parameter_cases": 325, + "core_metric_count": 49, + "browser_cases": 76, + "supplemental_browser_contexts": 48, + "ipad_simulator_cases": 73, + "preview_policy_ceilings": 22, + "requirements": 110, + "scenarios": 88, + "calibration_configurations": 96, + "remaining": [ + "Actual Linux runtime", + "Locked Mac native Safari/VoiceOver", + "Locked physical iPhone execution", + "Native touch and exact unexecuted scenario observations" + ], + "qualification_contract": "StructuredPreviews/Qualification.md", + "history_preserved": true, + "publication_performed": false, + "wasi": { + "source_head": "7fe0226", + "status": "passed", + "probes": 71, + "functions_per_configuration": 277, + "parameters_per_configuration": 288, + "public_consumer_functions": 14, + "cap_metrics": 49, + "mcp_server_claim": false + } } } diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/PreviewPolicy.json b/Documentation/Architecture/Release1.3/StructuredPreviews/PreviewPolicy.json index 9e92e2d..5d1baf9 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/PreviewPolicy.json +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/PreviewPolicy.json @@ -1,6 +1,6 @@ { "version": 1, - "status": "proposed_until_P0_P5_qualification", + "status": "frozen_native_measured_candidate; complete_P5_qualification_pending", "core_limits_unchanged": true, "defaults": { "rendererStructuredProfile": "inspection", @@ -81,5 +81,13 @@ "hexOrder": "CSS_RGB_RGBA", "namedColors": false }, - "accounting": "Eligibility policies supplement the one cumulative core/HTML meter; real global exhaustion is terminal. All finite values require checked representability and calibration." + "accounting": "Eligibility policies supplement the one cumulative core/HTML meter; real global exhaustion is terminal. All finite values require checked representability and calibration.", + "native_calibration": { + "configurations": 96, + "repeated_renders": 672, + "behavior_checks": 674, + "finite_boundary_parameters": 22, + "core_metric_meanings_unchanged": 49, + "rss_guarantee": false + } } diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md b/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md new file mode 100644 index 0000000..6f6a720 --- /dev/null +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md @@ -0,0 +1,58 @@ +# Structured previews: qualification boundary + +This is the consolidated revision 0.2 increment, not a package release. The dependent +branch preserves the completed W/G/RC and 1.3/CLI histories. The executable and five +libraries remain the only root products; external dependencies remain empty. + +The implemented L0–L4 / P0–P4 behavior includes the independent structured/syntax and +interactive/static choices, exact native source, finite prefix/color/media/inline/math +policies, prepared complete exports/print, guarded PDF and shared HTML-only CLI flags. +See [API/policy](APIAndPolicy.md) and [finite policy](PreviewPolicy.json). + +Native Swift 6.4, language 6, macOS 27.0, Xcode 27.2 and SDK 27.2 qualification at library +commit `76e4b262c4247ca3784b62b97bc21321e43d4044` passed 25 native commands, 372 debug/release +behavior functions and 325 parameter cases. All 49 cap probes and pinned independent +suites retain exact cardinality: JSON 318 observed/287 evaluated/31 policy and schema +2337/2300/37. Exclusions are not evaluated successes. ASAN 372/325, TSan 11 and strict +memory safety passed. Five DocC catalogues, declaration comments and compiled README +examples passed. + +The fixed assets passed 76 browser cases in three pinned engines. Separate stricter +host/no-script observations cover 48 contexts/576 layout checks, 25 approved/deep +viewport cases, 16 measured text contrasts and forced colors. Complete and explicit +inspection printing and corrupt-raster no-script source access have separate results. +Playwright WebKit is supplementary and cannot certify actual Safari or VoiceOver. + +Native export/PDF passed 65 public consumer cases/eight artifacts, 150 exact-once row +markers in A4/Letter plus tail, media/math/full strings and failed-decoder semantics. +The universal public Apple consumer built on both mobile SDKs; iPadOS 27.0 simulator +execution passed 73 cases/eight artifacts. Physical iPhone launch was denied because +it was locked. No receiving-editor, share-sheet or touch success is inferred. + +Native calibration uses eight bounded datasets, three finite profiles, 96 option +combinations, 672 repeated renders and 674 behavior checks. Browser observations use 18 +engine/dataset pairs, 90 loads and 75 search/disclosure samples each. Shared-host elapsed +time is not an idle-host comparison or throughput guarantee. Logical retained bytes, +process RSS, caller work and owned quotas remain distinct. The inherited 1.075s versus +0.96849s stream median (+11.0%) retains its existing reviewed disposition and 10% trigger. + +L5/P5 remains an explicit review boundary while Linux runtime, native Safari, +VoiceOver, native touch/physical execution and any exact unexecuted scenarios are open. +The supplied return materializes 110 requirement rows, 88 scenario rows, 48 requested +browser contexts, actual source archive, per-file manifests, command receipts and +failed-probe history. It does not turn related test counts into unexecuted scenario +passes. The matching Swift 6.4 WASI lane at `7fe0226` passed all 71 probes: debug/release +277 root functions and 288 parameter cases, 14 public consumer functions, all 49 +cap probes, 20 rooted vectors, pinned independent suites, strict memory safety and +missing-resource/runtime observations. Its original receipts remain source-bound. + +A source-preserving CLI ancestry reconciliation and WASI-only test/harness changes +leave all 194 library/manifest files byte-identical to the qualified native source. +The added public annotation consumer executed separately. Later documentation-only +changes reuse unchanged-source evidence with explicit equivalence, preserving original +receipt commits. The source archive's actual digest is recorded outside its own contents +to avoid a circular claim. + +No merge, tag, release, Site/social edit or outreach is authorized by this increment. +The focused draft PR is dependent on the current CLI integration stack. Maintainers +must review the materialized remaining actions before claiming complete qualification. From e0d5f0c26519c3eaa5faaa62f4104cc10ad5c419 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:55:20 +0200 Subject: [PATCH 51/59] docs: record native and portable structured-preview qualification follow-up --- .../Architecture/Release1.3/Progress.json | 27 ++++++++++-- .../StructuredPreviews/Qualification.md | 44 +++++++++++++++++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index ec22553..fcc044c 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -325,10 +325,8 @@ "scenarios": 88, "calibration_configurations": 96, "remaining": [ - "Actual Linux runtime", - "Locked Mac native Safari/VoiceOver", - "Locked physical iPhone execution", - "Native touch and exact unexecuted scenario observations" + "Native physical touch/portrait-landscape browser interaction", + "Native media touch interaction" ], "qualification_contract": "StructuredPreviews/Qualification.md", "history_preserved": true, @@ -342,6 +340,27 @@ "public_consumer_functions": 14, "cap_metrics": 49, "mcp_server_claim": false + }, + "qualification_follow_up": { + "source_head": "d89be60b66e753bb5f4be1b414710ab8c6155376", + "hosted_native_run": 38657093294, + "hosted_linux_run": 38657093290, + "hosted_checks_passed": 7, + "linux_architectures": [ + "x86_64", + "arm64" + ], + "linux_functions_per_configuration": 356, + "linux_parameters_per_configuration": 325, + "physical_iphone_cases": 73, + "physical_iphone_artifacts": 8, + "native_safari_contexts": 16, + "native_safari_scope": "Accessibility/control smoke; selected five responsive widths, console/network; separate automated layout/zoom scope retained", + "voiceover": "Maintainer manually confirmed field/type/index, disclosure state and inline math; original off preference restored", + "additional_public_swift_testing_functions": 6, + "additional_search_cases": 9, + "history_preserved": true, + "library_changes": false } } } diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md b/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md index 6f6a720..1fdf61c 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md @@ -26,8 +26,9 @@ Playwright WebKit is supplementary and cannot certify actual Safari or VoiceOver Native export/PDF passed 65 public consumer cases/eight artifacts, 150 exact-once row markers in A4/Letter plus tail, media/math/full strings and failed-decoder semantics. The universal public Apple consumer built on both mobile SDKs; iPadOS 27.0 simulator -execution passed 73 cases/eight artifacts. Physical iPhone launch was denied because -it was locked. No receiving-editor, share-sheet or touch success is inferred. +execution passed 73 cases/eight artifacts. The subsequent unlocked physical iPhone +run on iOS 27.2 also passed 73 cases/eight artifacts; its actual files were copied +back. Headless execution does not establish physical touch or share-sheet behavior. Native calibration uses eight bounded datasets, three finite profiles, 96 option combinations, 672 repeated renders and 674 behavior checks. Browser observations use 18 @@ -36,8 +37,8 @@ time is not an idle-host comparison or throughput guarantee. Logical retained by process RSS, caller work and owned quotas remain distinct. The inherited 1.075s versus 0.96849s stream median (+11.0%) retains its existing reviewed disposition and 10% trigger. -L5/P5 remains an explicit review boundary while Linux runtime, native Safari, -VoiceOver, native touch/physical execution and any exact unexecuted scenarios are open. +L5/P5 remains an explicit review boundary while native touch/portrait-landscape and +media touch observations remain open. The supplied return materializes 110 requirement rows, 88 scenario rows, 48 requested browser contexts, actual source archive, per-file manifests, command receipts and failed-probe history. It does not turn related test counts into unexecuted scenario @@ -56,3 +57,38 @@ to avoid a circular claim. No merge, tag, release, Site/social edit or outreach is authorized by this increment. The focused draft PR is dependent on the current CLI integration stack. Maintainers must review the materialized remaining actions before claiming complete qualification. + +## Qualification follow-up + +PR #30 at `d89be60b66e753bb5f4be1b414710ab8c6155376` passed all seven hosted native, +safety, DocC and Linux checks. Native run `38657093294` tested GitHub's merge commit +`adc9d424fd56253ba8d7885ad73418f33d1b5ccd`. Linux run `38657093290` executed Swift 6.4 +on Ubuntu 24.04/glibc 2.39 on both x86_64 and arm64: debug/release 356 functions and +325 parameter cases per architecture, ASAN 356/325, TSan 11 and strict memory safety. +All seven downloaded lane receipts preserve their original identities; their 991 +input file hashes match the reviewed source. Linux's source-archive invocation has +no Git directory, so its manifest, run and architecture identify the input. + +Actual Safari 27.0 (`22625.1.29.11.24`) on macOS 27.0 exercised 16 file/served +presentation contexts, including JavaScript disabled and stricter CSP. Native +accessibility trees confirmed controls and script-free fallback; selected 320, 390, +768, 1200 and 1440 point viewports were inspected. Console and local network +observations supplement the pinned automated engines. This is a native smoke and +selected-viewport scope; the three automated engines retain their separately +recorded layout/zoom/motion counts. WebKit is never relabelled as Safari. + +The maintainer manually checked VoiceOver on that artifact and reported that field, +type/index labels, disclosure state and the inline math expression worked as expected. +This is attributed human evidence, not an automated spoken-output assertion. The +JavaScript and VoiceOver preferences were restored to their observed original values. + +Six additional public-API Swift Testing probes passed against unchanged library +bytes. They replay the resolved default depth 128 and rejected 129, supplied workspace +annotations and project source mappings, exact repeated-search fixtures and both +pinned non-HTML numeric/scalar fixtures. The 500-field long-string card explicitly +exceeds the unchanged 14,400 point PDF ceiling; a finite 60-field adaptation produces +ten pages with every tail marker exactly once. Nine additional browser cases cover +100/1000/1100 repeated records, the 100-result/highlight bound, query byte overflow +and cancellation of stale searches. TextEdit reception of the pinned exports is +recorded separately from byte/content-type/selection assertions. No library feature, +quota, platform, dependency or product policy changed in this follow-up. From c3760730dc333a84810bba53cde4da339ada90af Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:15:34 +0200 Subject: [PATCH 52/59] feat(schema): qualify offline Responses provider helpers and CLI C5 adapters --- Documentation/Architecture/API.md | 10 + .../Architecture/ExtractionManifest.json | 11 + .../Architecture/Release1.4/Acceptance.json | 28 +- .../Architecture/Release1.4/CLIReference.md | 14 + .../Architecture/Release1.4/Integration.md | 14 + .../Architecture/Release1.4/Progress.json | 56 ++-- .../Release1.4/ProviderProfiles.json | 143 +++++++++ .../Architecture/Release1.4/Providers.md | 86 ++++++ .../Architecture/Release1.4/Reconciliation.md | 6 + Documentation/README.md | 2 + README.md | 10 + Scripts/ci.py | 22 +- Scripts/qualify_cli_cases.py | 24 ++ Scripts/qualify_cli_integration.py | 7 +- Scripts/qualify_schema_providers.py | 86 ++++++ Scripts/wasm_support.py | 2 +- Sources/JSONCLIEngine/CLIDiagnostics.swift | 9 + Sources/JSONCLIEngine/CLIHelp.swift | 8 +- Sources/JSONCLIEngine/CLISchemaCommand.swift | 61 +++- .../Documentation.docc/JSONSchema.md | 11 + .../Documentation.docc/ProviderSchemas.md | 57 ++++ .../JSONSchemaProviderInspection.swift | 283 ++++++++++++++++++ .../Providers/JSONSchemaProviderProfile.swift | 148 +++++++++ .../Providers/OpenAIJSONSchemaBridge.swift | 227 ++++++++++++++ Tests/Fixtures/Providers/boolean-schema.json | 1 + .../Providers/exact-number-schema.json | 1 + .../Providers/optional-field-schema.json | 10 + .../Providers/portable-summary-schema.json | 28 ++ Tests/Fixtures/Providers/registry.json | 32 ++ .../Fixtures/Providers/scalar-key-schema.json | 16 + Tests/JSONCLITests/IntegrationTests.swift | 6 +- Tests/JSONCLITests/ProviderCommandTests.swift | 131 ++++++++ .../JSONSchemaTests/ProviderBridgeTests.swift | 221 ++++++++++++++ Tools/CLIQualification/Package.swift | 4 +- .../Sources/ProviderConsumer/Main.swift | 53 ++++ 35 files changed, 1780 insertions(+), 48 deletions(-) create mode 100644 Documentation/Architecture/Release1.4/ProviderProfiles.json create mode 100644 Documentation/Architecture/Release1.4/Providers.md create mode 100644 Scripts/qualify_schema_providers.py create mode 100644 Sources/JSONSchema/Documentation.docc/ProviderSchemas.md create mode 100644 Sources/JSONSchema/Providers/JSONSchemaProviderInspection.swift create mode 100644 Sources/JSONSchema/Providers/JSONSchemaProviderProfile.swift create mode 100644 Sources/JSONSchema/Providers/OpenAIJSONSchemaBridge.swift create mode 100644 Tests/Fixtures/Providers/boolean-schema.json create mode 100644 Tests/Fixtures/Providers/exact-number-schema.json create mode 100644 Tests/Fixtures/Providers/optional-field-schema.json create mode 100644 Tests/Fixtures/Providers/portable-summary-schema.json create mode 100644 Tests/Fixtures/Providers/registry.json create mode 100644 Tests/Fixtures/Providers/scalar-key-schema.json create mode 100644 Tests/JSONCLITests/ProviderCommandTests.swift create mode 100644 Tests/JSONSchemaTests/ProviderBridgeTests.swift create mode 100644 Tools/CLIQualification/Sources/ProviderConsumer/Main.swift diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index 38c8427..1038748 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -202,3 +202,13 @@ JSONHTMLRenderer also accepts JSONDocument in its four rendering/report methods. Caller annotations use exact typed paths, explicit missing-parent attachment and coherent value byte spans without schema execution or source-byte archival. See [diagnostic rendering](Release1.3/DiagnosticAnnotations.md). + +## Provider-specific schema fragments (1.4 development) + +`OpenAIJSONSchemaBridge(profile:limits:)` in JSONSchema provides `check`/`checkWithReport`, +`responseFormat`/`functionTool`, and their complete Data/String and WithReport forms. +Profiles are `.openAIResponsesFormatV1` and `.openAIResponsesFunctionV1`; native JSON schemas, +offline registries and exact typed issue paths stay authoritative. A compatible local check +is neither native validation of an instance nor a service-acceptance guarantee. Finite limits +and all phase accounting are specified in [Providers](Release1.4/Providers.md) and the frozen +[profile table](Release1.4/ProviderProfiles.json). No new product, SDK client or Apple type is added. diff --git a/Documentation/Architecture/ExtractionManifest.json b/Documentation/Architecture/ExtractionManifest.json index c8a38e9..0668a65 100644 --- a/Documentation/Architecture/ExtractionManifest.json +++ b/Documentation/Architecture/ExtractionManifest.json @@ -188,6 +188,17 @@ "attribution": "BSD-3-Clause alternative; Licenses/json-schema-spec-BSD-AFL.txt", "validation": "Tests/JSONSchemaTests/EvaluationOutputTests.swift", "evidence": "Scripts/ci.py: current native, safety and documentation receipts" + }, + { + "repository": "Swift-JSON original implementation", + "scope": "JSONSchema portable provider profiles/checks, exact Responses fragments and CLI C5 adapters", + "provenance": "Original Apache-2.0 code; frozen user-provided provider fixtures copied unchanged from JSONSchema-Provider-Bridges-Specification-v0.1; profile source URLs/dates and individual fixture hashes in Release1.4/ProviderProfiles.json. No upstream algorithm, SDK or network client adapted.", + "paths": [ + "Sources/JSONSchema/Providers", + "Tests/Fixtures/Providers", + "Scripts/qualify_schema_providers.py", + "Tools/CLIQualification/Sources/ProviderConsumer" + ] } ], "original_implementation_notes": [ diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json index 9539e52..5de2cc9 100644 --- a/Documentation/Architecture/Release1.4/Acceptance.json +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -1,7 +1,7 @@ { "schema": "swift-json-cli-acceptance/1", "prepared": "2026-10-14", - "implementation_status": "Native resource-fix successor qualified; final immutable return records close artifact/head rows. Mandatory providers remain blocked.", + "implementation_status": "C5 portable schema-provider increment implemented and native behavior-qualified; final current-head native/safety/docs/Linux receipts are tracked separately.", "requirements": [ { "id": "CL001", @@ -1188,15 +1188,20 @@ "title": "Offline schema/provider commands", "verification": "Native schema check plus qualified local OpenAI helpers; schema object not quoted string, no network/key.", "obligation": "mandatory", - "status": "blocked", + "status": "passed", "evidence_required": true, "fixture_ids": [ "schema-provider-object-envelope" ], - "blocker": "Qualified schema-provider helpers absent; actual authored expectations 0/1 receive capabilityUnavailable exit2", "evidence": [ - "Documentation/Architecture/Release1.4/Integration.md" - ] + "Documentation/Architecture/Release1.4/Providers.md", + "Documentation/Architecture/Release1.4/ProviderProfiles.json", + "Tests/JSONSchemaTests/ProviderBridgeTests.swift", + "Tests/JSONCLITests/ProviderCommandTests.swift", + "Tools/CLIQualification/Sources/ProviderConsumer/Main.swift", + "Scripts/qualify_schema_providers.py" + ], + "qualification": "Native 13 schema +8 new CLI functions, public14, authoredC5 six, provider process37 (32 focused +5 unchanged fixtures). Final-source broad/native/Linux receipts required before a release recommendation." }, { "id": "CL079", @@ -1204,15 +1209,20 @@ "title": "Provider incompatibility and availability", "verification": "No constraint loss/lowering; compatibility false exit1; Apple signatures guarded and advertised only when qualified.", "obligation": "mandatory", - "status": "blocked", + "status": "passed", "evidence_required": true, "fixture_ids": [ "schema-provider-optionality-reject" ], - "blocker": "Qualified schema-provider helpers absent; actual authored expectations 0/1 receive capabilityUnavailable exit2", "evidence": [ - "Documentation/Architecture/Release1.4/Integration.md" - ] + "Documentation/Architecture/Release1.4/Providers.md", + "Documentation/Architecture/Release1.4/ProviderProfiles.json", + "Tests/JSONSchemaTests/ProviderBridgeTests.swift", + "Tests/JSONCLITests/ProviderCommandTests.swift", + "Tools/CLIQualification/Sources/ProviderConsumer/Main.swift", + "Scripts/qualify_schema_providers.py" + ], + "qualification": "Native 13 schema +8 new CLI functions, public14, authoredC5 six, provider process37 (32 focused +5 unchanged fixtures). Final-source broad/native/Linux receipts required before a release recommendation." }, { "id": "CL080", diff --git a/Documentation/Architecture/Release1.4/CLIReference.md b/Documentation/Architecture/Release1.4/CLIReference.md index 35a6027..fbffe41 100644 --- a/Documentation/Architecture/Release1.4/CLIReference.md +++ b/Documentation/Architecture/Release1.4/CLIReference.md @@ -120,3 +120,17 @@ remain blocked. Portable HTML needs neither Apple PDF preparation nor transfer providers. WASI CLI feasibility is distinct from compiled libraries/private kernel; Node poll_oneoff currently fails with IO exit7, while five WasmKit commands pass; POSIX SIGINT bridging remains unavailable. Actual receipts and remaining mandatory gates determine release readiness. + + +## Qualified portable provider increment + +`schema provider openai-format SCHEMA --name NAME` emits the exact native object for +Responses text.format. `openai-tool` emits one flat Responses function tool, with +strict true and object parameters. Both accept bounded `--description` and explicit +`--resource URI=PATH`; external-reference export remains incompatible without a +bundling policy. No network request, credential, model session or schema lowering +occurs. Incompatibility/unverified declarations exit1 with bounded typed issues, +while malformed schema/missing refs5, finite resource6, IO7 and cancellation130 remain +errors. `apple-check` stays unavailable and is omitted from help. See +[the actual helper/profile/consumer contract](Providers.md). Earlier unavailable +provider observations in this file are preserved history, superseded by this increment. diff --git a/Documentation/Architecture/Release1.4/Integration.md b/Documentation/Architecture/Release1.4/Integration.md index 13775e3..46762b1 100644 --- a/Documentation/Architecture/Release1.4/Integration.md +++ b/Documentation/Architecture/Release1.4/Integration.md @@ -135,3 +135,17 @@ peak logical retention is about66.5KB and measured RSS about7.5MB on a loaded ho There is no quiet-host throughput or hard RSS claim. Final-head hosted checks, source archive, clean extraction and acceptance closure are bound by the returned freeze/evidence records. Mandatory provider CL078/CL079 remain blocked. + + +## Portable provider successor + +The aded3c9 reconciliation is retained with35 passed hosted checks and its immutable +source/evidence return. A distinct provider successor implements CL078/CL079 through +public JSONSchema helpers; the historical unavailable results above are superseded +by the [provider contract](Providers.md), [frozen profile](ProviderProfiles.json) and +its new source-bound receipts. Native C5 vectors now have6 passes/0 blocked; the +full authored corpus has111 successes. Unsupported/unverified declaration reports +exit1; compilation, IO, resource and cancellation failures remain operational. +Apple model/schema bridges remain a separate slice. No prior WASI/performance result +is silently reused as qualification of new helper behavior. Current broad native, +safety, docs, Linux and reproduction receipts accompany the successor return. diff --git a/Documentation/Architecture/Release1.4/Progress.json b/Documentation/Architecture/Release1.4/Progress.json index eb17250..c0225b6 100644 --- a/Documentation/Architecture/Release1.4/Progress.json +++ b/Documentation/Architecture/Release1.4/Progress.json @@ -4,14 +4,14 @@ "renderer_base": "3c1e8a1a1bc8d03699665b8ab98f5ece7962aff7", "main_at_orientation": "25fb04f747e583209b4c4511abf39e11ef5b4f80", "branch": "codex/1.4-json-cli-integration", - "status": "Reconciled onto main9517233; local native/safety/docs passed; final hosted receipts tracked separately", + "status": "Portable C5 providers implemented and native behavior-qualified; current-head broad native/safety/docs/Linux qualification and reproduction are recorded in the new return", "gates": { "C0": "partial", "C1": "passed", "C2": "passed", "C3": "passed", "C4": "passed", - "C5": "blocked" + "C5": "portable_behavior_qualified_final_head_pending" }, "foundation_functions": 12, "foundation_parameter_cases": 4, @@ -21,19 +21,14 @@ "core_metrics": 49, "cli_metrics": 16, "publication_performed": false, - "root_test_functions": 348, + "root_test_functions": 369, "root_parameter_cases": 199, - "native_source_manifest_sha256": "792ade5810b9d8d2a43c3cd66fdebbe2895640fd1cea52ff19fef05cc36b0475", - "qualified_lanes": [ - "native", - "safety", - "docs" - ], + "native_source_manifest_sha256": null, + "qualified_lanes": [], "remaining_foundation_requirements": [ - "final restacked-head native/Linux hosted checks", - "main-aligned archive/reproduction binding", - "qualified schema-provider prerequisite", - "full WASI CLI host/signal qualification (limited disposition retained)" + "current provider-head native/safety/docs/Linux receipts and clean reproduction", + "full WASI CLI host/signal qualification remains a limited disposition", + "Apple GenerationSchema/DynamicGenerationSchema bridge is separate, unimplemented scope" ], "history": [ { @@ -104,6 +99,28 @@ "root_test_functions": 298, "root_parameter_cases": 199, "archive_sha256": "d7789117654d298bc9fe5157d71fe525761b7122ba4b5c59d311220d8c5e2be5" + }, + { + "branch": "codex/1.4-json-cli-integration", + "status": "Reconciled onto main9517233; local native/safety/docs passed; final hosted receipts tracked separately", + "gates": { + "C0": "partial", + "C1": "passed", + "C2": "passed", + "C3": "passed", + "C4": "passed", + "C5": "portable_behavior_qualified_final_head_pending" + }, + "root_test_functions": 348, + "root_parameter_cases": 199, + "native_source_manifest_sha256": "792ade5810b9d8d2a43c3cd66fdebbe2895640fd1cea52ff19fef05cc36b0475", + "new_cases_observed": 111, + "new_cases_passed": 109, + "new_cases_blocked": 2, + "head": "aded3c9930f580bd4fb902bba002ab75ed199a7b", + "hosted_checks": 35, + "archive_sha256": "5c62427fd0f5a8f94884117fbc756e8c54e0ab7e45885d5dc7d0b53ba2b51639", + "return_sha256": "48a3fae0e1144f420d62fa552676a467cabc879605c3bdfc84d9f3e9e927ca15" } ], "query_functions": 24, @@ -126,11 +143,16 @@ "integration_functions": 6, "integration_subprocess_cases": 37, "new_cases_observed": 111, - "new_cases_passed": 109, - "new_cases_blocked": 2, + "new_cases_passed": 111, + "new_cases_blocked": 0, "release_ready": false, "freeze_closure": "Final source/archive and hosted receipts are recorded in the materialized return pack without rewriting this frozen source.", "main_reconciled": "9517233ef0a3c1b961b738df58fe3ee4081d780c", - "linux_root_test_functions": 334, - "main_conflicts_resolved": true + "linux_root_test_functions": 355, + "main_conflicts_resolved": true, + "provider_functions": 13, + "provider_cli_functions": 8, + "provider_public_consumer_cases": 14, + "provider_subprocess_cases": 37, + "provider_profile_revision": "2026-10-15.v1" } diff --git a/Documentation/Architecture/Release1.4/ProviderProfiles.json b/Documentation/Architecture/Release1.4/ProviderProfiles.json new file mode 100644 index 0000000..fc188f6 --- /dev/null +++ b/Documentation/Architecture/Release1.4/ProviderProfiles.json @@ -0,0 +1,143 @@ +{ + "schema_version": 1, + "revision": "2026-10-15.v1", + "profiles": [ + "openai.responses.text-format.v1", + "openai.responses.function.v1" + ], + "provider": "openai", + "capability_category": "documented-base-model-subset", + "scope": "Local unchanged-declaration export; no remote acceptance guarantee; fine-tuned and unspecified model categories are not qualified", + "sources": [ + { + "url": "https://developers.openai.com/api/docs/guides/structured-outputs#supported-schemas", + "observed_date": "2026-10-15" + }, + { + "url": "https://developers.openai.com/api/docs/guides/function-calling#defining-functions", + "observed_date": "2026-10-15" + }, + { + "url": "https://developers.openai.com/api/reference/resources/responses/methods/create", + "observed_date": "2026-10-15" + } + ], + "supported_types": [ + "object", + "array", + "string", + "number", + "integer", + "boolean", + "null" + ], + "supported_keywords": [ + "type", + "properties", + "required", + "additionalProperties", + "items", + "anyOf", + "$ref", + "$defs", + "enum", + "description", + "title", + "pattern", + "format", + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum", + "multipleOf", + "minItems", + "maxItems" + ], + "formats": [ + "date-time", + "time", + "date", + "duration", + "email", + "hostname", + "ipv4", + "ipv6", + "uuid" + ], + "rules": { + "root": "Explicit object type; no root anyOf/$ref or inferred shape", + "objects": "Explicit properties object (including empty); each declaration explicitly additionalProperties:false; required exactly covers declared properties", + "nullable": "One declared type plus null; no absent-to-null rewriting", + "references": "Native offline compilation; only local fragment references exported, productive recursion retained without expansion; unresolved references are operational errors", + "unknown": "Unverified declarations block strict export, including const, minLength/maxLength, $schema/$id/anchors and unknown annotations", + "combinations": "type with anyOf/$ref, or anyOf with $ref, is unverified in this local revision", + "enum": "Only scalar enum values; complex values unverified" + }, + "provider_ceilings": { + "total_object_properties": 5000, + "nesting_levels": 10, + "counted_name_enum_const_characters": 120000, + "total_enum_values": 1000, + "single_enum_threshold": 250, + "single_large_enum_characters": 15000 + }, + "local_counting_rules": { + "text": "Conservative UTF-8 bytes across property/definition names and string enum values; const is unverified and cannot be exported. This is not a claim that provider characters mean UTF-8 bytes.", + "depth": "Schema declaration depth including primitive children; anyOf keeps depth, properties/items add one. Definitions checked independently plus acyclic reference expansion from root. Active-path recursion back edges are not unrolled; shared DAG targets are revisited at their incoming depth. This is a frozen local interpretation, not a universal provider reference-depth assertion.", + "format_name": "Nonempty ASCII letters/digits/underscore/dash, maximum 64 bytes, corresponding to the output-format name rule.", + "function_name": "Separately frozen conservative local ASCII/64-byte admission rule; no inference of all Responses function-name behavior from Chat Completions.", + "metadata": "Envelope name/description and schema title/description UTF-8 count against the same provider metadata capacity." + }, + "local_presets": { + "conservative": { + "schemaNodes": 2000, + "referenceEdges": 4000, + "declarations": 20000, + "metadataBytes": 32768, + "json": "conservative" + }, + "balanced": { + "schemaNodes": 10000, + "referenceEdges": 20000, + "declarations": 100000, + "metadataBytes": 262144, + "json": "balanced" + }, + "offline": { + "schemaNodes": 100000, + "referenceEdges": 200000, + "declarations": 1000000, + "metadataBytes": 1048576, + "json": "offline" + } + }, + "accounting": "Existing 49 JSON metrics unchanged; all graph work, native compilation, metadata, envelope, writer and result copies share one meter. Provider edge counts include actual repeated inspections during reference-depth walks. Retained bytes are logical weights, not process RSS. Extra cumulative provider quantities stay UInt64; large finite allowances do not allocate capacity or weaken native addressable limits.", + "foreign_boundary": "Platform Data/String allocation/encoding implementation instructions are not an RSS or instruction guarantee. No SDK/model/network call occurs.", + "apple": "GenerationSchema, DynamicGenerationSchema and GeneratedContent bridges not implemented or qualified by this C5 increment; no placeholder Apple-typed API; apple-check remains unavailable/omitted from help.", + "frozen_inputs": [ + { + "path": "Tests/Fixtures/Providers/boolean-schema.json", + "sha256": "a17fcf0a2f50e2d495e4f90ce263410edc183add6c62699a2facbccf60410f74" + }, + { + "path": "Tests/Fixtures/Providers/exact-number-schema.json", + "sha256": "f8078228342835e88212a5b7dd4480546d199565b2309ec868da09d0550b3f68" + }, + { + "path": "Tests/Fixtures/Providers/optional-field-schema.json", + "sha256": "18bfd2748a3500923d20dadf06197b30c2f31c55df4db10b140673d076a69c2d" + }, + { + "path": "Tests/Fixtures/Providers/portable-summary-schema.json", + "sha256": "240e35a5e300003b58ddbda455e4d0af19aa7191add78fe0eb53270585c1f3a6" + }, + { + "path": "Tests/Fixtures/Providers/registry.json", + "sha256": "8a8376831bce17bfeb2ea7e7e8fcef0e3b695b4a67422768376c09f2cd580535" + }, + { + "path": "Tests/Fixtures/Providers/scalar-key-schema.json", + "sha256": "9bace13bae63652cd1c412e7fe5bdffe96eb5645dc3aa060d439ca378a5451ff" + } + ] +} diff --git a/Documentation/Architecture/Release1.4/Providers.md b/Documentation/Architecture/Release1.4/Providers.md new file mode 100644 index 0000000..bd56392 --- /dev/null +++ b/Documentation/Architecture/Release1.4/Providers.md @@ -0,0 +1,86 @@ +# Offline provider schema helpers + +PR27 adds the portable C5 helpers to JSONSchema, with no new product or root dependency. +The parent is main-reconciled aded3c9930f580bd4fb902bba002ab75ed199a7b; all earlier +reconciliation, resource-fix and release records remain history. + +`OpenAIJSONSchemaBridge(profile:limits:)` is an immutable checked-Sendable facade. +Select `.openAIResponsesFormatV1` or `.openAIResponsesFunctionV1` explicitly. +`check` and `checkWithReport` first perform native offline compilation, then return +`JSONSchemaProviderCompatibility`. Its bounded typed issues distinguish unsupported +and unverified declarations; keys remain `.key(JSONKey)` and array positions `.index`. +`isCompatible` remains false when zero issue capacity truncates every issue. + +`responseFormat` returns the native object to insert at Responses `text.format`. +`functionTool` returns one flat Responses function tool with object `parameters`. +Both set strict true and retain the original schema object, order, numeric values, +number lexemes and scalar-distinct keys. They never insert required/default/null, +strip constraints or bundle external documents. Metadata remains native JSON text. +The complete value, Data and String methods have WithReport counterparts. Native +preserved-number writing, output and result-copy overlap belong to that one ledger. + +The [frozen profile tables](ProviderProfiles.json) identify purposes, capability +category, revision, keyword/format allowlists, all finite presets, source dates and +counting rules. Provider ceilings and conservative local policy are separate. +Fine-tuned models and unqualified declarations are not automatically accepted. +Objects explicitly declare their properties map, including an empty map. +Title/description are retained metadata; const and string length keywords remain +unverified in this initial local subset. The function-name ASCII/64-byte rule is +an explicit conservative local policy, separate from output-format metadata rules. +The implementation guarantees local checks and unchanged request construction; +it does not promise that any model, service revision or account will accept them. + +## Completion and failures + +The CLI offers `schema provider openai-format|openai-tool SCHEMA --name NAME`, optional +`--description`, and explicit offline `--resource URI=PATH`. Success exits0 with a +fragment. Completed profile incompatibility exits1 with `{compatible,profile,revision, +capabilityCategory,issues,truncated}` and no schema/parameters candidate. A report +then says completed:true. Missing resources/native schema errors exit5; quota/overflow +exit6, IO exit7 and cancellation130. They never become compatibility false. CLI +terminal reports keep cumulative remaining work; public helper throws end that +operation without a successful report. Named outputs retain ordinary atomic rules. + +Only qualified portable helpers are advertised. `apple-check` remains unavailable +(exit2) before input IO; no FoundationModels import, placeholder success or fake +Apple-typed API is introduced. Provider-brief P2/P3 and optional GeneratedContent +work remain outside this C5 closure. No API request, credential lookup, model +session or online resolver exists in these helpers. + +Completed application output is a different boundary. The caller must first handle +transport failure, refusal and incomplete generation, then parse completed text +with JSONParser and validate it with JSONSchemaValidator. Validation false differs +from thrown operational errors. Arbitrary streaming deltas and valid prefixes are +not completed output. No OpenAI event-envelope client is added here. + +## Qualification and review + +Meaningful Swift Testing covers 13 schema functions and eight additional CLI functions: +exact value/text/data/report parity, scalar keys, metadata, optional/null, closure, +typed paths, local references/productive recursion, cross-definition depth, external +resources, invalid native schemas/regexes, truncation, all custom capacities, inclusive +work/output/retention boundaries, terminal latching, in-loop cancellation and sharing. +`Tools/CLIQualification/ProviderConsumer` imports only JSON/JSONSchema and executes14 +public checks, including instance-invalid versus operational error and cumulative work. +The process harness executes32 focused scenarios plus the five unchanged provider +fixtures; all six authored C5 vectors are now required by native/Linux/CLI CI. +Native exact comparison checks the complete fragment; a mere exit0 is insufficient. +Static dependency/call inspection establishes absence of a network client or model +session; no paid API/service-acceptance or socket-trace claim is made. + +CI retains precise platform inventories: 369 Apple/355 Linux root functions and199 +parameter cases, with65 schema and85 CLI functions. TSan covers12 Apple/11 Linux +shared functions. All49 baseline metric probes and every inherited case/exclusion +remain guarded. The WASI core expected schema inventory increases to65; fresh WASI +helper/platform execution must be identified separately before claiming it passed. + +Review corrected a depth gap across referenced definitions, rejected nonproductive +reference/combinator cycles, preserved productive cycles without a global seen-set +rejection, retained UInt64 capacities on wasm32, charged filtered graph scans and +reserved envelope/path storage before growth and preserved document retrieval URI +through native reference resolution. A strengthened C5 harness initially used the comparator's wrong expected +container; its actual array-of-document-text contract was restored, preserving full +native comparison. Compiler-driven fixes are recorded in private receipts; no test +or supported baseline was weakened. Final source-bound native/safety/docs/Linux, +archive/reproduction, exact commands/exits/counts and remaining limitations accompany +the separate return. This document is not itself an assertion that a pending lane passed. diff --git a/Documentation/Architecture/Release1.4/Reconciliation.md b/Documentation/Architecture/Release1.4/Reconciliation.md index bb455f6..5b4ddbe 100644 --- a/Documentation/Architecture/Release1.4/Reconciliation.md +++ b/Documentation/Architecture/Release1.4/Reconciliation.md @@ -39,3 +39,9 @@ threads and no-bypass protection are unchanged. No PR merge/tag/publication occu Provider CL078/CL079 remain blocked by the absent qualified helper declarations; WASI CLI keeps its explicit limited disposition. Main reconciliation does not convert these into successes or qualify MCP server/conformance behavior. + + +The subsequent portable provider increment addresses the previously blocked CL078/79. +Its [separate provider record](Providers.md) and new source-bound receipts supersede +that prerequisite blocker while preserving every reconciliation/archive observation +above. It does not qualify Apple model/schema bridges or unrestricted WASI CLI IO. diff --git a/Documentation/README.md b/Documentation/README.md index 9e022d6..251d9f2 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -53,3 +53,5 @@ Original code is Apache-2.0. See the [license](../LICENSE), [notice](../NOTICE), [adaptation manifest](Architecture/ExtractionManifest.json) and [pinned inputs](Architecture/UpstreamPins.json). Qualification inputs are separate from the empty library dependency graph. + +- [Offline provider schemas and request fragments](Architecture/Release1.4/Providers.md), [frozen profile tables](Architecture/Release1.4/ProviderProfiles.json) and the [DocC guide](../Sources/JSONSchema/Documentation.docc/ProviderSchemas.md). diff --git a/README.md b/README.md index b52692b..f2a71d1 100644 --- a/README.md +++ b/README.md @@ -520,3 +520,13 @@ See the [CLI reference](Documentation/Architecture/Release1.4/CLIReference.md), [acceptance ledger](Documentation/Architecture/Release1.4/Acceptance.json). This feature stack is under review; provider prerequisites and any unqualified platform rows remain explicit release blockers. It does not change released tags. + +### Offline provider fragments (1.4 development) + +The development CLI supports `swift-json schema provider openai-format schema.json --name summary` +and `openai-tool`. The public JSONSchema facade `OpenAIJSONSchemaBridge` checks a frozen strict +Responses base-model subset and returns native request fragments, with exact values and finite +cumulative reports. It makes no requests and never rewrites optional properties, keys or numbers. +Incompatibility exits1; operational errors remain errors. Apple schema/model bridges are separate. +See the [provider contract](Documentation/Architecture/Release1.4/Providers.md) and +[compiled public consumer](Tools/CLIQualification/Sources/ProviderConsumer/Main.swift). diff --git a/Scripts/ci.py b/Scripts/ci.py index 5186df8..af37d35 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -7,7 +7,7 @@ from testing_events import read_events, ROOT_PARAMETER_CASES ROOT = pathlib.Path(__file__).resolve().parents[1] -SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes|sha256InputCapacityAndConcurrentInvocationsRemainIndependent|sharedFormattedHTMLRendererHasIndependentConcurrentInvocations|sharedAnnotatedDocumentRendererHasIndependentConcurrentInvocations|sharedExportPreparationsHaveIndependentConcurrentState|applePDFSimultaneousJobsHaveIndependentStateAndCancellation|sharedFilterPlanHasIndependentConcurrentInvocations' +SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks|qaSharedPayloadAndRootHaveIndependentConcurrentReadScopes|sha256InputCapacityAndConcurrentInvocationsRemainIndependent|sharedFormattedHTMLRendererHasIndependentConcurrentInvocations|sharedAnnotatedDocumentRendererHasIndependentConcurrentInvocations|sharedExportPreparationsHaveIndependentConcurrentState|applePDFSimultaneousJobsHaveIndependentStateAndCancellation|sharedFilterPlanHasIndependentConcurrentInvocations|sharedProviderBridgeHasIndependentConcurrentInvocations' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -57,7 +57,7 @@ def commands(lane, output): return [['swift', 'build', '-c', 'release', '--product', 'swift-json'], ['swift', 'test', '--filter', 'cli|sharedFilterPlan'], ['python3', '-B', 'Scripts/qualify_cli.py', '--binary', '.build/release/swift-json', '--output', str(output / 'subprocess.json')], ['python3', '-B', 'Scripts/qualify_cli_integration.py', '--binary', '.build/release/swift-json', '--output', str(output / 'integration-subprocess.json')], ['python3', '-B', 'Scripts/qualify_cli_limits.py', '--binary', '.build/release/swift-json', '--output', str(output / 'cli-limits.json')], ['swift', 'build', '-c', 'release', '--package-path', 'Tools/CLIQualification', '--product', 'CLIExactCompare'], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C1', '--output', str(output / 'foundation-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C2', '--output', str(output / 'vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C3', '--output', str(output / 'arithmetic-vectors.json')], - ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C4', '--output', str(output / 'advanced-vectors.json')]] + ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C4', '--output', str(output / 'advanced-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C5', '--output', str(output / 'integration-vectors.json')], ['swift', 'run', '-c', 'release', '--package-path', 'Tools/CLIQualification', 'ProviderConsumer'], ['python3', '-B', 'Scripts/qualify_schema_providers.py', '--binary', '.build/release/swift-json', '--output', str(output / 'providers.json')]] if lane == 'native': return [ ['python3', '-B', 'Scripts/public_check.py'], @@ -72,6 +72,9 @@ def commands(lane, output): ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C1', '--output', str(output / 'foundation-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C2', '--output', str(output / 'vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C3', '--output', str(output / 'arithmetic-vectors.json')], ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C4', '--output', str(output / 'advanced-vectors.json')], + ['python3', '-B', 'Scripts/qualify_cli_cases.py', '--binary', '.build/release/swift-json', '--comparator', 'Tools/CLIQualification/.build/release/CLIExactCompare', '--gate', 'C5', '--output', str(output / 'integration-vectors.json')], + ['swift', 'run', '-c', 'release', '--package-path', 'Tools/CLIQualification', 'ProviderConsumer'], + ['python3', '-B', 'Scripts/qualify_schema_providers.py', '--binary', '.build/release/swift-json', '--output', str(output / 'providers.json')], ['python3', '-B', 'Scripts/qualify_cli_ownership.py', '--output', str(output / 'cli-ownership')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'ownership', '--output', str(output / 'ownership.json')], ['python3', '-B', 'Scripts/qualify_release_1_1.py', '--mode', 'reader', '--output', str(output / 'reader.json')], @@ -124,11 +127,11 @@ def check_result(command, code, log, events=None): if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [77] if 'cli|sharedFilterPlan' in command else [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1, 3 if sys.platform == 'linux' else 4, 4] if 'thread' in command else sorted([11, 12, 77, 14, 61 if sys.platform == 'linux' else 75, 52, 107]) + expected = [85] if 'cli|sharedFilterPlan' in command else [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else [1, 1, 2, 3 if sys.platform == 'linux' else 4, 4] if 'thread' in command else sorted([11, 12, 85, 14, 61 if sys.platform == 'linux' else 75, 65, 107]) passed = passed and sorted(counts) == expected if '--filter' 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'] == {'JSONTests':107,'JSONSchemaTests':52,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONRegexRuntimeTests':12,'JSONHTMLTests':61 if sys.platform == 'linux' else 75,'JSONCLITests':77} + passed = passed and events['module_function_counts'] == {'JSONTests':107,'JSONSchemaTests':65,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONRegexRuntimeTests':12,'JSONHTMLTests':61 if sys.platform == 'linux' else 75,'JSONCLITests':85} 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)) == 199) if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) @@ -144,10 +147,13 @@ def check_result(command, code, log, events=None): if command[:3] == ['python3', '-B', 'Scripts/qualify_cli_limits.py'] and passed: data = json.loads(log.strip().splitlines()[-1]) passed = data['observed'] == data['passed'] == 33 and data['failed'] == 0 and data['metrics'] == 16 + if command[:3] == ['python3', '-B', 'Scripts/qualify_schema_providers.py'] and passed: + data = json.loads(log.strip().splitlines()[-1]) + passed = data['status'] == 'passed' and (data['observed'], data['passed'], data['failed']) == (37, 37, 0) and data['input_unchanged'] and data['static_no_network_client_or_model_session'] and len(data['frozen_inputs']) == 6 if command[:3] == ['python3', '-B', 'Scripts/qualify_cli_cases.py'] and passed: data = json.loads(log.strip().splitlines()[-1]) - expected = {'C1':12,'C2':38,'C3':17,'C4':38}[command[command.index('--gate')+1]] - passed = data['observed'] == data['passed'] == expected and data['failed'] == data['policy'] == 0 + expected = {'C1':12,'C2':38,'C3':17,'C4':38,'C5':6}[command[command.index('--gate')+1]] + passed = data['observed'] == data['passed'] == expected and data['failed'] == data['policy'] == data['blocked'] == 0 if command == ['swift', 'package', 'dump-package'] and passed: d = json.loads(log) passed = sorted(p['name'] for p in d['products']) == ['JSON', 'JSONHTML', 'JSONPatch', 'JSONSchema', 'JSONStreaming', 'swift-json'] and not d['dependencies'] and d['swiftLanguageVersions'] == ['6'] and d['toolsVersion']['_version'] == '6.4.0' and d['platforms'] == [{'options': [], 'platformName': 'macos', 'version': '27.0'}, {'options': [], 'platformName': 'ios', 'version': '27.0'}] @@ -156,6 +162,8 @@ def check_result(command, code, log, events=None): passed = passed and set(targets) == {'JSON', 'JSONSchema', 'JSONStreaming', 'JSONPatch', 'JSONHTML', 'JSONRegexRuntime', 'JSONTests', 'JSONSchemaTests', 'JSONStreamingTests', 'JSONPatchTests', 'JSONHTMLTests', 'JSONRegexRuntimeTests', 'JSONFilterRuntime', 'JSONCLIEngine', 'JSONCLI', 'JSONCLITests'} and targets['JSONHTML']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONFilterRuntime']['dependencies'] == [{'byName': ['JSON', None]}] and targets['JSONCLI']['type'] == 'executable' if command == ['swift', 'package', 'show-dependencies', '--format', 'json'] and passed: passed = json.loads(log)['dependencies'] == [] + if command[:2] == ['swift', 'run'] and 'ProviderConsumer' in command: + passed = passed and 'PROVIDER_CONSUMER passed cases=14' in log.splitlines() if command[:2] == ['swift', 'run'] and 'Tools/MobileQualification' in command: passed = passed and all(line in log.splitlines() for line in ['PUBLIC_BEHAVIOR passed fixtures=8', 'STREAM_CANCELLATION passed', 'DISK_PERSISTENCE passed containers=3 rows=8', 'MOBILE_QUALIFICATION passed cases=3']) if command[:3] == ['python3', '-B', 'Scripts/verify_testing_events.py']: @@ -238,7 +246,7 @@ def save(): if receipt['status'] == 'passed' and args.lane == 'wasm': try: wasi = json.loads((output / 'probes/wasi.json').read_text()) - if wasi['status'] != 'passed' or wasi['root_counts'] != {'JSONTests':107,'JSONSchemaTests':52,'JSONRegexRuntimeTests':12,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONHTMLTests':61}: + if wasi['status'] != 'passed' or wasi['root_counts'] != {'JSONTests':107,'JSONSchemaTests':65,'JSONRegexRuntimeTests':12,'JSONStreamingTests':14,'JSONPatchTests':11,'JSONHTMLTests':61}: raise ValueError('Missing WASI qualification boundary') receipt['materialized_wasi'] = {'path':'probes/wasi.json','sha256':hashlib.sha256((output / 'probes/wasi.json').read_bytes()).hexdigest()} except (OSError,KeyError,ValueError,TypeError) as error: diff --git a/Scripts/qualify_cli_cases.py b/Scripts/qualify_cli_cases.py index 0fb46fa..8a8a5d2 100644 --- a/Scripts/qualify_cli_cases.py +++ b/Scripts/qualify_cli_cases.py @@ -46,6 +46,30 @@ def main(): passed=passed and b'9007199254740993' in result.stdout if probe.get('assert_no_external_assets'): passed=passed and not any(x in result.stdout.lower() for x in [b'src="http',b'src="//',b'@import',b'url(http']) + if provider and result.returncode in (0,1): + source_path=next(pathlib.Path(x) for x in argv if x.endswith('.json') and pathlib.Path(x).is_file()) + original=source_path.read_bytes() + if probe.get('assert_path_types'): + payload=json.loads(result.stdout) + for pointer,kind in probe['assert_path_types'].items(): + selected=payload + for token in pointer.lstrip('/').split('/'): + token=token.replace('~1','/').replace('~0','~') + selected=selected[int(token)] if isinstance(selected,list) else selected[token] + passed=passed and (isinstance(selected,dict) if kind=='object' else False) + name=argv[argv.index('--name')+1] + fragment=b'{"type":"json_schema","name":'+json.dumps(name).encode()+b',"strict":true,"schema":'+original.strip()+b'}' + expected_bytes=json.dumps([fragment.decode('utf-8')],ensure_ascii=False).encode('utf-8') + actual=directory/'provider-actual.json';expected=directory/'provider-expected.json' + actual.write_bytes(result.stdout);expected.write_bytes(expected_bytes) + checked=subprocess.run([str(comparator),str(actual),str(expected)],capture_output=True,timeout=30) + comparison={'exit':checked.returncode,'stdout':checked.stdout.decode(),'stderr':checked.stderr.decode()};passed=passed and checked.returncode==0 + if probe.get('assert_no_lowering'): + payload=json.loads(result.stdout) + passed=passed and payload.get('compatible') is False and 'schema' not in payload and 'parameters' not in payload and any(x.get('code')=='allPropertiesRequired' for x in payload.get('issues',[])) and source_path.read_bytes()==original + if probe.get('assert_no_network'): + paths=list((ROOT/'Sources/JSONSchema/Providers').glob('*.swift'))+[ROOT/'Sources/JSONCLIEngine/CLISchemaCommand.swift'] + passed=passed and all(not any(token in path.read_text() for token in ['import FoundationNetworking','URLSession','URLRequest','import OpenAI','LanguageModelSession']) for path in paths) if provider and result.returncode==2: try:blocked=json.loads(result.stderr)['code']=='capabilityUnavailable' and not result.stdout except (ValueError,KeyError,TypeError):blocked=False diff --git a/Scripts/qualify_cli_integration.py b/Scripts/qualify_cli_integration.py index 1625906..593dee5 100644 --- a/Scripts/qualify_cli_integration.py +++ b/Scripts/qualify_cli_integration.py @@ -35,7 +35,10 @@ def file(name, text): run('schema-missing-resource',['schema','check',schema],exit=5,expected=b'') run('schema-boolean',['schema','check',file('false.json','false')],check=lambda p:json.loads(p.stdout)['schemaKind']=='boolean') run('schema-invalid-declaration',['schema','check',file('invalid-schema.json','{"type":123}')],exit=5,expected=b'') - for capability in ['openai-format','openai-tool','apple-check']: + provider=file('provider.json','{"type":"object","properties":{"value":{"type":"integer"}},"required":["value"],"additionalProperties":false}') + run('provider-format',['schema','provider','openai-format',provider,'--name','sample'],check=lambda p:json.loads(p.stdout).get('schema')==json.loads(pathlib.Path(provider).read_text()) and json.loads(p.stdout).get('strict') is True) + run('provider-tool',['schema','provider','openai-tool',provider,'--name','sample'],check=lambda p:json.loads(p.stdout).get('parameters')==json.loads(pathlib.Path(provider).read_text()) and json.loads(p.stdout).get('type')=='function') + for capability in ['apple-check']: run('unavailable-'+capability,['schema','--diagnostics=json','provider',capability,'not-opened.json','--name','sample'],exit=2,expected=b'',check=lambda p:json.loads(p.stderr)['code']=='capabilityUnavailable') run('filter-utf8-is-compile',['query','-f',file('filter.bin',b'\xff')],b'null',exit=3,expected=b'',check=lambda p:b'invalidQueryUTF8' in p.stderr) run('missing-filter-file-is-io',['query','-f',str(directory/'missing')],exit=7,expected=b'') @@ -72,6 +75,6 @@ def file(name, text): good=bool(prefix) and p.returncode==130 and json.loads(err)['category']=='cancelled' rows.append({'id':'cancel-after-real-pipe-progress','exit_code':p.returncode,'expected_exit':130,'accepted_prefix_bytes':len(prefix)+len(out),'status':'passed' if good else 'failed'}) receipt={'schema_version':1,'status':'passed' if rows and all(r['status']=='passed' for r in rows) else 'failed','observed':len(rows),'passed':sum(r['status']=='passed' for r in rows),'failed':sum(r['status']=='failed' for r in rows),'binary_sha256':hashlib.sha256(binary.read_bytes()).hexdigest(),'cases':rows, - 'scope':'Qualified portable C5 adapters and unavailable-capability rejection; provider successes remain blocked in authored C5 vector receipt'} + 'scope':'Qualified portable C5 adapters, native provider fragments and unavailable Apple capability rejection'} output=ROOT/args.output;output.parent.mkdir(parents=True,exist_ok=True);output.write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps({k:v for k,v in receipt.items() if k!='cases'}));return 0 if receipt['status']=='passed' else 1 if __name__=='__main__':raise SystemExit(main()) diff --git a/Scripts/qualify_schema_providers.py b/Scripts/qualify_schema_providers.py new file mode 100644 index 0000000..ceb53bf --- /dev/null +++ b/Scripts/qualify_schema_providers.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Real offline CLI provider processes; ordinary incompatibility and errors differ.""" +import argparse, hashlib, json, pathlib, subprocess, tempfile +ROOT = pathlib.Path(__file__).resolve().parents[1] + +def main(): + parser = argparse.ArgumentParser(); parser.add_argument('--binary', required=True); parser.add_argument('--output', required=True); args = parser.parse_args() + binary = (ROOT / args.binary).resolve(); rows = [] + schema = b'{"type":"object","properties":{"value":{"type":"integer","minimum":9007199254740993}},"required":["value"],"additionalProperties":false}' + with tempfile.TemporaryDirectory(prefix='schema-provider-') as directory: + directory = pathlib.Path(directory) + def file(name, data): + path = directory / name; path.write_bytes(data if isinstance(data, bytes) else data.encode()); return str(path) + source = file('schema.json', schema) + def run(name, command, code=0, inspect=lambda p: True): + actual = subprocess.run([str(binary), *command], input=b'', capture_output=True, timeout=30) + try: good = actual.returncode == code and inspect(actual) + except (ValueError, TypeError, KeyError, IndexError): good = False + row = {'id': name, 'command': ['swift-json', *command], 'exit_code': actual.returncode, 'expected_exit': code, 'status': 'passed' if good else 'failed', 'stdout_sha256': hashlib.sha256(actual.stdout).hexdigest(), 'stderr_sha256': hashlib.sha256(actual.stderr).hexdigest()} + if not good: row.update(stdout=actual.stdout.decode(errors='replace'), stderr=actual.stderr.decode(errors='replace')); print(json.dumps(row), flush=True) + rows.append(row) + def command(path=source, tool=False, name='sample', extra=()): return ['schema', 'provider', 'openai-tool' if tool else 'openai-format', path, '--name', name, *extra] + def fragment(p, field='schema'): + value = json.loads(p.stdout) + return value.get(field) == json.loads(schema) and value.get('strict') is True and value.get('name') == 'sample' and value.get('type') == ('json_schema' if field == 'schema' else 'function') and 'function' not in value and 'text' not in value and b'9007199254740993' in p.stdout + run('format-object-exact', command(), inspect=fragment) + run('tool-flat-exact', command(tool=True), inspect=lambda p: fragment(p, 'parameters')) + optional = file('optional.json', b'{"type":"object","properties":{"value":{"type":"integer"}},"additionalProperties":false}') + run('optionality-completed-no-candidate', command(optional, extra=['--report=json']), 1, lambda p: json.loads(p.stdout).get('compatible') is False and 'schema' not in json.loads(p.stdout) and json.loads(p.stderr.splitlines()[-1])['completed'] is True and any(x['code'] == 'allPropertiesRequired' for x in json.loads(p.stdout)['issues'])) + nullable = file('nullable.json', b'{"type":"object","properties":{"value":{"type":["integer","null"]}},"required":["value"],"additionalProperties":false}') + run('explicit-nullability-preserved', command(nullable), inspect=lambda p: json.loads(p.stdout)['schema'] == json.loads(pathlib.Path(nullable).read_bytes())) + unknown = file('unknown.json', schema[:-1] + b',"x-unqualified":1}') + run('unverified-not-success', command(unknown), 1, lambda p: any(x['unverified'] for x in json.loads(p.stdout)['issues'])) + unsupported = file('unsupported.json', schema[:-1] + b',"allOf":[{"type":"object"}]}') + run('unsupported-not-lowered', command(unsupported), 1, lambda p: any(x['code'] == 'unsupportedKeyword' for x in json.loads(p.stdout)['issues'])) + run('boolean-root-incompatible', command(file('boolean.json', 'true')), 1, lambda p: json.loads(p.stdout)['compatible'] is False) + run('nonobject-root-incompatible', command(file('string.json', '{"type":"string"}')), 1, lambda p: json.loads(p.stdout)['compatible'] is False) + local = file('local.json', '{"type":"object","properties":{"value":{"$ref":"#/$defs/value"}},"required":["value"],"additionalProperties":false,"$defs":{"value":{"type":"integer"}}}') + run('local-ref-no-bundling', command(local), inspect=lambda p: json.loads(p.stdout)['schema'] == json.loads(pathlib.Path(local).read_bytes())) + recursive = file('recursive.json', '{"type":"object","properties":{"next":{"anyOf":[{"$ref":"#"},{"type":"null"}]}},"required":["next"],"additionalProperties":false}') + run('productive-recursion-not-unrolled', command(recursive), inspect=lambda p: json.loads(p.stdout)['schema'] == json.loads(pathlib.Path(recursive).read_bytes())) + external = file('external.json', '{"type":"object","properties":{"value":{"$ref":"https://example.invalid/value"}},"required":["value"],"additionalProperties":false}') + run('unresolved-is-operational', command(external, extra=['--diagnostics=json', '--report=json']), 5, lambda p: not p.stdout and json.loads(p.stderr.splitlines()[-1])['completed'] is False) + resource = file('integer.json', '{"type":"integer"}') + run('explicit-external-resolved-export-rejected', command(external, extra=['--resource', 'https://example.invalid/value=' + resource]), 1, lambda p: any(x['code'] == 'externalReference' for x in json.loads(p.stdout)['issues'])) + run('strict-input-grammar', command(file('bad.json', '{')), 5, lambda p: not p.stdout) + run('strict-input-utf8', command(file('bad-utf8.json', b'\xff')), 5, lambda p: not p.stdout) + run('invalid-native-schema-operational', command(file('native-bad.json', '{"type":123}')), 5, lambda p: not p.stdout) + bad_regex = file('regex.json', '{"type":"object","properties":{"value":{"type":"string","pattern":"["}},"required":["value"],"additionalProperties":false}') + run('invalid-native-regex-operational', command(bad_regex), 5, lambda p: not p.stdout) + run('name-required-before-io', ['schema', 'provider', 'openai-format', 'not-opened.json'], 2, lambda p: not p.stdout) + run('bad-name-before-io', command('not-opened.json', name='bad name'), 2, lambda p: not p.stdout) + run('long-name-before-io', command('not-opened.json', name='x' * 65), 2, lambda p: not p.stdout) + run('nonascii-name-before-io', command('not-opened.json', name='é'), 2, lambda p: not p.stdout) + run('apple-capability-unavailable', ['schema', 'provider', 'apple-check', 'not-opened.json', '--diagnostics=json'], 2, lambda p: not p.stdout and json.loads(p.stderr)['code'] == 'capabilityUnavailable') + run('unknown-provider-usage', ['schema', 'provider', 'unknown', 'not-opened.json'], 2, lambda p: not p.stdout) + run('api-key-option-not-exposed', command(extra=['--api-key', 'synthetic-not-a-secret']), 2, lambda p: not p.stdout) + work = file('work.json', '{"json":{"compileWorkUnits":0}}') + run('compile-quota-is-operational', command(extra=['--limits-file', work, '--diagnostics=json', '--report=json']), 6, lambda p: not p.stdout and json.loads(p.stderr.splitlines()[-1])['completed'] is False) + output = file('output-limit.json', '{"json":{"outputBytes":1}}') + run('writer-quota-is-operational', command(extra=['--limits-file', output]), 6, lambda p: not p.stdout) + target = pathlib.Path(file('protected.json', b'original')) + run('quota-preserves-atomic-output', command(extra=['--limits-file', output, '--output', str(target), '--overwrite']), 6, lambda p: not p.stdout and target.read_bytes() == b'original' and not list(directory.glob('.swift-json-*.tmp'))) + run('missing-file-is-io', command(str(directory / 'missing.json')), 7, lambda p: not p.stdout) + tail = file('tail.json', schema + b' false') + run('late-tail-preserves-atomic-output', command(tail, extra=['--output', str(target), '--overwrite']), 5, lambda p: not p.stdout and target.read_bytes() == b'original') + zero_issues = file('issues.json', '{"json":{"issues":0}}') + run('truncation-never-compatible', command(optional, extra=['--limits-file', zero_issues]), 1, lambda p: json.loads(p.stdout)['truncated'] is True and not json.loads(p.stdout)['issues'] and json.loads(p.stdout)['compatible'] is False) + run('name-inclusive-64', command(name='x' * 64), inspect=lambda p: json.loads(p.stdout)['name'] == 'x' * 64) + description = 'exact 🦊\n' + run('metadata-native-escaped', command(extra=['--description', description]), inspect=lambda p: json.loads(p.stdout)['description'] == description) + scalar = file('scalar.json', '{"type":"object","properties":{"é":{"type":"string"},"é":{"type":"string"}},"required":["é","é"],"additionalProperties":false}') + run('scalar-distinct-keys-retained', command(scalar), inspect=lambda p: len(json.loads(p.stdout)['schema']['properties']) == 2) + frozen = ROOT / 'Tests/Fixtures/Providers' + for fixture, expected in [('portable-summary-schema.json',0),('optional-field-schema.json',1),('scalar-key-schema.json',0),('exact-number-schema.json',0),('boolean-schema.json',1)]: + path = frozen / fixture + run('frozen-' + fixture, command(str(path)), expected, lambda p, expected=expected, path=path: json.loads(p.stdout).get('schema') == json.loads(path.read_bytes()) if expected == 0 else json.loads(p.stdout).get('compatible') is False and 'schema' not in json.loads(p.stdout)) + unchanged = pathlib.Path(source).read_bytes() == schema + source_files = list((ROOT / 'Sources/JSONSchema/Providers').glob('*.swift')) + [ROOT / 'Sources/JSONCLIEngine/CLISchemaCommand.swift'] + forbidden = ['import FoundationNetworking', 'import OpenAI', 'URLSession', 'URLRequest', 'LanguageModelSession'] + static_offline = all(not any(token in path.read_text() for token in forbidden) for path in source_files) + passed = len(rows) == 37 and all(row['status'] == 'passed' for row in rows) and static_offline and unchanged + receipt = {'schema_version': 1, 'status': 'passed' if passed else 'failed', 'observed': len(rows), 'passed': sum(row['status'] == 'passed' for row in rows), 'failed': sum(row['status'] == 'failed' for row in rows), 'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(), 'input_unchanged': unchanged, 'static_no_network_client_or_model_session': static_offline, 'network_scope': 'Local implementation/source invariant; no paid API request or remote acceptance test', 'profile_revision': '2026-10-15.v1', 'frozen_inputs': [{'path': path.relative_to(ROOT).as_posix(), 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()} for path in sorted((ROOT / 'Tests/Fixtures/Providers').glob('*.json'))], 'source_files': [{'path': path.relative_to(ROOT).as_posix(), 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()} for path in source_files], 'cases': rows} + destination = ROOT / args.output; destination.parent.mkdir(parents=True, exist_ok=True); destination.write_text(json.dumps(receipt, indent=2) + '\n'); print(json.dumps({k: v for k, v in receipt.items() if k not in ['cases', 'source_files']})); return 0 if passed else 1 +if __name__ == '__main__': raise SystemExit(main()) diff --git a/Scripts/wasm_support.py b/Scripts/wasm_support.py index 89fd5dc..064430c 100644 --- a/Scripts/wasm_support.py +++ b/Scripts/wasm_support.py @@ -6,7 +6,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SDK = 'swift-6.4.0-RELEASE_wasm' -COUNTS = {'JSONTests': 107, 'JSONSchemaTests': 52, 'JSONRegexRuntimeTests': 12, +COUNTS = {'JSONTests': 107, 'JSONSchemaTests': 65, 'JSONRegexRuntimeTests': 12, 'JSONStreamingTests': 14, 'JSONPatchTests': 11, 'JSONHTMLTests': 61} GROUPS = {'core': 6, 'resources': 1, 'schema': 2, 'streaming': 3, 'html': 6} diff --git a/Sources/JSONCLIEngine/CLIDiagnostics.swift b/Sources/JSONCLIEngine/CLIDiagnostics.swift index 5215324..80e32ac 100644 --- a/Sources/JSONCLIEngine/CLIDiagnostics.swift +++ b/Sources/JSONCLIEngine/CLIDiagnostics.swift @@ -38,6 +38,15 @@ package struct CLIDiagnostics { case .counterOverflow(let metric): return .init(.resource, "counterOverflow", metric: metric.rawValue, overflow: true) } } + if let failure = error as? JSONSchemaProviderFailure { + switch failure { + case .invalidConfiguration: return .init(.usage, "invalidProviderLimits") + case .profileMismatch: return .init(.usage, "providerProfileMismatch") + case .invalidMetadata: return .init(.usage, "invalidProviderMetadata") + case .incompatible: return .init(.input, "providerIncompatible") // The schema command handles completed compatibility separately. + case .limitExceeded(let metric, let limit, let attempted): return .init(.resource, "limitExceeded", metric: "provider." + metric.rawValue, limit: limit, attemptedUsage: attempted, overflow: attempted == nil) + } + } if let failure = error as? JSONSchemaCompilationFailure { return .init(.input, "schemaCompilation." + failure.category.rawValue) } if let failure = error as? JSONSchemaEvaluationFailure { return .init(.input, "schemaEvaluation." + failure.category.rawValue) } // Schema errors remain operational; no unknown error becomes validation false. diff --git a/Sources/JSONCLIEngine/CLIHelp.swift b/Sources/JSONCLIEngine/CLIHelp.swift index 80a69fb..db7fa7b 100644 --- a/Sources/JSONCLIEngine/CLIHelp.swift +++ b/Sources/JSONCLIEngine/CLIHelp.swift @@ -20,7 +20,7 @@ package enum CLIHelp { #else let architecture = "unknown" #endif - return "{\"version\":\"1.4.0-dev\",\"revision\":\"unknown\",\"platform\":\"\(platform)\",\"architecture\":\"\(architecture)\",\"features\":[\"format\",\"validate\",\"pointer\",\"patch\",\"query\",\"stream\",\"schema-check\",\"render-html\",\"render-markdown\",\"render-text\"],\"dialect\":\"swift-json-filter/1\"}\n" + return "{\"version\":\"1.4.0-dev\",\"revision\":\"unknown\",\"platform\":\"\(platform)\",\"architecture\":\"\(architecture)\",\"features\":[\"format\",\"validate\",\"pointer\",\"patch\",\"query\",\"stream\",\"schema-check\",\"openai-format\",\"openai-tool\",\"render-html\",\"render-markdown\",\"render-text\"],\"dialect\":\"swift-json-filter/1\"}\n" } package static func text(for command: String?) throws -> String { let common = """ @@ -89,6 +89,12 @@ package enum CLIHelp { Usage: swift-json schema check SCHEMA [--resource URI=PATH] [OPTIONS] Compiles one offline Draft 2020-12 object/Boolean schema and emits completed metadata. This command does not validate an instance or fetch. + schema provider openai-format|openai-tool SCHEMA --name NAME + Optional --description TEXT and explicit --resource URI=PATH. + Frozen strict Responses base-model profiles preserve declarations unchanged. + Success emits a native text.format fragment or flat function tool. Incompatible + or unverified schemas emit bounded compatibility issues and exit 1. + These local helpers make no API requests and do not guarantee service acceptance. """ case "render": body = """ diff --git a/Sources/JSONCLIEngine/CLISchemaCommand.swift b/Sources/JSONCLIEngine/CLISchemaCommand.swift index ad95acc..c571fd5 100644 --- a/Sources/JSONCLIEngine/CLISchemaCommand.swift +++ b/Sources/JSONCLIEngine/CLISchemaCommand.swift @@ -5,13 +5,30 @@ import JSONFilterRuntime package enum CLISchemaCommand { package static func run(_ arguments: CLIArguments, context: inout CLIJobContext) throws -> Int32 { - if arguments.positionals.first == "provider" { - guard arguments.positionals.count >= 2, ["openai-format", "openai-tool", "apple-check"].contains(arguments.positionals[1]) else { throw CLIError(.usage, "unknownSchemaCommand") } - throw CLIError(.usage, "capabilityUnavailable") + let provider = arguments.positionals.first == "provider" + var purpose: JSONSchemaProviderProfile? + let schemaPath: String + if provider { + guard arguments.positionals.count == 3 else { throw CLIError(.usage, "invalidSchemaCommand") } + switch arguments.positionals[1] { + case "openai-format": purpose = .openAIResponsesFormatV1 + case "openai-tool": purpose = .openAIResponsesFunctionV1 + case "apple-check": throw CLIError(.usage, "capabilityUnavailable") + default: throw CLIError(.usage, "unknownSchemaCommand") + } + guard let name = arguments.options["name"], !name.isEmpty else { throw CLIError(.usage, "providerNameRequired") } + var count: UInt64 = 0 + for byte in name.utf8 { + try context.work(); count += 1 + guard (65...90).contains(byte) || (97...122).contains(byte) || (48...57).contains(byte) || byte == 95 || byte == 45 else { throw CLIError(.usage, "invalidProviderName") } + } + guard count <= 64 else { throw CLIError(.usage, "invalidProviderName") } + schemaPath = arguments.positionals[2] + } else { + guard arguments.positionals.count == 2, arguments.positionals[0] == "check" else { throw CLIError(.usage, "invalidSchemaCommand") } + guard arguments.options["name"] == nil && arguments.options["description"] == nil else { throw CLIError(.usage, "invalidSchemaOption") } + schemaPath = arguments.positionals[1] } - guard arguments.positionals.count == 2, arguments.positionals[0] == "check" else { throw CLIError(.usage, "invalidSchemaCommand") } - guard arguments.options["name"] == nil && arguments.options["description"] == nil else { throw CLIError(.usage, "invalidSchemaOption") } - let schemaPath = arguments.positionals[1] let resourcePaths = try arguments.repeated.map { pair -> String in let parts = pair.1.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) guard pair.0 == "resource", parts.count == 2, !parts[0].isEmpty, !parts[1].isEmpty else { throw CLIError(.usage, "invalidResource") } @@ -33,6 +50,21 @@ package enum CLISchemaCommand { documents.append(try JSONSchemaDocument(resource, retrievalURI: uri, meter: &context.meter)) } let registry = try JSONSchemaRegistry.checked(documents, meter: &context.meter) + if let purpose { + var limits: JSONSchemaProviderLimits = context.limits.profileName == "conservative" ? .conservative : context.limits.profileName == "offline" ? .offline : .balanced + limits.json = context.meter.limits + let bridge = OpenAIJSONSchemaBridge(profile: purpose, limits: limits) + let value: JSON, status: Int32 + do { + value = try bridge.prepare(raw, name: arguments.options["name"]!, description: arguments.options["description"], registry: registry, purpose: purpose, meter: &context.meter) + status = 0 + } catch JSONSchemaProviderFailure.incompatible(let compatibility) { + value = try compatibilityJSON(compatibility, context: &context); status = 1 + } + try CLIEngine.emit(value, arguments: arguments, output: output, compact: true, context: &context) + try output.finish(inputs: inputs, context: &context) + return status + } let plan = try JSONSchemaCompiler(options: .init(limits: context.meter.limits)).compile(raw, registry: registry, meter: &context.meter) var metadata: [(String, JSON)] = [("completed", .boolean(true)), ("dialect", .string("https://json-schema.org/draft/2020-12/schema")), ("schemaKind", .string(plan.document.raw.isBooleanSchema ? "boolean" : "object")), ("documents", .number(JSONNumber(integer: plan.plan.documents.count))), ("compiledNodes", .number(JSONNumber(integer: plan.plan.nodes.count))), ("resources", .number(JSONNumber(integer: plan.plan.resources.count)))] if let uri = plan.document.retrievalURI { metadata.append(("retrievalURI", .string(uri.string))) } @@ -41,5 +73,22 @@ package enum CLISchemaCommand { try output.finish(inputs: inputs, context: &context) return 0 } + private static func compatibilityJSON(_ report: JSONSchemaProviderCompatibility, context: inout CLIJobContext) throws -> JSON { + var issues: [JSON] = [] + for issue in report.issues { + try context.work(); try context.meter.retain(32) + var path: [JSON] = [] + for component in issue.path { + try context.work(); try context.meter.retain(32) + switch component { + case .key(let key): path.append(try CLIEngine.object([("kind", .string("key")), ("key", .string(key.string))], context: &context)) + case .index(let index): path.append(try CLIEngine.object([("kind", .string("index")), ("index", .number(JSONNumber(integer: index)))], context: &context)) + } + } + issues.append(try CLIEngine.object([("code", .string(issue.code.rawValue)), ("path", .array(path)), ("unverified", .boolean(issue.unverified)), ("explanation", .string(issue.explanation))], context: &context)) + } + return try CLIEngine.object([("compatible", .boolean(report.isCompatible)), ("profile", .string(report.profile.rawValue)), ("revision", .string(report.profile.revision)), ("capabilityCategory", .string(report.profile.capabilityCategory)), ("issues", .array(issues)), ("truncated", .boolean(report.truncated))], context: &context) + } + } private extension JSON { var isBooleanSchema: Bool { if case .boolean = self { return true }; return false } } diff --git a/Sources/JSONSchema/Documentation.docc/JSONSchema.md b/Sources/JSONSchema/Documentation.docc/JSONSchema.md index 522e2b9..19420cc 100644 --- a/Sources/JSONSchema/Documentation.docc/JSONSchema.md +++ b/Sources/JSONSchema/Documentation.docc/JSONSchema.md @@ -64,6 +64,7 @@ Internal mutable meters remain private. ### Guides - +- ### Documents and plans - ``JSONSchemaDocument`` @@ -86,3 +87,13 @@ Internal mutable meters remain private. - ``JSONSchemaNodeView`` - ``JSONDeclaredAnnotations`` - ``JSONSchemaReferenceTarget`` + +### Provider request fragments +- ``OpenAIJSONSchemaBridge`` +- ``JSONSchemaProviderProfile`` +- ``JSONSchemaProviderLimits`` +- ``JSONSchemaProviderCompatibility`` +- ``JSONSchemaProviderIssue`` +- ``JSONSchemaProviderIssueCode`` +- ``JSONSchemaProviderMetric`` +- ``JSONSchemaProviderFailure`` diff --git a/Sources/JSONSchema/Documentation.docc/ProviderSchemas.md b/Sources/JSONSchema/Documentation.docc/ProviderSchemas.md new file mode 100644 index 0000000..7f0e001 --- /dev/null +++ b/Sources/JSONSchema/Documentation.docc/ProviderSchemas.md @@ -0,0 +1,57 @@ +# Prepare strict Responses schema fragments + +Use explicit provider profiles to check an offline schema and preserve its declarations. + +```swift +import JSON +import JSONSchema + +let document = try JSONSchemaDocument(JSONParser().parse( + #"{"type":"object","properties":{"topic":{"type":"string"}},"required":["topic"],"additionalProperties":false}"# +)) +let bridge = OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1) +let checked = try bridge.checkWithReport(document) +if checked.result.isCompatible { + let fragment = try bridge.responseFormatWithReport(document, name: "summary") + // The caller inserts fragment.result at Responses text.format. + // statistics reports this complete helper operation's cumulative work. + _ = fragment.statistics[.workUnits] +} +``` + +Use a separate `.openAIResponsesFunctionV1` facade and ``OpenAIJSONSchemaBridge/functionTool(_:name:description:registry:)`` +for flat function tools. These helpers construct native JSON; they never make requests, +read credentials or select a model. Data/String conveniences use the same strict native +writer and preserve exact number lexemes. Every compound WithReport method accounts +for compilation, profile checking, metadata, envelope and any owned output copies in +one finite operation. Separate public calls have separate budgets; carry remaining +work explicitly, and end the application operation if a stage throws. + +Optional means absent; nullable means a present null. The strict profile requires all +properties to be required and objects to declare a properties map and be closed. It rejects incompatible schemas +instead of adding required/default/null or dropping constraints. Native compilation, +profile compatibility and remote service acceptance are different outcomes. +``JSONSchemaProviderCompatibility`` retains bounded typed paths and an explicit +truncated flag. Zero issue capacity cannot turn an incompatible check into success. +Thrown resource, cancellation, IO and compilation failures are never compatibility false. + +The frozen 2026-10-15.v1 base-model subset supports explicit object roots, selected +primitives/constraints, local definitions, anyOf and productive local recursion. +External references may be resolved only from an explicit registry, and remain +incompatible for export because there is no bundling policy. Unknown/unverified +keywords and unsupported combinations prevent strict export. No fine-tuned-model +support or universal provider equivalence is inferred. Conservative UTF-8 and finite +graph/depth policies are local rules; they do not define the service's character units. +Both names use an explicit ASCII/64-byte admission policy; the function policy is a +separate conservative local restriction. Full tables and source dates are in the +repository's Release1.4/ProviderProfiles.json contract. + +The application owns completion. Handle transport failures, refusals and incomplete +responses first. Parse only completed output text with JSONParser, then validate the +instance against its independently compiled native plan. Invalid instances and thrown +operational failures remain distinct. A syntactically valid streamed prefix is not a +completed response. No event-envelope client or Apple model session is provided. + +Public runnable examples and14 behavior checks live in +Tools/CLIQualification/Sources/ProviderConsumer. Apple GenerationSchema/DynamicGenerationSchema +bridges are a separate qualification slice and are not exposed by this increment. diff --git a/Sources/JSONSchema/Providers/JSONSchemaProviderInspection.swift b/Sources/JSONSchema/Providers/JSONSchemaProviderInspection.swift new file mode 100644 index 0000000..ab0d543 --- /dev/null +++ b/Sources/JSONSchema/Providers/JSONSchemaProviderInspection.swift @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Invocation-owned traversal of the compiler's finite graph; references are never unrolled. +package struct JSONSchemaProviderInspection { + let profile: JSONSchemaProviderProfile + let limits: JSONSchemaProviderLimits + var nodes: UInt64 = 0, edges: UInt64 = 0, declarations: UInt64 = 0, metadata: UInt64 = 0 + var properties: UInt64 = 0, enumValues: UInt64 = 0, countedText: UInt64 = 0 + var issues: [JSONSchemaProviderIssue] = [] + var incompatible = false, truncated = false + + mutating func charge(_ metric: JSONSchemaProviderMetric, _ amount: UInt64, meter: inout JSONWorkMeter) throws { + let current: UInt64, cap: UInt64 + switch metric { + case .schemaNodes: current = nodes; cap = limits.schemaNodes + case .referenceEdges: current = edges; cap = limits.referenceEdges + case .declarations: current = declarations; cap = limits.declarations + case .metadataBytes: current = metadata; cap = limits.metadataBytes + } + let (next, overflow) = current.addingReportingOverflow(amount) + guard !overflow, next <= cap else { + let error = JSONSchemaProviderFailure.limitExceeded(metric: metric, limit: cap, attempted: overflow ? nil : next) + meter.recordFailure(error); throw error + } + try meter.work(amount) + switch metric { + case .schemaNodes: nodes = next + case .referenceEdges: edges = next + case .declarations: declarations = next + case .metadataBytes: metadata = next + } + } + mutating func issue(_ code: JSONSchemaProviderIssueCode, _ path: [JSONLocationComponent], unverified: Bool = false, meter: inout JSONWorkMeter) throws { + incompatible = true; try meter.work() + guard UInt64(issues.count) < meter.limits[.issues] else { truncated = true; return } + // Path slots, key bytes and fixed explanations overlap the source/plan. + try meter.require(.pointerTokens, UInt64(path.count)) + for component in path { + try meter.work(); try meter.retain(32) + if case .key(let key) = component { + for _ in key.string.utf8 { try meter.work(); try meter.charge(.diagnosticBytes); try meter.retain(1) } + } + } + let value = JSONSchemaProviderIssue(code: code, path: path, unverified: unverified) + for _ in value.explanation.utf8 { try meter.work(); try meter.charge(.diagnosticBytes); try meter.retain(1) } + try meter.retain(64); issues.append(value) + } + mutating func total(_ current: UInt64, _ amount: UInt64, ceiling: UInt64, path: [JSONLocationComponent], meter: inout JSONWorkMeter) throws -> UInt64 { + let (next, overflow) = current.addingReportingOverflow(amount) + guard !overflow else { + let error = JSONSchemaProviderFailure.limitExceeded(metric: .declarations, limit: limits.declarations, attempted: nil) + meter.recordFailure(error); throw error + } + if next > ceiling { try issue(.profileCapacity, path, meter: &meter) } + return next + } + mutating func counted(_ text: String, path: [JSONLocationComponent], meter: inout JSONWorkMeter) throws -> UInt64 { + var bytes: UInt64 = 0 + for _ in text.utf8 { try meter.work(); bytes += 1 } + // Explicitly local conservative UTF-8 rule, not a claim about provider character units. + countedText = try total(countedText, bytes, ceiling: 120_000, path: path, meter: &meter) + return bytes + } + mutating func metadataText(_ text: String, meter: inout JSONWorkMeter) throws { + for _ in text.utf8 { try charge(.metadataBytes, 1, meter: &meter) } + } + func result() -> JSONSchemaProviderCompatibility { + .init(profile: profile, issues: issues, truncated: truncated, isCompatible: !incompatible, + checkedSchemaNodes: nodes, checkedReferenceEdges: edges, checkedDeclarations: declarations) + } + private func ascii(_ text: String, meter: inout JSONWorkMeter) throws -> Bool { + for byte in text.utf8 { try meter.work(); if byte > 127 { return false } } + return true + } + private func lookup(_ object: JSONObject, _ key: String, meter: inout JSONWorkMeter) throws -> JSON? { try object.lookup(key, meter: &meter) } + + mutating func inspect(_ compiled: CompiledJSONSchema, meter: inout JSONWorkMeter) throws -> JSONSchemaProviderCompatibility { + let plan = compiled.plan, rootDocument = plan.nodes[plan.root].document + struct Frame { let node: Int; let path: [JSONLocationComponent]; let depth: UInt64 } + try meter.retain(64) + var stack = [Frame(node: plan.root, path: [], depth: 1)] + while let frame = stack.popLast() { + try meter.checkpoint(); try charge(.schemaNodes, 1, meter: &meter) + let node = plan.nodes[frame.node], path = frame.path + guard case .object(let object) = node.raw else { + try issue(.booleanSchema, path, meter: &meter); continue + } + let type = try lookup(object, "type", meter: &meter) + let anyOf = try lookup(object, "anyOf", meter: &meter) + let ref = try lookup(object, "$ref", meter: &meter) + if frame.node == plan.root, type != .string("object") || anyOf != nil || ref != nil { + try issue(.objectRootRequired, path, meter: &meter) + } + if frame.depth > 10 { try issue(.profileCapacity, path, meter: &meter) } + var types: [String] = [] + if case .string(let value) = type { types = [value] } + else if case .array(let array) = type { + for value in array { try meter.work(); if case .string(let name) = value { try meter.retain(16); types.append(name) } } + if types.count != 2 || !types.contains("null") { try issue(.unverifiedDeclaration, path + [.key(JSONKey("type"))], unverified: true, meter: &meter) } + } + for name in types { + if try !ascii(name, meter: &meter) || !["object", "array", "string", "number", "integer", "boolean", "null"].contains(name) { + try issue(.unverifiedDeclaration, path + [.key(JSONKey("type"))], unverified: true, meter: &meter) + } + } + if type == nil && anyOf == nil && ref == nil || type != nil && (anyOf != nil || ref != nil) || anyOf != nil && ref != nil { + try issue(.unverifiedDeclaration, path, unverified: true, meter: &meter) + } + if types.contains("object") { + if try lookup(object, "additionalProperties", meter: &meter) != .boolean(false) { + try issue(.closedObjectRequired, path + [.key(JSONKey("additionalProperties"))], meter: &meter) + } + let declared = try lookup(object, "properties", meter: &meter)?.objectValue + let required = try lookup(object, "required", meter: &meter)?.arrayValue + if declared == nil { try issue(.unverifiedDeclaration, path + [.key(JSONKey("properties"))], unverified: true, meter: &meter) } + var complete = required != nil && (required?.count ?? 0) == (declared?.count ?? 0) + if let required { + for value in required { + try meter.work() + guard case .string(let name) = value else { complete = false; continue } + if try declared?.lookup(name, meter: &meter) == nil { complete = false } + } + } + if !complete { try issue(.allPropertiesRequired, path + [.key(JSONKey("required"))], meter: &meter) } + properties = try total(properties, UInt64(declared?.count ?? 0), ceiling: 5_000, path: path, meter: &meter) + } else if try lookup(object, "properties", meter: &meter) != nil || lookup(object, "required", meter: &meter) != nil || lookup(object, "additionalProperties", meter: &meter) != nil { + try issue(.unverifiedDeclaration, path, unverified: true, meter: &meter) + } + if types.contains("array"), try lookup(object, "items", meter: &meter) == nil { + try issue(.unverifiedDeclaration, path + [.key(JSONKey("items"))], unverified: true, meter: &meter) + } + for member in object { + try charge(.declarations, 1, meter: &meter) + let key = member.key.string, location = path + [.key(member.key)] + guard try ascii(key, meter: &meter) else { try issue(.unverifiedDeclaration, location, unverified: true, meter: &meter); continue } + switch key { + case "type", "properties", "required", "additionalProperties", "items", "anyOf", "$ref": break + case "$defs": + if case .object(let definitions) = member.value { + for definition in definitions { _ = try counted(definition.key.string, path: location + [.key(definition.key)], meter: &meter) } + } + case "description", "title": + if case .string(let text) = member.value { try metadataText(text, meter: &meter) } + case "enum": + if case .array(let values) = member.value { + enumValues = try total(enumValues, UInt64(values.count), ceiling: 1_000, path: location, meter: &meter) + var stringBytes: UInt64 = 0 + for (index, value) in values.enumerated() { + try meter.work() + if case .string(let text) = value { + let bytes = try counted(text, path: location + [.index(index)], meter: &meter) + stringBytes = try total(stringBytes, bytes, ceiling: values.count > 250 ? 15_000 : UInt64.max, path: location, meter: &meter) + } else if value.objectValue != nil || value.arrayValue != nil { + try issue(.unverifiedDeclaration, location + [.index(index)], unverified: true, meter: &meter) + } + } + } + case "pattern", "format": + if !types.contains("string") { try issue(.unverifiedDeclaration, location, unverified: true, meter: &meter) } + if key == "format", case .string(let format) = member.value, + try !ascii(format, meter: &meter) || !["date-time", "time", "date", "duration", "email", "hostname", "ipv4", "ipv6", "uuid"].contains(format) { + try issue(.unsupportedFormat, location, meter: &meter) + } + case "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf": + if !types.contains("number") && !types.contains("integer") { try issue(.unverifiedDeclaration, location, unverified: true, meter: &meter) } + case "minItems", "maxItems": + if !types.contains("array") { try issue(.unverifiedDeclaration, location, unverified: true, meter: &meter) } + case "allOf", "oneOf", "not", "if", "then", "else", "$dynamicRef", "$dynamicAnchor", "dependentRequired", "dependentSchemas", "unevaluatedProperties", "unevaluatedItems", "patternProperties", "prefixItems", "contains", "propertyNames", "uniqueItems": + try issue(.unsupportedKeyword, location, meter: &meter) + default: try issue(.unverifiedDeclaration, location, unverified: true, meter: &meter) + } + } + if let declared = try lookup(object, "properties", meter: &meter)?.objectValue { + for member in declared { _ = try counted(member.key.string, path: path + [.key(JSONKey("properties")), .key(member.key)], meter: &meter) } + } + if let ref { + if case .string(let text) = ref, text.utf8.first != 35 { try issue(.externalReference, path + [.key(JSONKey("$ref"))], meter: &meter) } + var chain: [Int] = [], current = frame.node + while true { + try meter.work() + var repeated = false + for previous in chain { try meter.work(); if previous == current { repeated = true; break } } + if repeated { try issue(.nonProductiveReference, path + [.key(JSONKey("$ref"))], meter: &meter); break } + try meter.retain(16); chain.append(current) + let target = plan.nodes[current] + guard let raw = target.raw.objectValue, + try lookup(raw, "type", meter: &meter) == nil, + try lookup(raw, "anyOf", meter: &meter) == nil, + let reference = target.references.first else { break } + try charge(.referenceEdges, 1, meter: &meter) + if plan.nodes[reference.target].document != rootDocument { try issue(.externalReference, path + [.key(JSONKey("$ref"))], meter: &meter); break } + current = reference.target + } + } + for child in node.children { + try charge(.referenceEdges, 1, meter: &meter) + guard [JSONSchemaPlan.Keyword.properties, .defs, .items, .anyOf].contains(child.keyword) else { continue } + var location = path + [.key(JSONKey(child.keyword.rawValue))] + if let token = child.token { + if child.keyword == .anyOf { + guard let index = Int(token.string), index >= 0 else { throw JSONFailure(.configuration, code: .invalidOperation) } + location.append(.index(index)) + } else { location.append(.key(token)) } + } + try meter.require(.pointerTokens, UInt64(location.count)) + try meter.retain(64) + for _ in location { try meter.work(); try meter.retain(32) } + stack.append(.init(node: child.target, path: location, depth: child.keyword == .defs ? 1 : child.keyword == .anyOf ? frame.depth : frame.depth + 1)) + } + } + try referenceDepth(compiled, meter: &meter) + return result() + } + /// Check non-recursive reference expansion too. Only active-path back edges are + /// skipped: a shared DAG node is revisited at each actual incoming depth. + private mutating func referenceDepth(_ compiled: CompiledJSONSchema, meter: inout JSONWorkMeter) throws { + let plan = compiled.plan, document = plan.nodes[plan.root].document + struct Frame { let node: Int; let depth: UInt64; let leaving: Bool } + try meter.retain(UInt64(plan.nodes.count) + 64) + var active = Array(repeating: false, count: plan.nodes.count) + var activeDepth: [UInt64] = [] + for _ in plan.nodes { try meter.work(); try meter.retain(8); activeDepth.append(0) } + var stack = [Frame(node: plan.root, depth: 1, leaving: false)] + for node in plan.nodes { + try meter.work() + guard node.document == document else { continue } + for child in node.children { + try meter.work() + guard child.keyword == .defs else { continue } + try charge(.referenceEdges, 1, meter: &meter) + try meter.retain(64); stack.append(.init(node: child.target, depth: 1, leaving: false)) + } + } + while let frame = stack.popLast() { + try meter.work() + if frame.leaving { active[frame.node] = false; continue } + if active[frame.node] { + if frame.depth == activeDepth[frame.node] { + let path = try typedPath(plan.nodes[frame.node].pointer, raw: compiled.document.raw, meter: &meter) + try issue(.nonProductiveReference, path, meter: &meter) + } + continue + } + let node = plan.nodes[frame.node] + if frame.depth > 10 { + let path = try typedPath(node.pointer, raw: compiled.document.raw, meter: &meter) + try issue(.profileCapacity, path, meter: &meter); continue + } + active[frame.node] = true; activeDepth[frame.node] = frame.depth + try meter.retain(64); stack.append(.init(node: frame.node, depth: frame.depth, leaving: true)) + for reference in node.references { + try meter.work() + guard plan.nodes[reference.target].document == document else { continue } + try charge(.referenceEdges, 1, meter: &meter) + try meter.retain(64); stack.append(.init(node: reference.target, depth: frame.depth, leaving: false)) + } + for child in node.children { + try meter.work() + guard [JSONSchemaPlan.Keyword.properties, .items, .anyOf].contains(child.keyword) else { continue } + try charge(.referenceEdges, 1, meter: &meter) + try meter.retain(64); stack.append(.init(node: child.target, depth: child.keyword == .anyOf ? frame.depth : frame.depth + 1, leaving: false)) + } + } + } + private func typedPath(_ pointer: JSONPointer, raw: JSON, meter: inout JSONWorkMeter) throws -> [JSONLocationComponent] { + var current = raw, path: [JSONLocationComponent] = [] + for token in pointer.tokens { + try meter.work(); try meter.retain(32) + switch current { + case .object(let object): + guard let value = try object.lookup(token.string, meter: &meter) else { throw JSONFailure(.configuration, code: .invalidOperation) } + path.append(.key(token)); current = value + case .array(let array): + guard let index = Int(token.string), index >= 0 && index < array.count else { throw JSONFailure(.configuration, code: .invalidOperation) } + path.append(.index(index)); current = array[index] + default: throw JSONFailure(.configuration, code: .invalidOperation) + } + } + return path + } + +} diff --git a/Sources/JSONSchema/Providers/JSONSchemaProviderProfile.swift b/Sources/JSONSchema/Providers/JSONSchemaProviderProfile.swift new file mode 100644 index 0000000..cd84dbd --- /dev/null +++ b/Sources/JSONSchema/Providers/JSONSchemaProviderProfile.swift @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +import JSON + +/// Frozen local checks for Responses base-model schemas; never a remote acceptance guarantee. +public enum JSONSchemaProviderProfile: String, Sendable, CaseIterable { + /// Contents of Responses `text.format`, using the documented base-model subset. + case openAIResponsesFormatV1 = "openai.responses.text-format.v1" + /// One Responses function tool, using the documented base-model subset. + case openAIResponsesFunctionV1 = "openai.responses.function.v1" + /// The policy revision; changing provider documentation never changes this profile at runtime. + public var revision: String { "2026-10-15.v1" } + /// The provider described by these local rules. + public var provider: String { "openai" } + /// Qualified capability category; fine-tuned and unspecified model behavior is not inferred. + public var capabilityCategory: String { "documented-base-model-subset" } +} + +/// Additional finite admission capacities; existing JSON limits also apply cumulatively. +public struct JSONSchemaProviderLimits: Sendable { + /// Native compilation, text, diagnostic, output and logical retention limits. + public var json: JSONLimits + /// Caller-schema graph nodes, excluding native meta-schema bootstrap nodes. + public var schemaNodes: UInt64 + /// Caller-schema child and reference edges, without recursive expansion. + public var referenceEdges: UInt64 + /// Immediate declarations across checked caller-schema nodes. + public var declarations: UInt64 + /// UTF-8 bytes of envelope and schema title/description metadata. + public var metadataBytes: UInt64 + /// Creates finite capacities. Zero forbids use; invalid capacities throw at operation entry. + /// - Parameters: + /// - json: Native limits shared by every owned phase. + /// - schemaNodes: Maximum inspected schema nodes. + /// - referenceEdges: Maximum inspected child/reference edges. + /// - declarations: Maximum inspected keyword declarations. + /// - metadataBytes: Maximum UTF-8 metadata bytes. + public init(json: JSONLimits = .balanced, schemaNodes: UInt64 = 10_000, + referenceEdges: UInt64 = 20_000, declarations: UInt64 = 100_000, + metadataBytes: UInt64 = 262_144) { + self.json = json; self.schemaNodes = schemaNodes; self.referenceEdges = referenceEdges + self.declarations = declarations; self.metadataBytes = metadataBytes + } + /// Finite default policy; logical byte capacities are not process RSS guarantees. + public static var balanced: Self { .init() } + /// Smaller finite local capacities. + public static var conservative: Self { .init(json: .conservative, schemaNodes: 2_000, referenceEdges: 4_000, declarations: 20_000, metadataBytes: 32_768) } + /// Larger finite local capacities; provider ceilings remain unchanged. + public static var offline: Self { .init(json: .offline, schemaNodes: 100_000, referenceEdges: 200_000, declarations: 1_000_000, metadataBytes: 1_048_576) } + package func validate() throws { + try json.validate() + for cap in [schemaNodes, referenceEdges, declarations, metadataBytes] { + guard cap < UInt64.max else { throw JSONSchemaProviderFailure.invalidConfiguration } + } + } +} + +/// Additional provider-operation accounting; these do not redefine the 49 native metrics. +public enum JSONSchemaProviderMetric: String, Sendable { + /// Number of caller-schema nodes inspected. + case schemaNodes + /// Number of caller-schema child/reference edges inspected. + case referenceEdges + /// Number of immediate keyword declarations inspected. + case declarations + /// Number of owned UTF-8 metadata bytes admitted. + case metadataBytes +} + +/// Fixed diagnostic codes; rejected payloads are never retained as messages. +public enum JSONSchemaProviderIssueCode: String, Sendable { + /// A strict Responses schema must declare an object root without a root union. + case objectRootRequired + /// An object must explicitly forbid additional properties. + case closedObjectRequired + /// All declared properties must be explicitly required; absence is not rewritten as null. + case allPropertiesRequired + /// The declaration lies outside the documented subset. + case unsupportedKeyword + /// The declaration or combination has not been qualified by this revision. + case unverifiedDeclaration + /// A format has not been qualified by this profile. + case unsupportedFormat + /// External reference export has no qualified bundling policy. + case externalReference + /// A reference/combinator cycle provides no qualified productive declaration. + case nonProductiveReference + /// A frozen provider ceiling or explicitly documented conservative local rule was exceeded. + case profileCapacity + /// A Boolean schema is outside the strict subset, except object closure declarations. + case booleanSchema +} + +/// One exact typed schema location and fixed explanation, with no rejected value payload. +public struct JSONSchemaProviderIssue: Sendable { + /// The bounded reason for incompatibility. + public let code: JSONSchemaProviderIssueCode + /// Property keys remain keys; combinator array positions remain indices. + public let path: [JSONLocationComponent] + /// Whether the declaration is unverified rather than known unsupported by this revision. + public let unverified: Bool + /// Fixed explanation independent of caller-controlled text. + public var explanation: String { + switch code { + case .objectRootRequired: "A strict Responses root must explicitly declare object type without a root union or reference." + case .closedObjectRequired: "Every object declaration must explicitly set additionalProperties to false." + case .allPropertiesRequired: "Every declared property must be required; omitted and nullable have different meanings." + case .unsupportedKeyword: "This keyword is outside the selected strict subset." + case .unverifiedDeclaration: "This declaration or combination is not qualified by the selected local revision." + case .unsupportedFormat: "This format is outside the qualified base-model format list." + case .externalReference: "External references cannot be exported without a qualified bundling policy." + case .nonProductiveReference: "A reference/combinator cycle has no qualified productive declaration." + case .profileCapacity: "A frozen ceiling or conservative UTF-8/declaration-depth rule was exceeded." + case .booleanSchema: "Boolean schemas are outside this strict subset." + } + } +} + +/// Completed local compatibility inspection, distinct from native validity and remote service acceptance. +public struct JSONSchemaProviderCompatibility: Sendable { + /// The frozen provider, API purpose, revision and capability category. + public let profile: JSONSchemaProviderProfile + /// Stored bounded issues; truncation never permits strict export. + public let issues: [JSONSchemaProviderIssue] + /// True when issue storage was exhausted; the check still completed incompatibly. + public let truncated: Bool + /// True only when no unsupported or unverified declaration was observed. + public let isCompatible: Bool + /// Number of inspected caller-schema nodes. + public let checkedSchemaNodes: UInt64 + /// Number of inspected child/reference edges. + public let checkedReferenceEdges: UInt64 + /// Number of inspected immediate declarations. + public let checkedDeclarations: UInt64 +} + +/// Operational/configuration failures and explicit strict-export rejection. +public enum JSONSchemaProviderFailure: Error, Sendable { + /// A finite local capacity is invalid. + case invalidConfiguration + /// A helper was called with a profile for another API purpose. + case profileMismatch + /// Endpoint metadata violates the frozen name/metadata policy. + case invalidMetadata + /// Native compilation completed, but strict export is incompatible or unverified. + case incompatible(JSONSchemaProviderCompatibility) + /// An owned provider capacity failed; no successful partial result is returned. + case limitExceeded(metric: JSONSchemaProviderMetric, limit: UInt64, attempted: UInt64?) +} diff --git a/Sources/JSONSchema/Providers/OpenAIJSONSchemaBridge.swift b/Sources/JSONSchema/Providers/OpenAIJSONSchemaBridge.swift new file mode 100644 index 0000000..90951c4 --- /dev/null +++ b/Sources/JSONSchema/Providers/OpenAIJSONSchemaBridge.swift @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON + +/// Offline strict Responses schema checks and exact request fragments. +/// +/// This facade never calls a service, selects a model, reads credentials or rewrites +/// declarations. A compatible local profile is not a remote acceptance guarantee. +/// Each compound operation owns one finite cumulative ledger through compilation, +/// checking, metadata, envelope construction and optional native UTF-8 writing. +public struct OpenAIJSONSchemaBridge: Sendable { + /// Frozen API purpose, revision and qualified capability category. + public let profile: JSONSchemaProviderProfile + /// Finite native and provider-specific local capacities. + public let limits: JSONSchemaProviderLimits + /// Creates an immutable facade; configuration is checked at operation entry. + /// - Parameters: + /// - profile: The explicit Responses endpoint purpose. + /// - limits: Finite capacities for the complete owned operation. + public init(profile: JSONSchemaProviderProfile, limits: JSONSchemaProviderLimits = .balanced) { + self.profile = profile; self.limits = limits + } + /// Completes native compilation and local compatibility inspection without modifying the schema. + /// - Parameters: + /// - schema: An admitted declaration; admission alone is not compilation. + /// - registry: Explicit offline resources. External-reference export remains incompatible. + /// - Returns: Compatibility false for unsupported/unverified declarations. + /// - Throws: Malformed schema, unresolved reference, resource/configuration failure or cancellation. + public func check(_ schema: JSONSchemaDocument, registry: JSONSchemaRegistry = .empty) throws -> JSONSchemaProviderCompatibility { + try checkWithReport(schema, registry: registry).result + } + /// Checks compatibility with cumulative compilation/inspection accounting. + /// - Parameters: + /// - schema: The exact admitted declaration to inspect. + /// - registry: Immutable explicitly supplied offline resources. + /// - Returns: Completed compatibility, including explicit issue truncation and charged work. + /// - Throws: Native compilation, finite resource, configuration or cancellation failures. + public func checkWithReport(_ schema: JSONSchemaDocument, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + try limits.validate() + var meter = try JSONWorkMeter(limits: limits.json, domain: .schemaCompilation) + let result = try check(schema.raw, retrievalURI: schema.retrievalURI, registry: registry, meter: &meter) + return .init(result: result, statistics: meter.statistics()) + } + /// Returns the object inserted directly at Responses `text.format`. + /// - Parameters: + /// - schema: Original exact object schema; incompatible declarations are never lowered. + /// - name: Nonempty ASCII letters/digits/underscore/dash, at most 64 bytes. + /// - description: Optional exact description under finite metadata limits. + /// - registry: Explicit offline resources; no external bundling is performed. + /// - Returns: Native JSON with object-valued schema and strict true. + /// - Throws: Profile/metadata errors, incompatible report, native error, resource failure or cancellation. + public func responseFormat(_ schema: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSON { + try responseFormatWithReport(schema, name: name, description: description, registry: registry).result + } + /// Returns one flat Responses function tool, not a Chat Completions wrapper. + /// - Parameters: + /// - parameters: Original exact parameter schema. + /// - name: Endpoint name admitted by the frozen ASCII/64-byte policy. + /// - description: Optional exact description under finite metadata limits. + /// - registry: Explicit offline resources, without automatic bundling. + /// - Returns: Native JSON with type function, object parameters and strict true. + /// - Throws: Profile/metadata errors, incompatible report, native error, resource failure or cancellation. + public func functionTool(_ parameters: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSON { + try functionToolWithReport(parameters, name: name, description: description, registry: registry).result + } + /// Prepares a strict text-format fragment with complete owned accounting. + /// - Parameters: + /// - schema: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete fragment and cumulative statistics. + /// - Throws: Strict rejection or operational/configuration failure; no successful partial report. + public func responseFormatWithReport(_ schema: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + try valueReport(schema, name: name, description: description, registry: registry, purpose: .openAIResponsesFormatV1) + } + /// Prepares a strict function tool with complete owned accounting. + /// - Parameters: + /// - parameters: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete tool and cumulative statistics. + /// - Throws: Strict rejection or operational/configuration failure; no successful partial report. + public func functionToolWithReport(_ parameters: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + try valueReport(parameters, name: name, description: description, registry: registry, purpose: .openAIResponsesFunctionV1) + } + /// Serializes the complete format fragment with the native preserved-number writer. + /// - Parameters: + /// - schema: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: UTF-8 bytes; this is not an HTTP request body wrapper. + /// - Throws: Strict rejection or operational/configuration failure. + public func responseFormatData(_ schema: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> Data { + try responseFormatDataWithReport(schema, name: name, description: description, registry: registry).result + } + /// Serializes the complete function tool through the native writer. + /// - Parameters: + /// - parameters: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete UTF-8 tool JSON bytes. + /// - Throws: Strict rejection or operational/configuration failure. + public func functionToolData(_ parameters: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> Data { + try functionToolDataWithReport(parameters, name: name, description: description, registry: registry).result + } + /// Serializes a text-format fragment and charges its owned String copy. + /// - Parameters: + /// - schema: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete strict UTF-8 JSON text. + /// - Throws: Strict rejection or operational/configuration failure. + public func responseFormatString(_ schema: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> String { + try responseFormatStringWithReport(schema, name: name, description: description, registry: registry).result + } + /// Serializes a function tool and charges its owned String copy. + /// - Parameters: + /// - parameters: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete strict UTF-8 JSON tool text. + /// - Throws: Strict rejection or operational/configuration failure. + public func functionToolString(_ parameters: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> String { + try functionToolStringWithReport(parameters, name: name, description: description, registry: registry).result + } + /// Produces bytes with one ledger through compilation, envelope, writing and Data copy. + /// - Parameters: + /// - schema: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: UTF-8 Data and cumulative accounting. + /// - Throws: Strict rejection or operational/configuration failure. + public func responseFormatDataWithReport(_ schema: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + let (bytes, statistics) = try bytesReport(schema, name: name, description: description, registry: registry, purpose: .openAIResponsesFormatV1) + return .init(result: Data(bytes), statistics: statistics) + } + /// Produces function bytes with one ledger through every owned phase. + /// - Parameters: + /// - parameters: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: UTF-8 Data and cumulative accounting. + /// - Throws: Strict rejection or operational/configuration failure. + public func functionToolDataWithReport(_ parameters: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + let (bytes, statistics) = try bytesReport(parameters, name: name, description: description, registry: registry, purpose: .openAIResponsesFunctionV1) + return .init(result: Data(bytes), statistics: statistics) + } + /// Produces text with one ledger, including the overlapping owned UTF-8 String copy. + /// - Parameters: + /// - schema: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete text and cumulative accounting. + /// - Throws: Strict rejection or operational/configuration failure. + public func responseFormatStringWithReport(_ schema: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + let (bytes, statistics) = try bytesReport(schema, name: name, description: description, registry: registry, purpose: .openAIResponsesFormatV1) + return .init(result: String(decoding: bytes, as: UTF8.self), statistics: statistics) + } + /// Produces function text with one ledger, including its overlapping owned copy. + /// - Parameters: + /// - parameters: Exact admitted source schema. + /// - name: Frozen endpoint-compatible name. + /// - description: Optional bounded metadata. + /// - registry: Explicit immutable offline resources. + /// - Returns: Complete text and cumulative accounting. + /// - Throws: Strict rejection or operational/configuration failure. + public func functionToolStringWithReport(_ parameters: JSONSchemaDocument, name: String, description: String? = nil, registry: JSONSchemaRegistry = .empty) throws -> JSONOperationReport { + let (bytes, statistics) = try bytesReport(parameters, name: name, description: description, registry: registry, purpose: .openAIResponsesFunctionV1) + return .init(result: String(decoding: bytes, as: UTF8.self), statistics: statistics) + } + private func valueReport(_ schema: JSONSchemaDocument, name: String, description: String?, registry: JSONSchemaRegistry, purpose: JSONSchemaProviderProfile) throws -> JSONOperationReport { + try limits.validate() + var meter = try JSONWorkMeter(limits: limits.json, domain: .schemaCompilation) + let value = try prepare(schema.raw, name: name, description: description, registry: registry, purpose: purpose, retrievalURI: schema.retrievalURI, meter: &meter) + return .init(result: value, statistics: meter.statistics()) + } + private func bytesReport(_ schema: JSONSchemaDocument, name: String, description: String?, registry: JSONSchemaRegistry, purpose: JSONSchemaProviderProfile) throws -> ([UInt8], JSONOperationStatistics) { + try limits.validate() + var meter = try JSONWorkMeter(limits: limits.json, domain: .schemaCompilation) + let value = try prepare(schema.raw, name: name, description: description, registry: registry, purpose: purpose, retrievalURI: schema.retrievalURI, meter: &meter) + let bytes = try JSONWriter(options: .init(limits: limits.json, numberStyle: .preserved)).write(value, meter: &meter) + for _ in bytes { try meter.work(); try meter.retain(1) } + return (bytes, meter.statistics()) + } + package func check(_ raw: JSON, retrievalURI: JSONSchemaURI? = nil, registry: JSONSchemaRegistry, meter: inout JSONWorkMeter) throws -> JSONSchemaProviderCompatibility { + try limits.validate(); try meter.checkpoint() + let compiled = try JSONSchemaCompiler(options: .init(limits: meter.limits)).compile(raw, retrievalURI: retrievalURI, registry: registry, meter: &meter) + var state = JSONSchemaProviderInspection(profile: profile, limits: limits) + return try state.inspect(compiled, meter: &meter) + } + /// Borrowing composition used by the CLI; no public helper resets its ledger. + package func prepare(_ raw: JSON, name: String, description: String?, registry: JSONSchemaRegistry, + purpose: JSONSchemaProviderProfile, retrievalURI: JSONSchemaURI? = nil, meter: inout JSONWorkMeter) throws -> JSON { + try limits.validate(); try meter.checkpoint() + guard profile == purpose else { throw JSONSchemaProviderFailure.profileMismatch } + var state = JSONSchemaProviderInspection(profile: profile, limits: limits) + try state.metadataText(name, meter: &meter) + var size: UInt64 = 0 + for byte in name.utf8 { + try meter.work(); size += 1 + guard (65...90).contains(byte) || (97...122).contains(byte) || (48...57).contains(byte) || byte == 95 || byte == 45 else { throw JSONSchemaProviderFailure.invalidMetadata } + } + guard size > 0 && size <= 64 else { throw JSONSchemaProviderFailure.invalidMetadata } + try JSONAdmission.text(name, key: false, meter: &meter) + if let description { try state.metadataText(description, meter: &meter); try JSONAdmission.text(description, key: false, meter: &meter) } + let compiled = try JSONSchemaCompiler(options: .init(limits: meter.limits)).compile(raw, retrievalURI: retrievalURI, registry: registry, meter: &meter) + let compatibility = try state.inspect(compiled, meter: &meter) + guard compatibility.isCompatible else { throw JSONSchemaProviderFailure.incompatible(compatibility) } + try meter.retain(JSONLogicalWeights.member * 4) + var members = [JSONMember("type", value: .string(purpose == .openAIResponsesFormatV1 ? "json_schema" : "function")), JSONMember("name", value: .string(name))] + if let description { try meter.retain(JSONLogicalWeights.member); members.append(.init("description", value: .string(description))) } + members.append(.init("strict", value: .boolean(true))) + members.append(.init(purpose == .openAIResponsesFormatV1 ? "schema" : "parameters", value: raw)) + let value = JSON.object(try JSONObject.indexed(admitted: members, meter: &meter)) + try JSONAdmission.value(value, meter: &meter) + return value + } +} diff --git a/Tests/Fixtures/Providers/boolean-schema.json b/Tests/Fixtures/Providers/boolean-schema.json new file mode 100644 index 0000000..27ba77d --- /dev/null +++ b/Tests/Fixtures/Providers/boolean-schema.json @@ -0,0 +1 @@ +true diff --git a/Tests/Fixtures/Providers/exact-number-schema.json b/Tests/Fixtures/Providers/exact-number-schema.json new file mode 100644 index 0000000..f1ab17f --- /dev/null +++ b/Tests/Fixtures/Providers/exact-number-schema.json @@ -0,0 +1 @@ +{"type":"object","properties":{"value":{"type":"number","minimum":0.1,"maximum":9007199254740993}},"required":["value"],"additionalProperties":false} diff --git a/Tests/Fixtures/Providers/optional-field-schema.json b/Tests/Fixtures/Providers/optional-field-schema.json new file mode 100644 index 0000000..b8cc8ba --- /dev/null +++ b/Tests/Fixtures/Providers/optional-field-schema.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "note": { + "type": "string" + } + }, + "required": [], + "additionalProperties": false +} diff --git a/Tests/Fixtures/Providers/portable-summary-schema.json b/Tests/Fixtures/Providers/portable-summary-schema.json new file mode 100644 index 0000000..a2e5902 --- /dev/null +++ b/Tests/Fixtures/Providers/portable-summary-schema.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "topic": { + "type": "string", + "enum": [ + "research", + "publishing", + "software" + ] + }, + "references": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "title", + "topic", + "references" + ], + "additionalProperties": false +} diff --git a/Tests/Fixtures/Providers/registry.json b/Tests/Fixtures/Providers/registry.json new file mode 100644 index 0000000..4409395 --- /dev/null +++ b/Tests/Fixtures/Providers/registry.json @@ -0,0 +1,32 @@ +{ + "schema_version": 1, + "fixtures": [ + { + "path": "fixtures/portable-summary-schema.json", + "sha256": "240e35a5e300003b58ddbda455e4d0af19aa7191add78fe0eb53270585c1f3a6", + "purpose": "Synthetic baseline for exact envelope and post-generation validation; native SDK/provider execution pending." + }, + { + "path": "fixtures/optional-field-schema.json", + "sha256": "18bfd2748a3500923d20dadf06197b30c2f31c55df4db10b140673d076a69c2d", + "purpose": "Must fail strict OpenAI profile without an automatic required/null rewrite." + }, + { + "path": "fixtures/boolean-schema.json", + "sha256": "a17fcf0a2f50e2d495e4f90ce263410edc183add6c62699a2facbccf60410f74", + "purpose": "Valid native schema kind; must fail the strict OpenAI object-root profile." + }, + { + "path": "fixtures/scalar-key-schema.json", + "sha256": "9bace13bae63652cd1c412e7fe5bdffe96eb5645dc3aa060d439ca378a5451ff", + "purpose": "Native keys remain distinct; Apple projection must preserve them through a qualified path or reject explicitly." + }, + { + "path": "fixtures/exact-number-schema.json", + "sha256": "f8078228342835e88212a5b7dd4480546d199565b2309ec868da09d0550b3f68", + "purpose": "Native wire lexemes remain exact; Double-backed Apple bound projection requires exact-policy rejection or a qualified exact alternative." + } + ], + "native_execution": "not_run", + "live_provider_execution": "not_run" +} diff --git a/Tests/Fixtures/Providers/scalar-key-schema.json b/Tests/Fixtures/Providers/scalar-key-schema.json new file mode 100644 index 0000000..3852d01 --- /dev/null +++ b/Tests/Fixtures/Providers/scalar-key-schema.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "é": { + "type": "string" + }, + "é": { + "type": "string" + } + }, + "required": [ + "é", + "é" + ], + "additionalProperties": false +} diff --git a/Tests/JSONCLITests/IntegrationTests.swift b/Tests/JSONCLITests/IntegrationTests.swift index 5bff88c..b55049f 100644 --- a/Tests/JSONCLITests/IntegrationTests.swift +++ b/Tests/JSONCLITests/IntegrationTests.swift @@ -85,17 +85,17 @@ private func integrationDirectory() throws -> URL { #expect(bounded.committedResults == 0) #expect(try Data(contentsOf: output) == Data("1\n2\n3\n4\n".utf8)) } -@Test func cliHelpVersionAndUnavailablePrerequisitesAreTruthful() throws { +@Test func cliHelpVersionAndProviderAvailabilityAreTruthful() throws { for argv in [["render", "--presentation", "bogus"], ["render", "--format", "text", "--theme", "dark"], ["query", "--input-framing", "bogus", "("]] { var context = try CLIJobContext() #expect(throws: CLIError.self) { _ = try CLIArguments(argv, context: &context) } #expect(context.used(.totalInputBytes) == 0) } #expect(try CLIHelp.text(for: "stream").contains("closing syntax")) - #expect(try !CLIHelp.text(for: "schema").contains("openai")) + #expect(try CLIHelp.text(for: "schema").contains("openai-format")) #expect(throws: CLIError.self) { _ = try CLIHelp.text(for: "server") } var context = try CLIJobContext() - let arguments = try CLIArguments(["schema", "provider", "openai-format", "not-opened.json", "--name", "sample"], context: &context) + let arguments = try CLIArguments(["schema", "provider", "apple-check", "not-opened.json", "--name", "sample"], context: &context) do { _ = try CLISchemaCommand.run(arguments, context: &context); Issue.record("Prerequisite is not integrated") } catch let error as CLIError { #expect(error.exit == 2 && error.code == "capabilityUnavailable") } #expect(context.used(.totalInputBytes) == 0) diff --git a/Tests/JSONCLITests/ProviderCommandTests.swift b/Tests/JSONCLITests/ProviderCommandTests.swift new file mode 100644 index 0000000..00a2d2b --- /dev/null +++ b/Tests/JSONCLITests/ProviderCommandTests.swift @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Testing +import JSON +import JSONSchema +@testable import JSONFilterRuntime +@testable import JSONCLIEngine + +private let providerSource = ##"{"type":"object","properties":{"value":{"type":"integer","minimum":9007199254740993}},"required":["value"],"additionalProperties":false}"## +private func providerDirectory() throws -> URL { + let url = FileManager.default.temporaryDirectory.appendingPathComponent("cli-provider-" + UUID().uuidString) + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) + return url +} +@Test func cliSchemaProviderFormatAndToolUsePublicNativeExactHelpers() throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), output = root.appendingPathComponent("fragment.json") + try Data(providerSource.utf8).write(to: input) + for command in ["openai-format", "openai-tool"] { + var context = try CLIJobContext() + let args = try CLIArguments(["schema", "provider", command, input.path, "--name", "exact", "--description", "a 🦊", "--output", output.path, "--overwrite"], context: &context) + #expect(try CLISchemaCommand.run(args, context: &context) == 0) + let data = try Data(contentsOf: output), value = try JSONParser().parse(data) + let field = command == "openai-format" ? "schema" : "parameters" + #expect(try value.objectValue?[JSONKey(field)] == JSONParser().parse(providerSource)) + #expect(String(decoding: data, as: UTF8.self).contains("9007199254740993")) + #expect(value.objectValue?[JSONKey("strict")] == .boolean(true)) + #expect(context.committedResults == 1) + } +} +@Test func cliSchemaProviderOptionalityIsCompletedIncompatibilityWithoutCandidate() throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), output = root.appendingPathComponent("result.json") + let source = ##"{"type":"object","properties":{"0":{"type":"integer"}},"additionalProperties":false}"## + try Data(source.utf8).write(to: input) + var context = try CLIJobContext() + let args = try CLIArguments(["schema", "provider", "openai-format", input.path, "--name", "x", "--output", output.path], context: &context) + #expect(try CLISchemaCommand.run(args, context: &context) == 1) + let result = try JSONParser().parse(Data(contentsOf: output)) + #expect(result.objectValue?[JSONKey("compatible")] == .boolean(false)) + #expect(result.objectValue?[JSONKey("schema")] == nil) + #expect(try JSONPointer(parsing: "/issues/0/code").resolve(in: result) == .string("allPropertiesRequired")) + #expect(try JSONPointer(parsing: "/issues/0/path/0/kind").resolve(in: result) == .string("key")) + #expect(try Data(contentsOf: input) == Data(source.utf8)) + #expect(context.committedResults == 1) +} +@Test func cliSchemaProviderMetadataAndUnqualifiedAppleRejectBeforeInputPull() throws { + let vectors = [ + ["schema", "provider", "openai-format", "not-opened.json"], + ["schema", "provider", "openai-tool", "not-opened.json", "--name", "bad name"], + ["schema", "provider", "openai-format", "not-opened.json", "--name", String(repeating: "x", count: 65)], + ["schema", "provider", "apple-check", "not-opened.json"], + ["schema", "provider", "unknown", "not-opened.json"] + ] + for vector in vectors { + var context = try CLIJobContext() + let args = try CLIArguments(vector, context: &context) + do { _ = try CLISchemaCommand.run(args, context: &context); Issue.record("Expected early usage error") } + catch { #expect(CLIDiagnostics.classify(error).exit == 2) } + #expect(context.used(.totalInputBytes) == 0) + } +} +@Test func cliSchemaProviderMalformedSchemaAndUnknownReferenceStayOperational() throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), output = root.appendingPathComponent("result.json") + for source in [##"{"type":"object","required":1}"##, ##"{"type":"object","properties":{"x":{"$ref":"https://example.invalid/missing"}},"required":["x"],"additionalProperties":false}"##] { + try Data(source.utf8).write(to: input) + var context = try CLIJobContext() + let args = try CLIArguments(["schema", "provider", "openai-format", input.path, "--name", "x", "--output", output.path], context: &context) + do { _ = try CLISchemaCommand.run(args, context: &context); Issue.record("Expected operational schema error") } + catch { #expect(CLIDiagnostics.classify(error).exit == 5) } + #expect(!FileManager.default.fileExists(atPath: output.path)) + } +} +@Test func cliSchemaProviderCumulativeParseCompileEnvelopeAndWriteCannotResetWork() throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), output = root.appendingPathComponent("result.json") + try Data(providerSource.utf8).write(to: input) + let vector = ["schema", "provider", "openai-format", input.path, "--name", "x", "--output", output.path, "--overwrite"] + var measured = try CLIJobContext() + let args = try CLIArguments(vector, context: &measured) + #expect(try CLISchemaCommand.run(args, context: &measured) == 0) + let original = try Data(contentsOf: output), work = measured.used(.totalWorkUnits) + var limits = CLILimits(); limits[.totalWorkUnits] = work - 1 + var bounded = try CLIJobContext(limits: limits) + let retry = try CLIArguments(vector, context: &bounded) + do { _ = try CLISchemaCommand.run(retry, context: &bounded); Issue.record("Expected cumulative quota error") } + catch { #expect(CLIDiagnostics.classify(error).exit == 6) } + #expect(try bounded.committedResults == 0 && Data(contentsOf: output) == original) +} +@Test func cliSchemaProviderCancellationInsideCompilationPreservesAtomicOutput() async throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), output = root.appendingPathComponent("result.json") + try Data(providerSource.utf8).write(to: input); try Data("original".utf8).write(to: output) + let task = Task { + var context = try CLIJobContext() + let args = try CLIArguments(["schema", "provider", "openai-format", input.path, "--name", "x", "--output", output.path, "--overwrite"], context: &context) + context.meter.checkpointObserver = { work in if work > 1_000 { withUnsafeCurrentTask { $0?.cancel() } } } + do { _ = try CLISchemaCommand.run(args, context: &context); Issue.record("Expected cancellation") } + catch { #expect(CLIDiagnostics.classify(error).exit == 130) } + let work = context.used(.totalWorkUnits) + #expect(throws: CancellationError.self) { try context.checkpoint() } + #expect(context.used(.totalWorkUnits) == work && context.committedResults == 0) + } + try await task.value + #expect(try Data(contentsOf: output) == Data("original".utf8)) +} +@Test func cliSchemaProviderExplicitExternalResourceIsResolvedButNeverBundled() throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), external = root.appendingPathComponent("external.json"), output = root.appendingPathComponent("result.json") + try Data(##"{"type":"object","properties":{"value":{"$ref":"https://example.invalid/value"}},"required":["value"],"additionalProperties":false}"##.utf8).write(to: input) + try Data(##"{"type":"integer"}"##.utf8).write(to: external) + var context = try CLIJobContext() + let args = try CLIArguments(["schema", "provider", "openai-tool", input.path, "--name", "x", "--resource", "https://example.invalid/value=" + external.path, "--output", output.path], context: &context) + #expect(try CLISchemaCommand.run(args, context: &context) == 1) + let result = try JSONParser().parse(Data(contentsOf: output)) + #expect(try JSONPointer(parsing: "/issues/0/code").resolve(in: result) == .string("externalReference")) + #expect(result.objectValue?[JSONKey("parameters")] == nil) +} +@Test func cliSchemaProviderMalformedTailCannotPublishAValidPrefix() throws { + let root = try providerDirectory(); defer { try? FileManager.default.removeItem(at: root) } + let input = root.appendingPathComponent("schema.json"), output = root.appendingPathComponent("result.json") + try Data((providerSource + " false").utf8).write(to: input) + try Data("original".utf8).write(to: output) + var context = try CLIJobContext() + let args = try CLIArguments(["schema", "provider", "openai-format", input.path, "--name", "x", "--output", output.path, "--overwrite"], context: &context) + do { _ = try CLISchemaCommand.run(args, context: &context); Issue.record("Expected strict trailing syntax failure") } + catch { #expect(CLIDiagnostics.classify(error).exit == 5) } + #expect(try Data(contentsOf: output) == Data("original".utf8)) + #expect(context.committedResults == 0) +} diff --git a/Tests/JSONSchemaTests/ProviderBridgeTests.swift b/Tests/JSONSchemaTests/ProviderBridgeTests.swift new file mode 100644 index 0000000..ccd6ea9 --- /dev/null +++ b/Tests/JSONSchemaTests/ProviderBridgeTests.swift @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONSchema +import Testing + +private func providerSchema(_ source: String = ##"{"type":"object","properties":{"value":{"type":"integer"}},"required":["value"],"additionalProperties":false}"##) throws -> JSONSchemaDocument { + try JSONSchemaDocument(JSONParser().parse(source)) +} +private let formatBridge = OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1) + +@Test func providerResponsesEnvelopesAndAllNativeTextDataReportsAgree() throws { + let schema = try providerSchema(), tool = OpenAIJSONSchemaBridge(profile: .openAIResponsesFunctionV1) + let format = try formatBridge.responseFormatWithReport(schema, name: "summary", description: "Exact 🦊") + let function = try tool.functionToolWithReport(schema, name: "summary", description: "Exact 🦊") + #expect(try JSONPointer(parsing: "/schema").resolve(in: format.result) == schema.raw) + #expect(try JSONPointer(parsing: "/parameters").resolve(in: function.result) == schema.raw) + #expect(format.result.objectValue?[JSONKey("type")] == .string("json_schema")) + #expect(function.result.objectValue?[JSONKey("type")] == .string("function")) + #expect(function.result.objectValue?[JSONKey("function")] == nil) + #expect(format.result.objectValue?[JSONKey("text")] == nil) + #expect(format.result.objectValue?[JSONKey("strict")] == .boolean(true)) + for isTool in [false, true] { + let bridge = isTool ? tool : formatBridge + let value = try isTool ? bridge.functionTool(schema, name: "summary", description: "Exact 🦊") : bridge.responseFormat(schema, name: "summary", description: "Exact 🦊") + let data = try isTool ? bridge.functionToolData(schema, name: "summary", description: "Exact 🦊") : bridge.responseFormatData(schema, name: "summary", description: "Exact 🦊") + let text = try isTool ? bridge.functionToolString(schema, name: "summary", description: "Exact 🦊") : bridge.responseFormatString(schema, name: "summary", description: "Exact 🦊") + let dr = try isTool ? bridge.functionToolDataWithReport(schema, name: "summary", description: "Exact 🦊") : bridge.responseFormatDataWithReport(schema, name: "summary", description: "Exact 🦊") + let sr = try isTool ? bridge.functionToolStringWithReport(schema, name: "summary", description: "Exact 🦊") : bridge.responseFormatStringWithReport(schema, name: "summary", description: "Exact 🦊") + #expect(try JSONParser().parse(data) == value) + #expect(try JSONParser().parse(text) == value) + #expect(dr.result == data && sr.result == text) + #expect(dr.statistics[.workUnits] > format.statistics[.workUnits]) + #expect(dr.statistics[.outputBytes] == UInt64(data.count)) + } +} +@Test func providerOptionalityNullabilityClosureAndRootRemainDistinct() throws { + let optional = try providerSchema(##"{"type":"object","properties":{"value":{"type":"integer"}},"additionalProperties":false}"##) + let raw = optional.raw + let check = try formatBridge.checkWithReport(optional) + #expect(!check.result.isCompatible && check.result.issues.contains { $0.code == .allPropertiesRequired }) + #expect(throws: JSONSchemaProviderFailure.self) { _ = try formatBridge.responseFormat(optional, name: "summary") } + #expect(optional.raw == raw) + let nullable = try providerSchema(##"{"type":"object","properties":{"value":{"type":["integer","null"]}},"required":["value"],"additionalProperties":false}"##) + #expect(try formatBridge.check(nullable).isCompatible) + let open = try providerSchema(##"{"type":"object","properties":{},"required":[]}"##) + #expect(try formatBridge.check(open).issues.contains { $0.code == .closedObjectRequired }) + #expect(try !formatBridge.check(providerSchema("true")).isCompatible) + #expect(try !formatBridge.check(providerSchema(##"{"type":"string"}"##)).isCompatible) + #expect(check.statistics[.compileWorkUnits] > 0) +} +@Test func providerUnknownDeclarationsAndTypedNumericKeyUnionPathsAreHonest() throws { + let schema = try providerSchema(##"{"type":"object","properties":{"0":{"anyOf":[{"type":"string"},{"type":"integer","x-local":1}]}},"required":["0"],"additionalProperties":false}"##) + let report = try formatBridge.check(schema) + let issue = try #require(report.issues.first { $0.code == .unverifiedDeclaration }) + #expect(issue.unverified) + #expect(issue.path == [.key(JSONKey("properties")), .key(JSONKey("0")), .key(JSONKey("anyOf")), .index(1), .key(JSONKey("x-local"))]) + let invalid = try providerSchema(##"{"type":"object","properties":{},"required":[],"additionalProperties":false,"allOf":[{"type":"object"}]}"##) + #expect(try formatBridge.check(invalid).issues.contains { $0.code == .unsupportedKeyword }) +} +@Test func providerExactNumbersScalarKeysAndDeclarationOrderSurviveOutput() throws { + let source = ##"{"type":"object","properties":{"é":{"type":"integer","minimum":9007199254740993},"é":{"type":"number","maximum":1.2300e+2}},"required":["é","é"],"additionalProperties":false}"## + let schema = try providerSchema(source) + let encoded = try formatBridge.responseFormatString(schema, name: "exact") + #expect(encoded.contains("9007199254740993") && encoded.contains("1.2300e+2")) + let roundTrip = try JSONPointer(parsing: "/schema").resolve(in: JSONParser().parse(encoded)) + #expect(roundTrip == schema.raw) + let original = try #require(schema.raw.objectValue?[JSONKey("properties")]?.objectValue) + let retained = try #require(roundTrip.objectValue?[JSONKey("properties")]?.objectValue) + #expect(Array(original).map(\.key) == Array(retained).map(\.key)) + #expect(retained.count == 2) +} +@Test func providerLocalDefinitionsAndProductiveRecursionAreNotUnrolled() throws { + let recursive = try providerSchema(##"{"type":"object","properties":{"next":{"anyOf":[{"$ref":"#"},{"type":"null"}]}},"required":["next"],"additionalProperties":false}"##) + #expect(try formatBridge.check(recursive).isCompatible) + let definitions = try providerSchema(##"{"type":"object","properties":{"value":{"$ref":"#/$defs/value"}},"required":["value"],"additionalProperties":false,"$defs":{"value":{"type":"integer"}}}"##) + #expect(try formatBridge.check(definitions).isCompatible) + let cycle = try providerSchema(##"{"type":"object","properties":{"value":{"$ref":"#/$defs/value"}},"required":["value"],"additionalProperties":false,"$defs":{"value":{"$ref":"#/$defs/value"}}}"##) + #expect(try formatBridge.check(cycle).issues.contains { $0.code == .nonProductiveReference }) + let epsilon = try providerSchema(##"{"type":"object","properties":{"value":{"$ref":"#/$defs/value"}},"required":["value"],"additionalProperties":false,"$defs":{"value":{"anyOf":[{"$ref":"#/$defs/value"}]}}}"##) + #expect(try formatBridge.check(epsilon).issues.contains { $0.code == .nonProductiveReference }) + + var chainDefinitions: [String] = [] + for index in 0..<12 { + let next = index == 11 ? "{\"type\":\"null\"}" : "{\"$ref\":\"#/$defs/n" + String(index + 1) + "\"}" + chainDefinitions.append("\"n" + String(index) + "\":{\"type\":\"object\",\"properties\":{\"next\":" + next + "},\"required\":[\"next\"],\"additionalProperties\":false}") + } + let deep = try providerSchema("{\"type\":\"object\",\"properties\":{\"next\":{\"$ref\":\"#/$defs/n0\"}},\"required\":[\"next\"],\"additionalProperties\":false,\"$defs\":{" + chainDefinitions.joined(separator: ",") + "}}") + #expect(try formatBridge.check(deep).issues.contains { $0.code == .profileCapacity }) + +} +@Test func providerExternalRegistryResolutionIsOperationalBeforeExportCompatibility() throws { + let schema = try providerSchema(##"{"type":"object","properties":{"value":{"$ref":"https://example.invalid/value"}},"required":["value"],"additionalProperties":false}"##) + #expect(throws: JSONSchemaCompilationFailure.self) { _ = try formatBridge.check(schema) } + let uri = try JSONSchemaURI("https://example.invalid/value") + let external = try JSONSchemaDocument(JSONParser().parse(##"{"type":"integer"}"##), retrievalURI: uri) + let registry = try JSONSchemaRegistry(documents: [external]) + let result = try formatBridge.check(schema, registry: registry) + #expect(!result.isCompatible && result.issues.contains { $0.code == .externalReference }) + let relativeRaw = try JSONParser().parse(##"{"type":"object","properties":{"value":{"$ref":"value"}},"required":["value"],"additionalProperties":false}"##) + let relative = try JSONSchemaDocument(relativeRaw, retrievalURI: JSONSchemaURI("https://example.invalid/root")) + #expect(try formatBridge.check(relative, registry: registry).issues.contains { $0.code == .externalReference }) + do { _ = try formatBridge.responseFormat(relative, name: "x", registry: registry); Issue.record("Resolved external references are not exportable") } + catch JSONSchemaProviderFailure.incompatible(let report) { #expect(report.issues.contains { $0.code == .externalReference }) } + +} +@Test func providerFrozenCeilingsAndConservativeUtf8RulesHaveRealBoundaries() throws { + let enumItems = (0...1_000).map(String.init).joined(separator: ",") + let schema = try providerSchema("{\"type\":\"object\",\"properties\":{\"value\":{\"type\":\"integer\",\"enum\":[" + enumItems + "]}},\"required\":[\"value\"],\"additionalProperties\":false}") + #expect(try formatBridge.check(schema).issues.contains { $0.code == .profileCapacity }) + let first = String(repeating: "🦊", count: 15_000) + func namedSchema(_ second: String) throws -> JSONSchemaDocument { + try providerSchema("{\"type\":\"object\",\"properties\":{\"" + first + "\":{\"type\":\"string\"},\"" + second + "\":{\"type\":\"string\"}},\"required\":[\"" + first + "\",\"" + second + "\"],\"additionalProperties\":false}") + } + var textLimits = JSONSchemaProviderLimits.offline + textLimits.json[.pointerBytes] = 1_048_576 + let utf8 = OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: textLimits) + #expect(try utf8.check(namedSchema(String(repeating: "a", count: 60_000))).isCompatible) + #expect(try utf8.check(namedSchema(String(repeating: "a", count: 60_001))).issues.contains { $0.code == .profileCapacity }) + let format = try providerSchema(##"{"type":"object","properties":{"value":{"type":"string","format":"uri"}},"required":["value"],"additionalProperties":false}"##) + #expect(try formatBridge.check(format).issues.contains { $0.code == .unsupportedFormat }) + let unsupportedLength = try providerSchema(##"{"type":"object","properties":{"value":{"type":"string","minLength":1}},"required":["value"],"additionalProperties":false}"##) + #expect(try formatBridge.check(unsupportedLength).issues.contains { $0.unverified }) +} +@Test func providerFiniteCapacitiesTruncationAndInvalidConfigurationsRemainErrors() throws { + let schema = try providerSchema() + for metric in [JSONSchemaProviderMetric.schemaNodes, .referenceEdges, .declarations, .metadataBytes] { + var limits = JSONSchemaProviderLimits() + switch metric { case .schemaNodes: limits.schemaNodes = 0; case .referenceEdges: limits.referenceEdges = 0; case .declarations: limits.declarations = 0; case .metadataBytes: limits.metadataBytes = 0 } + #expect(throws: JSONSchemaProviderFailure.self) { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits).responseFormat(schema, name: "x") } + } + let measured = try formatBridge.checkWithReport(schema) + for metric in [JSONSchemaProviderMetric.schemaNodes, .referenceEdges, .declarations] { + var limits = JSONSchemaProviderLimits() + let used: UInt64 + switch metric { + case .schemaNodes: used = measured.result.checkedSchemaNodes; limits.schemaNodes = used + case .referenceEdges: used = measured.result.checkedReferenceEdges; limits.referenceEdges = used + case .declarations: used = measured.result.checkedDeclarations; limits.declarations = used + case .metadataBytes: used = 0 + } + #expect(used > 0) + #expect(try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits).check(schema).isCompatible) + switch metric { case .schemaNodes: limits.schemaNodes -= 1; case .referenceEdges: limits.referenceEdges -= 1; case .declarations: limits.declarations -= 1; case .metadataBytes: break } + #expect(throws: JSONSchemaProviderFailure.self) { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits).check(schema) } + } + var wide = JSONSchemaProviderLimits(); wide.schemaNodes = UInt64(1) << 40; wide.referenceEdges = UInt64(1) << 40; wide.declarations = UInt64(1) << 40; wide.metadataBytes = UInt64(1) << 40 + #expect(try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: wide).check(schema).isCompatible) + var invalid = JSONSchemaProviderLimits(); invalid.schemaNodes = UInt64.max + #expect(throws: JSONSchemaProviderFailure.self) { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: invalid).check(schema) } + var limits = JSONSchemaProviderLimits(); limits.json[.issues] = 0 + let bridge = OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits) + let unsupported = try providerSchema(##"{"type":"object","properties":{"value":{"type":"integer"}},"additionalProperties":true}"##) + let result = try bridge.check(unsupported) + #expect(!result.isCompatible && result.issues.isEmpty && result.truncated) + #expect(throws: JSONSchemaProviderFailure.self) { _ = try bridge.responseFormat(unsupported, name: "x") } +} +@Test func providerCompoundWorkAndMetadataBoundariesAreInclusiveWithoutHelperResets() throws { + let schema = try providerSchema(), name = String(repeating: "n", count: 64) + #expect(try formatBridge.responseFormat(schema, name: name).objectValue != nil) + for bad in ["", name + "n", "with space", "é"] { + #expect(throws: JSONSchemaProviderFailure.self) { _ = try formatBridge.responseFormat(schema, name: bad) } + } + #expect(throws: JSONSchemaProviderFailure.self) { _ = try formatBridge.functionTool(schema, name: "x") } + let measured = try formatBridge.responseFormatDataWithReport(schema, name: "x") + var limits = JSONSchemaProviderLimits(); limits.json[.workUnits] = measured.statistics[.workUnits] + #expect(try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits).responseFormatData(schema, name: "x") == measured.result) + limits.json[.workUnits] -= 1 + #expect(throws: JSONFailure.self) { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits).responseFormatData(schema, name: "x") } + for metric in [JSONResourceMetric.outputBytes, .retainedBytes] { + var capacity = JSONSchemaProviderLimits() + capacity.json[metric] = metric == .outputBytes ? measured.statistics[.outputBytes] : measured.statistics.peakLogicalRetainedBytes + #expect(try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: capacity).responseFormatData(schema, name: "x") == measured.result) + capacity.json[metric] -= 1 + #expect(throws: JSONFailure.self) { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: capacity).responseFormatData(schema, name: "x") } + } + var metadata = JSONSchemaProviderLimits(); metadata.metadataBytes = 1 + #expect(try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: metadata).responseFormat(schema, name: "x").objectValue != nil) + metadata.metadataBytes = 0 + #expect(throws: JSONSchemaProviderFailure.self) { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: metadata).responseFormat(schema, name: "x") } +} +@Test func providerCancellationInsideActualOwnedCheckStopsFurtherWork() async throws { + let schema = try providerSchema() + let task = Task { + var meter = try JSONWorkMeter(limits: .balanced, domain: .schemaCompilation) + meter.checkpointObserver = { work in if work > 1_000 { withUnsafeCurrentTask { $0?.cancel() } } } + do { _ = try formatBridge.check(schema.raw, registry: .empty, meter: &meter); Issue.record("Expected in-loop cancellation") } + catch { #expect(error is CancellationError) } + let work = meter.used(.workUnits) + #expect(throws: CancellationError.self) { try meter.work() } + #expect(meter.used(.workUnits) == work) + } + try await task.value +} +@Test func providerCaughtCapacityFailureCannotRestartBorrowedLedger() throws { + let schema = try providerSchema() + var limits = JSONSchemaProviderLimits(); limits.schemaNodes = 0 + var meter = try JSONWorkMeter(limits: .balanced, domain: .schemaCompilation) + let bounded = OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: limits) + #expect(throws: JSONSchemaProviderFailure.self) { _ = try bounded.check(schema.raw, registry: .empty, meter: &meter) } + let work = meter.used(.workUnits) + #expect(throws: JSONSchemaProviderFailure.self) { _ = try formatBridge.prepare(schema.raw, name: "x", description: nil, registry: .empty, purpose: .openAIResponsesFormatV1, meter: &meter) } + #expect(meter.used(.workUnits) == work) +} +@Test func providerMalformedSchemasAndNativeRegexFailuresAreNotCompatibilityFalse() throws { + for raw in [##"{"type":"object","minimum":"wrong"}"##, ##"{"type":"object","properties":{"value":{"type":"string","pattern":"["}},"required":["value"],"additionalProperties":false}"##] { + let schema = try providerSchema(raw) + #expect(throws: JSONSchemaCompilationFailure.self) { _ = try formatBridge.check(schema) } + } +} +@Test func sharedProviderBridgeHasIndependentConcurrentInvocations() async throws { + let schema = try providerSchema() + try await withThrowingTaskGroup(of: JSON.self) { group in + for _ in 0..<8 { group.addTask { try formatBridge.responseFormat(schema, name: "parallel") } } + var count = 0 + for try await value in group { + #expect(value.objectValue?[JSONKey("schema")] == schema.raw); count += 1 + } + #expect(count == 8) + } +} diff --git a/Tools/CLIQualification/Package.swift b/Tools/CLIQualification/Package.swift index 4dc3f43..07d3f4a 100644 --- a/Tools/CLIQualification/Package.swift +++ b/Tools/CLIQualification/Package.swift @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import PackageDescription let package = Package(name: "CLIQualification", platforms: [.macOS("27.0"), .iOS("27.0")], - products: [.executable(name: "CLIExactCompare", targets: ["CLIExactCompare"])], + products: [.executable(name: "CLIExactCompare", targets: ["CLIExactCompare"]), .executable(name: "ProviderConsumer", targets: ["ProviderConsumer"])], dependencies: [.package(name: "Swift-JSON", path: "../..")], - targets: [.target(name: "CLIMobileConsumer", dependencies: [ + targets: [.executableTarget(name: "ProviderConsumer", dependencies: [.product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON")]), .target(name: "CLIMobileConsumer", dependencies: [ .product(name: "JSON", package: "Swift-JSON"), .product(name: "JSONSchema", package: "Swift-JSON"), .product(name: "JSONStreaming", package: "Swift-JSON"), .product(name: "JSONPatch", package: "Swift-JSON"), .product(name: "JSONHTML", package: "Swift-JSON")]), .executableTarget(name: "CLIExactCompare", dependencies: [ diff --git a/Tools/CLIQualification/Sources/ProviderConsumer/Main.swift b/Tools/CLIQualification/Sources/ProviderConsumer/Main.swift new file mode 100644 index 0000000..859a571 --- /dev/null +++ b/Tools/CLIQualification/Sources/ProviderConsumer/Main.swift @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import JSON +import JSONSchema + +@main struct ProviderConsumer { + static func main() throws { + let parser = JSONParser() + let raw = try parser.parse(##"{"type":"object","properties":{"value":{"type":"number","minimum":9007199254740993,"maximum":1.2300e+20},"é":{"type":"string"},"e\u0301":{"type":"string"}},"required":["value","é","e\u0301"],"additionalProperties":false}"##) + let schema = try JSONSchemaDocument(raw) + let format = OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1) + let tool = OpenAIJSONSchemaBridge(profile: .openAIResponsesFunctionV1) + var count = 0 + func check(_ good: Bool) { precondition(good); count += 1 } + let inspection = try format.checkWithReport(schema) + check(inspection.result.isCompatible && inspection.statistics[.compileWorkUnits] > 0) + let value = try format.responseFormatWithReport(schema, name: "summary", description: "Exact local fragment") + check(value.result.objectValue?[JSONKey("schema")] == raw && value.result.objectValue?[JSONKey("strict")] == .boolean(true)) + let function = try tool.functionTool(schema, name: "summarize") + check(function.objectValue?[JSONKey("parameters")] == raw && function.objectValue?[JSONKey("type")] == .string("function")) + let data = try format.responseFormatDataWithReport(schema, name: "summary") + let string = try format.responseFormatStringWithReport(schema, name: "summary") + check(try parser.parse(data.result) == parser.parse(string.result)) + check(string.result.contains("9007199254740993") && string.result.contains("1.2300e+20")) + let keys = try JSONPointer(parsing: "/schema/properties").resolve(in: parser.parse(data.result)).objectValue + check(keys?.count == 3 && keys?[JSONKey("é")] != nil && keys?[JSONKey("e\u{0301}")] != nil) + let optional = try JSONSchemaDocument(parser.parse(##"{"type":"object","properties":{"value":{"type":"string"}},"additionalProperties":false}"##)) + check(try !format.check(optional).isCompatible) + do { _ = try format.responseFormat(optional, name: "x"); preconditionFailure("No silent optionality lowering") } + catch JSONSchemaProviderFailure.incompatible(let report) { check(!report.isCompatible && report.issues.contains { $0.code == .allPropertiesRequired }) } + let recursive = try JSONSchemaDocument(parser.parse(##"{"type":"object","properties":{"next":{"anyOf":[{"$ref":"#"},{"type":"null"}]}},"required":["next"],"additionalProperties":false}"##)) + check(try format.check(recursive).isCompatible) + let compiled = try JSONSchemaCompiler().compile(schema) + let validator = JSONSchemaValidator() + check(try validator.validate(parser.parse(##"{"value":9007199254740993,"é":"a","e\u0301":"b"}"##), against: compiled).isValid) + check(try !validator.validate(parser.parse(##"{"value":1,"é":"a","e\u0301":"b"}"##), against: compiled).isValid) + var bounded = JSONSchemaProviderLimits(); bounded.json[.workUnits] = data.statistics[.workUnits] - 1 + do { _ = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: bounded).responseFormatData(schema, name: "summary"); preconditionFailure("Expected shared work failure") } + catch let error as JSONFailure { check(error.category == .resource) } + var issues = JSONSchemaProviderLimits(); issues.json[.issues] = 0 + let truncated = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: issues).check(optional) + check(!truncated.isCompatible && truncated.truncated && truncated.issues.isEmpty) + var wide = JSONSchemaProviderLimits(); wide.schemaNodes = UInt64(1) << 40; wide.referenceEdges = UInt64(1) << 40; wide.declarations = UInt64(1) << 40; wide.metadataBytes = UInt64(1) << 40 + let wideAccepted = try OpenAIJSONSchemaBridge(profile: .openAIResponsesFormatV1, limits: wide).check(schema).isCompatible + check(wideAccepted && format.profile.revision == "2026-10-15.v1" && format.profile.capabilityCategory == "documented-base-model-subset") + if CommandLine.arguments.dropFirst().contains("--emit") { + print(try format.responseFormatString(schema, name: "summary", description: "Exact local fragment")) + print(try tool.functionToolString(schema, name: "summarize")) + } + print("PROVIDER_CONSUMER passed cases=\(count)") + precondition(count == 14) + } +} From 1fd32d19b4e16891073cb9fdba7ecdcc7660bf88 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 05:32:45 +0200 Subject: [PATCH 53/59] docs: close physical JSONHTML preview qualification --- .../Architecture/Release1.3/Progress.json | 26 ++++++++-- .../StructuredPreviews/APIAndPolicy.md | 4 ++ .../StructuredPreviews/Qualification.md | 48 +++++++++++++++++-- .../Documentation.docc/StructuredPreviews.md | 3 +- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index fcc044c..1face47 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -311,7 +311,7 @@ "briefing_revision": "0.2", "release_assignment": null, "branch": "codex/jsonhtml-structured-previews", - "status": "Implementation reviewable; complete L5/P5 qualification remains open", + "status": "L5/P5 scoped qualification complete; dependent draft PR remains unmerged", "native_library_head": "76e4b262c4247ca3784b62b97bc21321e43d4044", "source_preserving_cli_base": "aded3c9930f580bd4fb902bba002ab75ed199a7b", "native_functions": 372, @@ -324,10 +324,7 @@ "requirements": 110, "scenarios": 88, "calibration_configurations": 96, - "remaining": [ - "Native physical touch/portrait-landscape browser interaction", - "Native media touch interaction" - ], + "remaining": [], "qualification_contract": "StructuredPreviews/Qualification.md", "history_preserved": true, "publication_performed": false, @@ -361,6 +358,25 @@ "additional_search_cases": 9, "history_preserved": true, "library_changes": false + }, + "qualification_touch_follow_up": { + "tested_source_head": "e0d5f0c26519c3eaa5faaa62f4104cc10ad5c419", + "hosted_native_run": 38673549572, + "hosted_linux_run": 38673549500, + "hosted_checks_passed": 7, + "requirements_passed": 110, + "scenarios_passed": 88, + "physical_iphone_consumer_cases": 73, + "physical_iphone_consumer_artifacts": 8, + "physical_touch": "Maintainer confirmed requested tab/disclosure/search, media, rotation and script-off access after fixture findings; bounded native observation supplements the manual result", + "fixture_findings": [ + "Original 4x4 admission samples decode but are too small for recognizable visual QA", + "Inline expressions crossing the finite prefix or destination ceiling remain literal; compact 415-scalar image qualified" + ], + "library_changes": false, + "limits_widened": false, + "history_preserved": true, + "merge_tag_publication_performed": false } } } diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md b/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md index 297a7b3..1df537a 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md @@ -135,3 +135,7 @@ The approved responsive transitions are 1200px (outer spacing/title), 920px full-row search). At visual depth bucket 3, nested record borders and padding stop accumulating. The complete 120-container alternating fixture is checked at 320px; static source links share the themed accessible link color. + +Complete values expand the full original string. Derived inline previews retain their +finite prefix and destination ceilings in both value profiles; complete output does +not promise that every expression in a long string is interpreted as rich content. diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md b/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md index 1fdf61c..9f3190d 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/Qualification.md @@ -37,8 +37,8 @@ time is not an idle-host comparison or throughput guarantee. Logical retained by process RSS, caller work and owned quotas remain distinct. The inherited 1.075s versus 0.96849s stream median (+11.0%) retains its existing reviewed disposition and 10% trigger. -L5/P5 remains an explicit review boundary while native touch/portrait-landscape and -media touch observations remain open. +The native touch/portrait-landscape and media follow-up is recorded below; earlier +open-gate observations remain in the private evidence history. The supplied return materializes 110 requirement rows, 88 scenario rows, 48 requested browser contexts, actual source archive, per-file manifests, command receipts and failed-probe history. It does not turn related test counts into unexecuted scenario @@ -55,8 +55,8 @@ receipt commits. The source archive's actual digest is recorded outside its own to avoid a circular claim. No merge, tag, release, Site/social edit or outreach is authorized by this increment. -The focused draft PR is dependent on the current CLI integration stack. Maintainers -must review the materialized remaining actions before claiming complete qualification. +The focused draft PR is dependent on the current CLI integration stack. Qualification +evidence does not itself authorize merging or publication. ## Qualification follow-up @@ -92,3 +92,43 @@ ten pages with every tail marker exactly once. Nine additional browser cases cov and cancellation of stale searches. TextEdit reception of the pinned exports is recorded separately from byte/content-type/selection assertions. No library feature, quota, platform, dependency or product policy changed in this follow-up. + +## Physical touch and media closure — 2026-10-15 + +All seven hosted checks at `e0d5f0c26519c3eaa5faaa62f4104cc10ad5c419` passed: +Native run `38673549572` and Linux run `38673549500`. Downloaded command logs and +all 991 source-input hashes were verified against that checkout. Native debug/release +and ASAN remain 372 functions/325 parameter cases; each Linux architecture remains +356/325, with TSan 11 and the existing strict-memory/cap/conformance checks retained. + +The actual iPhone consumer rerun passed 73 cases/eight artifacts. The maintainer then +performed finger-touch tab/disclosure/search, portrait-landscape-portrait, media and +script-disabled source/disclosure checks in a private WKWebView host. After the two +fixture findings below were isolated, the maintainer confirmed the requested checks +worked as expected. This closes JL26/PV38 and their dependent QC25/QC56/QC84 rows: +110/110 requirement rows and 88/88 scenario rows are recorded passed within their +existing scoped evidence. Human observations remain attributed to the maintainer. + +The initial 4-by-4-pixel admission samples decoded on the phone but were unsuitable +for a recognizable visual check. Original 256-by-144 PNG/JPEG QA supplements made the +pictures visible. A long inline data-URL expression remained literal at the unchanged +512-scalar preview boundary; a complete 415-scalar expression rendered a recognizable +64-by-36 inline image. Complete values expand original text; derived inline previews +still obey their prefix and destination eligibility ceilings. No renderer, asset, +limit, platform, product or dependency was changed to obtain these outcomes. + +Private device records separately preserve trusted touch events, 430-point portrait +and 814-point landscape observations, native image decode dimensions, and disabled +content JavaScript with the enhanced toolbar absent. They are supplementary to the +maintainer's final observation, not an automated assertion of every requested gesture. +The compact inline decode was captured with scripting enabled; script-off standalone +media was independently captured on the preceding fixture. The final combined manual +check covers the remaining user-visible behavior. Device identifiers, signed apps and +local evidence paths are excluded from public source. + +The self-contained return preserves the previous immutable source/archive and receipts, +materializes current-head hosted evidence and the test-host/fixture source, and retains +both initial findings and failed harness assumptions. Pinned browser probes distinguish +inspection cutoff, destination ineligibility, complete original text and positive +compact inline media. These additional fixtures do not replace the independent pins +or existing browser matrix. Merge, tag and publication remain separate decisions. diff --git a/Sources/JSONHTML/Documentation.docc/StructuredPreviews.md b/Sources/JSONHTML/Documentation.docc/StructuredPreviews.md index 1d3a3b9..2b87dc8 100644 --- a/Sources/JSONHTML/Documentation.docc/StructuredPreviews.md +++ b/Sources/JSONHTML/Documentation.docc/StructuredPreviews.md @@ -21,7 +21,8 @@ Inspection prefixes are labelled and bounded independently by 512 decoded Unicod scalars, 2048 UTF-8 bytes and 512 escaped-display scalars. A scalar or control escape is never split. Keys, exact numbers and node structure remain complete. Native Original value disclosures and the complete JSON pane/source work without JavaScript. -Complete values and the print representation expand full original strings. Copy and +Complete values and the print representation expand full original strings. Derived +inline previews still obey their source-prefix and destination eligibility limits. Copy and download use native exact writer bytes, not a JavaScript JSON model. Declare typed paths through ``JSONHTMLPreviewHint``. Literal declarations suppress From b06b52f76982022b3383efd424f98988c4b16961 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:27:27 +0200 Subject: [PATCH 54/59] docs: reconcile CLI product acceptance with merged native bridges --- .../Architecture/Release1.4/Acceptance.json | 15 ++++++++++----- Documentation/Architecture/Release1.4/Plan.md | 6 ++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Documentation/Architecture/Release1.4/Acceptance.json b/Documentation/Architecture/Release1.4/Acceptance.json index 5b17493..e0743ac 100644 --- a/Documentation/Architecture/Release1.4/Acceptance.json +++ b/Documentation/Architecture/Release1.4/Acceptance.json @@ -19,15 +19,19 @@ { "id": "CL002", "gate": "C0", - "title": "Six public products with correct kinds", - "verification": "Package dump contains five libraries plus swift-json executable; private targets have no products.", + "title": "Seven public products with correct kinds after E2 reconciliation", + "verification": "Package dump contains six libraries, including merged opt-in JSONBridges, plus swift-json executable; private targets have no products.", "obligation": "mandatory", "status": "passed", "evidence_required": true, "fixture_ids": [], "evidence": [ "Documentation/Architecture/Release1.4/Qualification.md" - ] + ], + "historical_six_product_contract": { + "title": "Six public products with correct kinds", + "verification": "Package dump contains five libraries plus swift-json executable; private targets have no products." + } }, { "id": "CL003", @@ -1130,7 +1134,7 @@ { "id": "CL090", "gate": "C5", - "title": "Six-product CI/documentation", + "title": "Combined E2/CLI product CI and documentation", "verification": "Existing native/safety/docs plus affected Linux lanes, exact product kinds and public CLI help/docs/examples.", "obligation": "mandatory", "status": "not_run", @@ -1181,6 +1185,7 @@ "notes": [ "Package integrity is separate from native qualification.", "Optional extensions do not block the scoped candidate when not claimed.", - "WASI CLI supported/blocked/limited disposition is mandatory; full CLI support is separately earned." + "WASI CLI supported/blocked/limited disposition is mandatory; full CLI support is separately earned.", + "Main e8c3e4b added the approved JSONBridges product before CLI merge. Current integration preserves six libraries plus the CLI; original briefing/product evidence remains historical." ] } diff --git a/Documentation/Architecture/Release1.4/Plan.md b/Documentation/Architecture/Release1.4/Plan.md index 9ae86d6..0beabd8 100644 --- a/Documentation/Architecture/Release1.4/Plan.md +++ b/Documentation/Architecture/Release1.4/Plan.md @@ -1,7 +1,7 @@ # CLI release plan The maintainer-approved 1.4.0 train adds `swift-json` as an executable beside the -five libraries. JSONFilterRuntime and JSONCLIEngine are package-private targets; +six libraries, including the independently merged opt-in JSONBridges. JSONFilterRuntime and JSONCLIEngine are package-private targets; JSONRegexRuntime remains private. Root external dependencies remain empty. Historical four/five-product releases and their evidence are unchanged. @@ -14,7 +14,7 @@ branches and draft PRs; it does not change the parallel transfer/PDF or 1.2 work | Gate | Cohesive implementation and acceptance | | --- | --- | | C0 | Source/protection/prerequisite audit, executable/private target shapes, one noncopyable job ledger | -| C1 | Bounded argv/stdio/files, diagnostics/exits, atomic output, format/validate/Pointer/Patch, six-product inventory | +| C1 | Bounded argv/stdio/files, diagnostics/exits, atomic output, format/validate/Pointer/Patch, seven-product inventory | | C2 | Original flat filter IR and pull VM, input framing, generators/paths/control/bindings/common builtins | | C3 | Private exact decimal arithmetic; explicit scale plus half-even division policy | | C4 | Qualified richer filters, lexical functions/reductions/path updates; explicit unsupported features | @@ -38,3 +38,5 @@ host; WASI executable feasibility is separate from the already qualified librari Native and sanitizer receipts, subprocess outcomes, commands, toolchain, source and binary identities accompany every qualified milestone. All 49 baseline probes and independent-suite exclusions remain required and separate from CLI additions. + +Main reconciliation for merging PR23–PR27 preserves E2 main e8c3e4b and its public bridges, ergonomics behavior, six DocC catalogs and compiler consumers. Only the CLI executable is added to those six libraries. Exact combined module inventories and all 49 baseline probes remain mandatory. From 83f83f60a6bea3fc84fa2a7030273a9427d02517 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:30:19 +0200 Subject: [PATCH 55/59] fix(ci): preserve the combined shared-state sanitizer inventory --- Scripts/ci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/ci.py b/Scripts/ci.py index e60ba4b..0d04736 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -119,7 +119,7 @@ def check_result(command, code, log, events=None): if events is not None: counts = events['bundle_counts'] if command[:2] == ['swift', 'test']: - expected = [36] if 'cli|sharedFilterPlan' in command else [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else sorted([119, 55, 14, 11, 12, 61 if sys.platform == 'linux' else 75, 36, 10]) + expected = [36] if 'cli|sharedFilterPlan' in command else [38] if 'export|Export|apple' in command else [14] if 'Tools/WasmAudit' in command else [2, 3, 3, 11] if 'qa' in command else [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command or 'Tools/MobileQualification' in command else [1] if 'Tools/Qualification' in command else sorted([1, 1, 3 if sys.platform == 'linux' else 4, 1, 2, 4]) if 'thread' in command else sorted([119, 55, 14, 11, 12, 61 if sys.platform == 'linux' else 75, 36, 10]) passed = passed and sorted(counts) == expected if '--filter' not in command and not any(x.startswith('Tools/') for x in command): if events is not None: From 1bd394d134a6c1bd7d4b5e2fd5c16013acbe1e6c Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:34:23 +0200 Subject: [PATCH 56/59] docs: align CLI guide with qualified providers and current library products --- Documentation/Architecture/Release1.4/CLIReference.md | 6 +++--- Sources/JSON/Documentation.docc/CommandLine.md | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Documentation/Architecture/Release1.4/CLIReference.md b/Documentation/Architecture/Release1.4/CLIReference.md index fbffe41..dc37197 100644 --- a/Documentation/Architecture/Release1.4/CLIReference.md +++ b/Documentation/Architecture/Release1.4/CLIReference.md @@ -114,9 +114,9 @@ promise is made. Stdout may contain an accepted prefix after a late failure; cancellation cannot recall accepted bytes. Collectors/replay/slurp/sort need finite storage; pull evaluation does not imply constant memory for every filter. -Known schema-provider commands remain unavailable because their qualified helper -prerequisite is absent at this source base. Their authored success/rejection cases -remain blocked. Portable HTML needs neither Apple PDF preparation nor transfer +At the pre-C5 source snapshot, schema-provider commands were unavailable and their +authored success/rejection cases were blocked. The qualified portable increment +below supersedes that observation. Portable HTML needs neither Apple PDF preparation nor transfer providers. WASI CLI feasibility is distinct from compiled libraries/private kernel; Node poll_oneoff currently fails with IO exit7, while five WasmKit commands pass; POSIX SIGINT bridging remains unavailable. Actual receipts and remaining mandatory gates determine release readiness. diff --git a/Sources/JSON/Documentation.docc/CommandLine.md b/Sources/JSON/Documentation.docc/CommandLine.md index 6b46f4f..4de3d01 100644 --- a/Sources/JSON/Documentation.docc/CommandLine.md +++ b/Sources/JSON/Documentation.docc/CommandLine.md @@ -2,7 +2,7 @@ Run strict JSON workflows through the package's `swift-json` executable. -The prospective 1.4 package keeps five library products and adds one executable. +The prospective 1.4 package keeps six library products and adds one executable. The CLI engine and query runtime are package-private; applications continue to choose the library products they need. The root package has no external dependencies. @@ -69,5 +69,10 @@ swift run swift-json schema check schema.json --resource https://example.test/it Stream success requires closing syntax and EOF. An accepted stdout prefix may precede a late error; named output is installed only after completion. Schema check compiles offline and reports metadata; it does not validate an instance. -Provider commands require helpers that are absent in this candidate and are -not advertised. Use `swift-json help COMMAND` for compiled command options. +`schema provider openai-format SCHEMA --name NAME` produces a strict Responses +text-format fragment; `openai-tool` produces a flat function tool. Both use the +public offline JSONSchema helpers, preserve the exact schema and share the CLI's +cumulative budget. Incompatible declarations exit 1 with bounded issues and no +candidate. Native errors, quotas and cancellation remain operational failures. +Apple model/schema bridges remain unavailable. Local compatibility is not remote +service acceptance. Use `swift-json help COMMAND` for compiled command options. From 19080c27f9ddc082126bf5f09aa877211c8fed49 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:47:45 +0200 Subject: [PATCH 57/59] docs: finish combined API inventory and curate the CLI guide --- Documentation/Architecture/API.md | 5 +++-- Documentation/Architecture/Release1.4/Providers.md | 4 +++- Sources/JSON/Documentation.docc/JSON.md | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Documentation/Architecture/API.md b/Documentation/Architecture/API.md index d447b91..e23965b 100644 --- a/Documentation/Architecture/API.md +++ b/Documentation/Architecture/API.md @@ -13,11 +13,12 @@ in the [README](../../README.md) are compiled and executed by the public consume | JSONSchema | JSONSchemaDocument, JSONSchemaRegistry, JSONSchemaCompiler, CompiledJSONSchema, JSONSchemaValidator | Offline immutable plan, invocation-owned evaluation and private native regex | | JSONStreaming | JSONEventSequence, JSONValueSequence, NDJSONValueSequence, JSONTopLevelArraySequence, JSONAsyncWriter | Exclusive pull state, shared codec, strict framing and awaited handoffs | | JSONPatch | JSONPatch, JSONPatchOperation, JSONPatchResult, JSONMergePatch | Transaction staging, exact comparison and iterative editing | +| swift-json | format, validate, pointer, patch, query, stream, schema, render | Bounded process/file adapters and private CLI engine/filter runtime; no public query library | | JSONHTML | JSONHTMLRenderer, JSONHTMLRenderingOptions, JSONHTMLLimits, JSONHTMLRenderReport, JSONHTMLAnnotation, JSONExportDocument, JSONExportOptions, JSONExportReport | Exact HTML, native Apple transfer, immutable prepared JSON/text/Markdown/static HTML/opt-in styled RTF and cumulative budgets | JSON imports no siblings. JSONSchema depends on JSON and package-private -JSONRegexRuntime. Streaming, Patch, HTML and Bridges depend on JSON. There are six public -products and no external root-package dependencies. Runtime code uses no HTTP, +JSONRegexRuntime. Streaming, Patch, HTML and Bridges depend on JSON. There are seven public products: six libraries and the swift-json executable, +and no external root-package dependencies. Runtime code uses no HTTP, UI, MCP or documentation-tool dependencies. Immutable facades store value configuration. Every operation owns its mutable diff --git a/Documentation/Architecture/Release1.4/Providers.md b/Documentation/Architecture/Release1.4/Providers.md index bd56392..4557d82 100644 --- a/Documentation/Architecture/Release1.4/Providers.md +++ b/Documentation/Architecture/Release1.4/Providers.md @@ -53,7 +53,7 @@ with JSONParser and validate it with JSONSchemaValidator. Validation false diffe from thrown operational errors. Arbitrary streaming deltas and valid prefixes are not completed output. No OpenAI event-envelope client is added here. -## Qualification and review +## C5 qualification before E2 reconciliation Meaningful Swift Testing covers 13 schema functions and eight additional CLI functions: exact value/text/data/report parity, scalar keys, metadata, optional/null, closure, @@ -84,3 +84,5 @@ native comparison. Compiler-driven fixes are recorded in private receipts; no te or supported baseline was weakened. Final source-bound native/safety/docs/Linux, archive/reproduction, exact commands/exits/counts and remaining limitations accompany the separate return. This document is not itself an assertion that a pending lane passed. + +The E2 main reconciliation retains this provider implementation and its cases unchanged. Its combined root inventory is 395 Apple / 381 Linux functions, with 199 parameter cases, 69 schema and 85 CLI functions. Shared TSan inventory is 14 Apple / 13 Linux. See [the reconciliation record](Reconciliation.md) and final source-bound merge receipts for the refreshed composition; the earlier counts above remain the qualified C5 snapshot. diff --git a/Sources/JSON/Documentation.docc/JSON.md b/Sources/JSON/Documentation.docc/JSON.md index 7be7ee0..8197bd6 100644 --- a/Sources/JSON/Documentation.docc/JSON.md +++ b/Sources/JSON/Documentation.docc/JSON.md @@ -55,6 +55,7 @@ belongs exclusively to each invocation. - - +- - ### Exact model From 80b5ea77253f75095bb2f81247b62776f7f62f39 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:16:48 +0200 Subject: [PATCH 58/59] docs: align forward preview contract with approved seven products --- .../Release1.3/StructuredPreviews/APIAndPolicy.md | 6 ++++-- README.md | 2 +- Scripts/README.md | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md b/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md index 1df537a..395e7be 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/APIAndPolicy.md @@ -4,8 +4,10 @@ This additive work implements the maintainer's consolidated briefing revision 0. The revision is a design input, not a package release number. Release placement is unassigned. Earlier Release1.3 and Release1.4 records retain their historical gates. -The root package exposes five libraries (JSON, JSONSchema, JSONStreaming, JSONPatch, -JSONHTML) and the sixth `swift-json` executable. External package dependencies remain +The approved current baseline exposes six libraries (JSON, JSONSchema, JSONStreaming, +JSONPatch, JSONHTML and opt-in JSONBridges) plus the `swift-json` executable. This +supersedes the earlier five-library forward inventory; see [reconciliation](Reconciliation.md). +JSONBridges depends on JSON only and is never re-exported by core. External dependencies remain empty. JSONHTML imports JSON; it does not require schema, an image codec, a Markdown engine, a TeX engine or a browser at generation time. Original code is Apache-2.0. diff --git a/README.md b/README.md index 3292714..b5cb356 100644 --- a/README.md +++ b/README.md @@ -513,7 +513,7 @@ swift build -c release --product swift-json ### CLI development for 1.4 -The isolated 1.4 feature stack adds the `swift-json` executable beside the five +The isolated 1.4 feature stack adds the `swift-json` executable beside the six libraries, with no external root dependencies. Build from the feature source with Swift6.4+ (`swift build -c release --product swift-json`), or use `swift run swift-json help`. Native qualification requires macOS/SDK27+. diff --git a/Scripts/README.md b/Scripts/README.md index 3e703ac..3cb974c 100644 --- a/Scripts/README.md +++ b/Scripts/README.md @@ -9,7 +9,7 @@ separate from the dependency-free library. Native lanes fail on unsupported host | `python3 Scripts/preflight.py` | Actual native OS/compiler/SDK receipt | | `python3 Scripts/ci.py --lane native` | Manifests, builds, Swift Testing, consumers and suites | | `python3 Scripts/ci.py --lane safety` | Address/Thread Sanitizer and strict memory safety | -| `python3 Scripts/ci.py --lane linux` | Actual Linux preflight, all six products, exact root/cap/corpus/consumer counts | +| `python3 Scripts/ci.py --lane linux` | Actual Linux preflight, all seven products, exact root/cap/corpus/consumer counts | | `python3 Scripts/ci.py --lane linux-safety` | Linux Address/Thread Sanitizer and strict memory safety | | `python3 Scripts/ci.py --lane wasm` | Matched Swift 6.4 WASI debug/release, exact root/corpus/cap/consumer counts, two-host public parity and structured resource failures | | `python3 Scripts/qualify_linux_reproduction.py --output PATH` | Actual source archive, byte-equal rebuild, clean extraction, build/tests and public consumer | From bc05d7941e8928ec367ce70503c65fe37d334dad Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:05:54 +0200 Subject: [PATCH 59/59] fix(ci): preserve export qualification and event failure receipts --- .../Architecture/Release1.3/Progress.json | 7 ++++++ .../StructuredPreviews/Reconciliation.md | 13 +++++++++++ Scripts/ci.py | 22 +++++++++++++++---- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/Documentation/Architecture/Release1.3/Progress.json b/Documentation/Architecture/Release1.3/Progress.json index eb8c8ce..2781770 100644 --- a/Documentation/Architecture/Release1.3/Progress.json +++ b/Documentation/Architecture/Release1.3/Progress.json @@ -409,6 +409,13 @@ "qualification_policy": "Fresh combined-source receipts required; parent receipts immutable; unchanged HTML physical/browser evidence retains its scoped source identity.", "contract": "StructuredPreviews/Reconciliation.md", "merge_tag_publication_performed": false + }, + "final_merge_review": { + "finding": "Existing main export lane and event I/O diagnostics were absent from the reconciled harness", + "resolution": "Restore main export commands/CLI choice and diagnostic receipt fields; retain combined [1,39] focused discovery guard", + "library_cli_asset_fixture_changes": false, + "qualification_policy": "Fresh hosted required checks and actual local export lane; unchanged implementation/WASI receipts retained through equivalence", + "merge_authorization": "Maintainer requested ready and merge after checks/reviews pass; tags/publication separate" } } } diff --git a/Documentation/Architecture/Release1.3/StructuredPreviews/Reconciliation.md b/Documentation/Architecture/Release1.3/StructuredPreviews/Reconciliation.md index 888694c..ac31c71 100644 --- a/Documentation/Architecture/Release1.3/StructuredPreviews/Reconciliation.md +++ b/Documentation/Architecture/Release1.3/StructuredPreviews/Reconciliation.md @@ -72,3 +72,16 @@ separately from the earlier pinned return. Byte-identical HTML implementation/te assets retain their prior browser, physical-touch, VoiceOver and calibration evidence within its stated scope. The inherited approximately 11% streaming comparison and existing disposition remain unchanged. No PR merge, tag or publication is performed. + + +## Final merge review: preserve the export lane + +Review against current main found that the earlier CLI reconciliation had dropped +its existing `export` lane and event-reader I/O diagnostics. Restore the complete +five-command export lane, including the native public consumer and three Apple SDK +probes, while retaining the structured combined focused inventory `[1, 39]`. +Restore missing-event error capture and both console/ABI parameter counts in receipts. +Library, CLI, assets, fixtures, product policy and all counters remain unchanged. +Fresh required hosted checks and actual export-lane execution qualify the harness +correction. Earlier source-bound implementation/WASI evidence is retained through +exact equivalence; no previous receipt is relabelled to the new head. diff --git a/Scripts/ci.py b/Scripts/ci.py index 6b37e85..2a92ad3 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -41,6 +41,14 @@ def commands(lane, output): ['swift', 'test', '-c', 'release', '--filter', 'qa'], ['swift', 'test', '-c', 'release', '--sanitize', 'address', '--scratch-path', '.build/ci-address', '--filter', 'qa'], ] + if lane == 'export': + return [ + ['python3', '-B', 'Scripts/public_check.py'], + ['python3', '-B', 'Scripts/verify_html_assets.py'], + ['swift', 'test', '--filter', 'export|Export|apple'], + ['swift', 'test', '-c', 'release', '--filter', 'export|Export|apple'], + ['python3', '-B', 'Scripts/qualify_json_export.py', '--output', str(output / 'apple')], + ] if lane == 'browser': return [ ['python3', '-B', 'Scripts/verify_html_assets.py'], @@ -176,7 +184,7 @@ def check_result(command, code, log, events=None): def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'cli'], required=True) + parser.add_argument('--lane', choices=['native', 'safety', 'docs', 'r0', 'qa', 'linux', 'linux-safety', 'wasm', 'browser', 'export', 'cli'], required=True) parser.add_argument('--output') args = parser.parse_args() output = ROOT / (args.output or '.build/ci/' + args.lane) @@ -211,15 +219,21 @@ def save(): log = log_path.read_text(errors='replace').replace(str(ROOT), '.') log_path.write_text(log) events = None + event_error = None try: - events = read_events(event_path.read_text()) if event_path is not None else None + if event_path is not None: + events = read_events(event_path.read_text()) passed, counts = check_result(command, code, log, events=events) - except (KeyError, ValueError, TypeError): + except (OSError, KeyError, ValueError, TypeError) as error: passed, counts = False, [] + event_error = str(error) entry = {'command': [x.replace(str(ROOT), '.') for x in command], 'exit_code': code, 'status': 'passed' if passed else 'failed', 'log': log_path.relative_to(ROOT).as_posix(), 'log_sha256': hashlib.sha256(log_path.read_bytes()).hexdigest(), 'swift_testing_function_counts': counts, 'parameterized_case_invocations': sum(int(n) for n in re.findall(r'with (\d+) test cases passed', log))} if events is not None: - entry['swift_testing_events'] = dict(events, path=event_path.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(event_path.read_bytes()).hexdigest()) + entry['console_parameterized_case_invocations'] = entry['parameterized_case_invocations'] entry['parameterized_case_invocations'] = events['parameter_case_invocations'] + entry['swift_testing_events'] = dict(events, path=event_path.relative_to(ROOT).as_posix(), sha256=hashlib.sha256(event_path.read_bytes()).hexdigest()) + if event_error is not None: + entry['event_error'] = event_error receipt['commands'].append(entry) print(json.dumps(entry), flush=True) if not passed:

      ", meter: &meter) - try output.escaped(options.title, meter: &meter) - try output.append("

      ", meter: &meter) - try output.append( - options.valueProfile == .inspection && options.layout == .structured - ? "Inspection view · bounded string previews; exact values remain available." - : "Complete values · exact JSON source remains available.", meter: &meter) - try output.append("

      ", meter: &meter) - let copy = try nativeBytes(value, options: writingOptions, meter: &meter) - output.setCopyBytes(UInt64(copy.count)) - try output.append( - "

      Download exact JSON (UTF-8; works without JavaScript)

      ", meter: &meter) - if interactive { - try output.append( - "

        ", meter: &meter) - } - try meter.release(UInt64(copy.count)) - var outline: [StructuredOutline] = [] - if options.layout == .structured || interactive { - try output.append( - "
        ", - meter: &meter) - try structuredBody( - value, plan: plan, profile: options.valueProfile, namespace: "jh-n-", - interactive: interactive, countNodes: true, annotations: annotations, outline: &outline, - output: &output, meter: &meter) - try output.append("
        ", meter: &meter) - if interactive, options.outlineDepth != nil { - try output.append( - "", meter: &meter) - } - try output.append("
        ", meter: &meter) - } - if interactive || options.layout == .syntax { - try output.append( - "

        Exact JSON source

        ",
        -        meter: &meter)
        -      try highlighted(value, options: writingOptions, output: &output, meter: &meter)
        -      try output.append("
        ", meter: &meter) - } else { - try output.append( - "
        Exact JSON source (complete)
        ",
        -        meter: &meter)
        -      try highlighted(value, options: writingOptions, output: &output, meter: &meter)
        -      try output.append("
        ", meter: &meter) - } - if !plan.issues.isEmpty { - try output.addDOM(UInt64(plan.issues.count) * 4 + 4, meter: &meter) - try output.append( - "

        Preview issues

          ", - meter: &meter) - for issue in plan.issues { - try meter.work() - try output.append("
        • ", meter: &meter) - let pointer = try pointerBytes(issue.path, meter: &meter) - try output.escaped(String(decoding: pointer, as: UTF8.self), meter: &meter) - try meter.release(UInt64(pointer.count)) - try output.append(": ", meter: &meter) - try output.escaped(issue.message, meter: &meter) - try output.append("
        • ", meter: &meter) - } - try output.append("
        ", meter: &meter) - } - try annotationSection(annotations, output: &output, meter: &meter) - // Printing is a separately charged representation of the same immutable plan/root. - try output.append( - "
        ", meter: &meter) - if options.layout == .structured { - var ignored: [StructuredOutline] = [] - try structuredBody( - value, plan: plan, profile: options.printValueProfile, namespace: "jh-p-", - interactive: false, countNodes: false, annotations: [], outline: &ignored, output: &output, - meter: &meter) - } else { - try output.append("
        ", meter: &meter)
        -      try highlighted(value, options: writingOptions, output: &output, meter: &meter)
        -      try output.append("
        ", meter: &meter) - } - try meter.release(plan.reservation) - try output.append("