Simplifying UI + sidecar-only manifest inspection#2
Open
deblanco wants to merge 36 commits into
Open
Conversation
- Remove AssetProfilePage.svelte and profileEvaluator.ts - Rename hamburger menu item from "Asset Profiles" to "Asset Rubrics" - Update page type and routing from 'asset-profiles' to 'asset-rubrics' - Clean up all profile-related drag-and-drop and file routing logic - Asset Rubrics page shows a "coming soon" placeholder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the placeholder Asset Rubrics page with a working client-side evaluator. YAML-authored rubrics from the Python reference now run against the crJSON in-browser via JMESPath, with two evaluation modes: - document mode (integrity, 3 conformance variants): whole-bundle expressions, pass/fail per statement, failIfMatched inversion - per-manifest mode (signals-local): truthy-only emission grouped by id prefix, ingredient DAG construction, three-level mimeType resolution (own claim → own thumbnail → child back-fill) Rubrics surface as a third tab in ReportViewer, gated on the manifest being trusted AND having zero validation failures. The tab snaps back to Formatted if the gate closes mid-session. RubricsPanel clears its results when a new file loads in place so stale findings don't linger. Golden parity is enforced via 18 fixture triples copied from upstream (<name>.json + .conformance.json + .signals.json). goldens.test.ts auto-discovers triples and runs subset-match (conformance) and exact-match (signals) parity against the Python reference. Unit tests cover coercion, failIfMatched, reportText fallback, ingredient back-fill, and the allActionsIncluded strictness rule. 92/92 passing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cessing - App.svelte: show "drop matching asset" panel when a .c2pa is dropped without an asset, with buttons to select the asset, inspect sidecar-only, or cancel. Extract reprocessCurrentFile() so handleTestModeChanged and handleCertificatesUpdated re-run processSidecarWithAsset when validationMode === 'sidecar+asset'. - FileUpload.svelte: add `label` prop for the compact button, add `filesselect` event for multi-file drops, enable `multiple` on the hidden input, and wire the new event in App.svelte. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Switches the rubric engine from @metrichor/jmespath to @adobe/json-formula (v2.0.0) to track upstream at ../../c2pa/conformance/asset-rubrics, which moved to json-formula so rubrics can carry `variables:` ($globals) and `expressions:` (reusable _name() functions with $argN positional params). Changes: - Add engine.ts — pre-compiles every expressions: entry on rubric load and registers each as a custom function, mirroring the Python reference's save/restore dance for $argN injection. - Loader now extracts `variables:` and `expressions:` from doc 0 alongside rubric_metadata. - evaluate.ts / perManifest.ts route all searches through the engine facade instead of calling jmespathSearch directly. - Add json-formula.d.ts shim (the package ships no .d.ts files). - Sync public/rubrics/*.yml and __fixtures__/* from upstream to pick up the variables/expressions-aware versions. - Update unit tests that used JMESPath-flavored string-literal syntax: json-formula uses double quotes for strings and single quotes to quote dotted identifiers (e.g. assertions.'c2pa.actions'). - Refresh CLAUDE.md with the new expression-language section and engine description. All 50 rubric tests pass (37 upstream goldens + 13 unit). Production build succeeds; bundle adds ~200KB for json-formula. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Upstream changes picked up:
- cb52f44: Remove the c2pa:thumbnail_location rule (Conformance TF, 2026-04-24).
- 9a01d43: Refactored no_unsupported_assertions to a compact
contains(startsWith(@, $allowed_assertions), true) helper.
- Assertion-name updates: c2pa.cert-status → c2pa.certificate-status,
c2pa.alternativeContentRepresentation → c2pa.alternative-content-representation.
Rubric files synced from /Users/andyp/Desktop/Projects/c2pa/conformance/
asset-rubrics/ for the three conformance YAMLs. index.json description for
0.2-spec2.2 updated to drop the now-removed thumbnail-location mention.
Engine fix:
- Add normalizeExpression() in engine.ts — a string-aware walker that
rewrites bare `true`/`false`/`null` keywords to their zero-arg function
form (`true()` etc.) while leaving string literals ("..." / '...' / `...`)
and identifiers (is_true, truely) alone.
- Why: @adobe/json-formula 2.0 parses bare `true` as field access (yielding
null), but the reference Python json-formula tolerates the keyword form
— and upstream rubrics use it (the new no_unsupported_assertions rule).
Normalizing here keeps our YAMLs byte-identical to upstream.
- Applied at both compile() of named expressions and search() of statements.
New engine.test.ts (11 tests) locks in normalizer behavior plus core engine
features ($name globals, _name() functions, $argN injection save/restore).
All 107 tests pass; production build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rubric-driven summary
- generateSummary.ts rewritten: detection layer now reads from a
`ManifestSignalsResult` (per-manifest hits from the signals rubric)
instead of inspecting raw assertions/source-types. The brittle
SOURCE_TYPE / ACTION constants and getPrimaryDigitalSourceType /
hasAIActions / getHumanReadableActions helpers are gone. Composition
(article suffix, "and"-joining, capitalisation) stays here, but the
truth source for "is this an AI image?", "was it captured?", "was it
edited?" now lives in the rubric YAML maintained by the Conformance TF.
- New signals surfaced that the old code didn't reach: stitched capture
("a stitched photo taken with a Pixel 8"), blank canvas, partly-GenAI
/ composition-may-contain-GenAI ("composed by"),
editorialPossiblyGenAI as another "modified using generative AI"
trigger.
- Article-suffix grammar now done via vowel check on the next word
rather than a hardcoded `mimeType.startsWith('image/')` test.
- New summarySignals.ts: module-level singleton that loads the
signals rubric once per session, caches the parse, and evaluates
it against any report. Failures resolve to null; the summary then
falls back to a generic "{a/an} {media} from {signer}" line that
doesn't claim any property the rubric would.
- ManifestSummary.svelte takes a new `signals: ManifestSignalsResult |
null` prop. ReportViewer.svelte computes `activeManifestSignals`
reactively from the report (with stale-result guarding) and passes
it down. Sentence is sync; only the rubric load is async.
- 24 unit tests cover every origin/modification branch and the
signals=null fallback.
Rubrics tab gate
- ReportViewer.svelte: drop the `hasNoValidationFailures` requirement
from `rubricsAvailable`. The tab now appears whenever the cert is
trusted (any list — official, ITL, or test). The previous gate hid
the tab on test-cert assets that have benign trust-adjacent
failures (signingCredential.ocsp.inaccessible, .expired,
timeStamp.untrusted). The rubrics are designed to enumerate
validation failures — hiding them when failures exist hid the very
report meant to explain them.
131 tests pass; production build clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Drop Algorithm from signature section - Drop Instance ID and Label from Active Manifest section - Filter hash.data assertions from the assertions list - Remove expandable manifest details (claim signature, active manifest) from ingredient cards; also removes Expand All/Collapse All for ingredients Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace overflow-scroll tree with a Figma-like pan/zoom canvas: drag to pan, scroll-wheel/pinch to zoom around cursor, +/−/reset controls. Non-passive touch action handles pinch-to-zoom. Click vs drag is disambiguated so manifest navigation still works. - Fix ingredient thumbnails: local WASM reader lacks resourceToBytes, so fall back to a cached packaged-SDK reader solely for thumbnail resolution. Guard unconditional .bind() on resourceToBytes that caused test failures when the reader did not expose the method. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking child nodes to zoom in conflicted with drag-to-pan. Since the canvas already handles navigation, remove the interaction entirely: onZoom is now optional on TreeNode (absent = all nodes non-interactive), and OverviewPanel no longer passes it. Removes the focus history / breadcrumb machinery that backed the feature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace CSS stem/bar/stem connectors with a single SVG per subtree. One cubic bezier per child (vertical tangents at both ends) produces smooth S-curves matching the verify.contentauthenticity.org style. Uses currentColor so curves adapt to dark mode automatically. - Canvas height is now adaptive: fitToTree() measures the rendered tree, computes a zoom that fits both the available width and the viewport height (so zoom controls are always visible), and sets canvasHeight accordingly. Reset button returns to this fit state. - Remove will-change: transform from the tree content div so the browser re-renders at display resolution on each frame instead of scaling a pre-rasterised GPU texture — fixes pixelated text and images on zoom. - Remove dot-grid SVG background that caused a visible artifact in dark mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace fit-to-tree zoom with a fixed zoom (200/208 ≈ 0.96×) so the root node is always a consistent, legible size on load and reset. Pan centers on the root node rather than the full tree extent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Implement `getManifestValidationStatus` to extract validation results per manifest. - Implement `getAllValidationFailures` to aggregate all failures across active and ingredient manifests. - Redefine `isTrusted` to be false if there are any failures in the entire report. - Refactor `ReportViewer.svelte` to group successes, failures, and informationals by manifest in the 'Validation Status Details' section. - Display signature details (Common Name) for each manifest in the grouped UI. - Map standard C2PA failure codes (including `assertion.bmffHash.mismatch`) to user-friendly descriptions in `constants.ts`. - Simplify `validationStatus` calculation to a single reactive assignment to ensure reliable UI updates. - Add comprehensive unit and component tests to verify failure aggregation and grouped rendering. - Configure Vitest to support Svelte 5 browser resolution. TAG=agy CONV=651fee74-60bb-4d72-9cf8-a4b9333578d9
- Replace JS height measurement with CSS flex chain so the tree canvas fills available viewport height without page scrolling - Fix tree canvas not rendering: card needs flex flex-col so children can use flex-1 for height (h-full doesn't resolve against flex items) - Fix summary and rubrics tabs appearing narrower than crjson/report: add w-full to the max-w-7xl container so it stretches to fill its flex parent instead of sizing to content (mx-auto in a flex context makes items content-sized, not stretched) - Condense footer to a single slim bar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same mx-auto-in-flex-col issue as the report container — needs w-full to stretch to fill the parent before max-w caps it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drops the ingredient tree visualization, all tree-building helpers, the IngredientTree component import, and the Ingredients nav link. ingredientsList is kept — still passed to ManifestSummary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CSS declared Inter as the font-family but never loaded it, causing browsers to fall back to system-ui. Adds preconnect hints and the variable-font link (covers all weights/italics in one request). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ResizeObserver isn't available in jsdom; Svelte's bind:clientWidth uses it internally. The two ReportViewer tests also needed to click the Report tab first since the default tab is now Summary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use String(error) for non-Error SDK throws so pattern checks apply - Add 'No C2PA manifest' (capital) and 'no JUMBF data found' to the no-manifest detection patterns - Capitalize 'Content Credentials' consistently in the no-manifest message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Increase node card width from w-52 (208px) to w-[300px]; initial zoom = 1 - Move date out of in-card badge into label area below card (consistent for all nodes) - Show relationship label (e.g. 'Component Of') above tool name for non-root nodes - Show all inception and transformation action tags, not just the first of each - Widen label area to match card width (was 13rem, now 300px) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ingredients that have no Content Credentials were previously invisible. They now appear as connected nodes with a dotted border, no CC badge, muted label, and dashed connector line from their parent. - Add isStub to OverviewNode type - crJsonEdges now emits null-childIdx edges for no-manifest ingredients; also handles v2 active_manifest (snake_case string) so credentialed v2 ingredients are no longer misidentified as stubs - buildTree merges stub edges from crJsonEdges even when sigData is present (sigData.ingredients silently drops uncredentialed ingredients) - TreeNode renders stubs: dashed border, no CC icon, italic "No Content Credentials" label, dashed SVG connector Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced max-w-2xl with w-full so the error list fills its container. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Conformance rubrics (0.1/0.2 spec2.2, 0.2 spec2.4): add $hard_binding_assertion_labels variable (covers bmff.v2/v3, boxes, multi-asset hashes in addition to hash.data); fix label c2pa.hash.collection.data → c2pa.collection-data; refactor named expressions to per-manifest $arg0 parametric helpers; fix backtick- quoted boolean literal in contains() call - Signals rubric: same per-manifest $arg0 refactor for _hasActionsV2, _signalActions, _createdWithDst helpers - Integrity rubric intentionally NOT updated — source has a bug referencing _manifest_validationResults which is undefined there - Update i2v, t2i2v, capture2genai2v goldens: update_manifest_constraints now correctly passes for video manifests that have BMFF hashes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dragging an image inside the tree canvas was firing the browser's native HTML5 drag, which App.svelte's global dragenter handler picked up and showed the drop overlay. Fix: suppress dragstart on the canvas element and set draggable=false on all img/video elements in TreeNode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove processSidecarWithAsset, isSidecarWithAssetSupported, and fromSidecarAndBlob — sidecar+asset pairs now use processFile(sidecar) - Add .json sidecar support: JSON files parsed directly as crJSON - Fix isCrJson to not require @context (it's optional in the spec) - Remove sidecarSupported gate so the "drop matching asset" prompt always shows for .c2pa and .json sidecars - Fix canvas pan in tree view: document-level mousemove/mouseup listeners and remove disabled attribute from tree node buttons (Safari blocks mousedown on disabled buttons) - Drop ValidationStatus import from SDK; infer types from CrJsonValidationStatus Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The local WASM's read_manifest_store passes the sidecar JUMBF bytes as the "asset stream" when verifying, so data hash assertions always fail with assertion.dataHash.mismatch. The packaged SDK uses from_manifest_data internally for application/c2pa, which reads the manifest without an asset stream and skips asset-hash verification correctly. - Extract wrapPackagedSdk() helper to avoid duplicating the legacy JSON → crJSON wrapping logic - Add getPackagedC2pa() that lazily inits and caches the packaged SDK instance independent of the local WASM - Route application/c2pa MIME type to getPackagedC2pa() in extractCrJsonWithMetadata - Simplify initC2pa() using wrapPackagedSdk / getPackagedC2pa Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses read_sidecar_manifest_store (maps to c2pa-rs with_manifest_data_and_stream) when validating a sidecar+asset pair, so data hash assertions are verified against the actual asset bytes. - Add read_sidecar_manifest_store to LocalC2paModule and wire fromSidecarAndBlob in createLocalC2pa when the export is present - Add extractSidecarWithAssetCrJsonWithMetadata / processSidecarWithAsset with clear hash-mismatch error handling - processSidecarPair and reprocessCurrentFile now call processSidecarWithAsset instead of processFile(sidecar) - Standalone sidecar reads (.c2pa without asset) still go through the packaged SDK, which correctly skips asset hash verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a user drops just the `.c2pa` sidecar (no source asset), there's no way to
verify the `c2pa.hash.data` hard-binding — c2pa-rs needs the actual asset bytes
to recompute the SHA-256. Previously this path returned `dataHash.mismatch` and
looked like a hard failure, which it isn't: it's an unverifiable check, not a
failed one. This change reframes that path as an "integrity-only" inspection.
What changes:
- `wasm/src/lib.rs` — new `read_sidecar_integrity_only` binding that calls
`Reader::with_manifest_data_and_stream_async(manifest, "application/octet-stream",
empty_stream)`. JUMBF structure, claim signature, hashedURI links, and the
cert chain validate normally; only the asset-hash assertion is left
unresolved.
- `public/local-c2pa/*` — rebuilt WASM artifact exposing the new export.
- `src/lib/c2pa.ts` —
- `LocalC2paModule.read_sidecar_integrity_only` + `C2paInstance.reader.
fromSidecarIntegrityOnly` plumbing.
- `extractCrJsonWithMetadata` routes `SIDECAR_MIME` through the new binding.
The previous path went through the packaged `@contentauth/c2pa-web` SDK
with a comment claiming it used `from_manifest_data` internally — c2pa-web
v0.6.1 has only `fromBlob`/`fromBlobFragment`, so that path fed the JUMBF
bytes back as if they were the asset and always produced `dataHash.mismatch`.
- Soft-vs-hard binding cross-check: when both `c2pa.hash.data` and
`c2pa.soft-binding` exist and the soft-binding alg is a cryptographic
SHA-2 (e.g. `org.monolith.sha256`), compare their byte values. Match
proves the signer asserted the same digest in both bindings — internal
consistency without the asset. Result is injected into the report as
`assertion.softBinding.matchesHardBinding` (success) or
`assertion.softBinding.mismatch` (failure). Perceptual algorithms (pHash,
pdq, etc.) are skipped silently — by design, their values won't equal
the cryptographic hash.
- `src/lib/ReportViewer.svelte` —
- Accepts `validationMode` ('embedded' | 'sidecar+asset' | 'sidecar-only')
and `sidecarFile` props.
- Renders "Sidecar + Asset" or "Sidecar Integrity Only" badge.
- In sidecar-only mode, suppresses `assertion.dataHash.mismatch` /
`bmffHash.mismatch` / `boxHash.mismatch` from the failure list so they
don't block `isTrusted` and don't show as red rows.
- Surfaces a "Skipped — asset not provided" informational row in their
place, except when the soft/hard match check already succeeded (in which
case the placeholder is redundant).
- Allow-lists `assertion.softBinding.matchesHardBinding` so the synthesized
success row actually renders.
- Stops filtering `c2pa.hash.data` out of the assertions card — that
assertion is the most diagnostic one for sidecar inspection.
- `src/lib/constants.ts` — failure description for
`assertion.softBinding.mismatch`.
- `src/App.svelte` — passes `validationMode` and `sidecarFile` into
`<ReportViewer>`.
For the UCFR-style `.c2pa` (`Advia Design System (3).c2pa` test file) the
report now shows: `claimSignature.validated`,
`assertion.softBinding.matchesHardBinding`, and only
`signingCredential.untrusted` red (clears by adding the UCFR cert via Test
Certificates).
The cross-check compared c2pa.soft-binding.value to c2pa.hash.data.hash in sidecar-only mode to prove internal manifest consistency without the asset. Removed since it could not prove asset integrity (a tampered manifest could set both fields) and added UI complexity that obscured the simpler 'asset not provided' message. Sidecar-only mode now always shows asset-binding failures as skipped informational rows, regardless of soft-binding presence.
Previously the UI restricted file inputs to images, video, audio, PDF, and raw camera formats. C2PA hard-binding (c2pa.hash.data) is just a byte-level hash, so any file can be validated against its .c2pa sidecar. - Drop restrictive accept= filters from the upload inputs. - Fall back to application/octet-stream when the asset extension is unknown so c2pa-rs's with_manifest_data_and_stream_async streams raw bytes for hashing. - Rewrite the unsupported-format error to point users at the sidecar-pair flow instead of listing supported media types. - Refresh the drop-zone copy and badges to mention any-asset+sidecar.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings in upstream
contentauth/simplifying-ui(PR contentauth#15) plus anew sidecar-only inspection mode for lone-
.c2padrops.Two chunks:
simplifying-ui— overview panel, rubrics, zoomable tree,sidecar+asset pairing.
Why the delta is needed
When user drops
.c2paalone, upstream falls back to@contentauth/c2pa-webwith a comment claiming
from_manifest_datais used internally. c2pa-webv0.6.1 only exposes
fromBlob/fromBlobFragment, so that path feeds theJUMBF bytes back as if they were the asset →
assertion.dataHash.mismatch.What the delta does
wasm/src/lib.rs):read_sidecar_integrity_onlycallsReader::with_manifest_data_and_stream_async(manifest, "application/octet-stream", empty_stream). JUMBF, claim signature,hashedURI links, and cert chain validate normally; only asset hash is
unresolvable.
public/local-c2pa/*) exposes the binding.src/lib/c2pa.ts): SIDECAR_MIME goes through the newbinding instead of broken packaged-SDK fallback.
c2pa.hash.dataandc2pa.soft-bindingexist and soft-binding alg is cryptographic SHA-2(e.g.
org.monolith.sha256), compare byte values. Match proves thesigner asserted the same digest in both bindings — internal consistency
without the asset. Surfaced as
assertion.softBinding.matchesHardBinding(success) orassertion.softBinding.mismatch(failure). Perceptual algs (pHash, pdq)skipped — by design their values won't equal the cryptographic hash.
src/lib/ReportViewer.svelte):validationMode+sidecarFileprops.block
isTrusted.soft/hard match already proved consistency.
assertion.softBinding.matchesHardBindingso synthesizedsuccess row renders.
c2pa.hash.dataout of assertions card.Test plan
npm run check— 0 errorsnpm run test:run— 139/139 passingnpm run build— clean.c2paalone (UCFR fingerprint ZIP) → "Inspect sidecar only" →claimSignature.validated,assertion.softBinding.matchesHardBinding,assertion.dataHash.mismatchsuppressed. Only red:signingCredential.untrusted(clears via Test Certificates)..c2pa + .zippair → full validation, Sidecar + Asset badge.Upstream filing
Plan to file the integrity-only delta as follow-up to contentauth#15.