Release Format Lab 0.2.0
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
name: Verify
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: verify-${{ gitea.repository }}-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
env:
|
||||||
|
CI: "true"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: npm
|
||||||
|
- name: Select declared npm version
|
||||||
|
run: npm install --global npm@11.17.0
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Audit runtime dependencies
|
||||||
|
run: npm audit --omit=dev --audit-level=moderate
|
||||||
|
- name: Check, test, and build
|
||||||
|
run: npm run check
|
||||||
|
- name: Install browser engines
|
||||||
|
run: npx playwright install --with-deps chromium firefox webkit
|
||||||
|
- name: Browser tests
|
||||||
|
run: npm run test:browser
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 - 2026-09-02
|
||||||
|
|
||||||
|
- Add shared evidence envelopes, a reusable format catalogue and bounded local
|
||||||
|
corpus runs for comparing measured round-trip behaviour across recipes.
|
||||||
|
|
||||||
## 0.1.0 - 2026-09-01
|
## 0.1.0 - 2026-09-01
|
||||||
|
|
||||||
- Add property-aware conversion graphs for data, image, audio/video, and
|
- Add property-aware conversion graphs for data, image, audio/video, and
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Explore conversion paths, state the expected preservation boundary, and measure
|
|||||||
what a representative round trip actually changes. Format Lab is a local-first
|
what a representative round trip actually changes. Format Lab is a local-first
|
||||||
module in the add·ideas Toolbox.
|
module in the add·ideas Toolbox.
|
||||||
|
|
||||||
## v0.1.0 capabilities
|
## v0.2.0 capabilities
|
||||||
|
|
||||||
- Browse directed format graphs for structured data, images, audio/video, and
|
- Browse directed format graphs for structured data, images, audio/video, and
|
||||||
subtitles
|
subtitles
|
||||||
@@ -20,11 +20,14 @@ module in the add·ideas Toolbox.
|
|||||||
untested when it is outside the canonical model
|
untested when it is outside the canonical model
|
||||||
- Export the target data and a deterministic evidence report containing the
|
- Export the target data and a deterministic evidence report containing the
|
||||||
catalog version, exact path, expectations, measurements, and notices
|
catalog version, exact path, expectations, measurements, and notices
|
||||||
|
- Export a versioned portable evidence contract shared with other audit tools,
|
||||||
|
inspect a broad masked-offset file-signature catalog, and download a
|
||||||
|
deterministic edge-case corpus as individually named fixtures plus manifest
|
||||||
|
|
||||||
## Honest scope
|
## Honest scope
|
||||||
|
|
||||||
The image, audio/video, and subtitle graphs are planning contracts linked to the
|
The image, audio/video, and subtitle graphs are planning contracts linked to the
|
||||||
specialized Toolbox apps; v0.1.0 does not duplicate their codec or renderer
|
specialized Toolbox apps; v0.2.0 does not duplicate their codec or renderer
|
||||||
engines. Catalog statuses are conservative expectations, not measurements of a
|
engines. Catalog statuses are conservative expectations, not measurements of a
|
||||||
particular file or encoder. The executable data lab supports records only—not
|
particular file or encoder. The executable data lab supports records only—not
|
||||||
arbitrary JSON documents, XML mixed content/namespaces, or an external CSV type
|
arbitrary JSON documents, XML mixed content/namespaces, or an external CSV type
|
||||||
@@ -34,6 +37,10 @@ Inputs are capped at 2 MiB, 10,000 records, 200 fields per record, 500,000
|
|||||||
top-level values, and 8 MiB output. CSV cells are strings and spreadsheet-formula
|
top-level values, and 8 MiB output. CSV cells are strings and spreadsheet-formula
|
||||||
prefixes are neutralized; the resulting semantic change is visible in the
|
prefixes are neutralized; the resulting semantic change is visible in the
|
||||||
round-trip measurement.
|
round-trip measurement.
|
||||||
|
Signature matches identify byte patterns and likely containers only; they do
|
||||||
|
not validate an entire file or establish that its content is safe. Parquet,
|
||||||
|
Arrow, YAML, AVIF/HEIC/TIFF/GIF, Ogg/M4A/MOV/AAC and SBV/SAMI appear in the
|
||||||
|
planning catalog without inventing executable conversion support.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Corresponding source
|
# Corresponding source
|
||||||
|
|
||||||
The corresponding source for Format Lab 0.1.0 is available at:
|
The corresponding source for Format Lab 0.2.0 is available at:
|
||||||
|
|
||||||
https://git.add-ideas.de/lotobo/format-lab/src/tag/v0.1.0
|
https://git.add-ideas.de/lotobo/format-lab/src/tag/v0.2.0
|
||||||
|
|
||||||
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
|
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Third-party notices
|
# Third-party notices
|
||||||
|
|
||||||
Format Lab 0.1.0 directly depends on these runtime packages:
|
Format Lab 0.2.0 directly depends on these runtime packages:
|
||||||
|
|
||||||
| Package | Pinned version | Declared licence |
|
| Package | Pinned version | Declared licence |
|
||||||
| -------------------------------- | -------------: | ---------------- |
|
| -------------------------------- | -------------: | ---------------- |
|
||||||
| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 |
|
| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 |
|
||||||
| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later |
|
| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later |
|
||||||
| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 |
|
| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 |
|
||||||
| `react` | 19.2.8 | MIT |
|
| `react` | 19.2.8 | MIT |
|
||||||
| `react-dom` | 19.2.8 | MIT |
|
| `react-dom` | 19.2.8 | MIT |
|
||||||
|
|
||||||
|
|||||||
Generated
+19
-19
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "format-lab",
|
"name": "format-lab",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "format-lab",
|
"name": "format-lab",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3",
|
"@add-ideas/toolbox-contract": "0.3.0",
|
||||||
"@add-ideas/toolbox-helpers": "0.1.0",
|
"@add-ideas/toolbox-helpers": "0.2.0",
|
||||||
"@add-ideas/toolbox-shell-react": "0.2.3",
|
"@add-ideas/toolbox-shell-react": "0.3.0",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@add-ideas/toolbox-testkit": "0.2.3",
|
"@add-ideas/toolbox-testkit": "0.3.0",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@playwright/test": "1.62.1",
|
"@playwright/test": "1.62.1",
|
||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
@@ -42,24 +42,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-contract": {
|
"node_modules/@add-ideas/toolbox-contract": {
|
||||||
"version": "0.2.3",
|
"version": "0.3.0",
|
||||||
"license": "Apache-2.0",
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-contract/-/0.3.0/toolbox-contract-0.3.0.tgz",
|
||||||
"engines": {
|
"integrity": "sha512-dKrK7BjOFwqJaBfJuhKxZKIld4sH0AKjEn6a0yLnbdMUFY+fFv4VSLGV2tNSBD016gumc2iNqOjUj/ld7x4rtA==",
|
||||||
"node": ">=20"
|
"license": "Apache-2.0"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-helpers": {
|
"node_modules/@add-ideas/toolbox-helpers": {
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22"
|
"node": ">=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-shell-react": {
|
"node_modules/@add-ideas/toolbox-shell-react": {
|
||||||
"version": "0.2.3",
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-shell-react/-/0.3.0/toolbox-shell-react-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-74p6JzAOG0YCAKdlc1hLofV4ZIko7vb448S75cIiM88PKm93EHl5VD7g8YVyfM56Ui97UY9dmy+Whiq4sGzpsg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3"
|
"@add-ideas/toolbox-contract": "0.3.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=18 <20",
|
"react": ">=18 <20",
|
||||||
@@ -67,17 +68,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@add-ideas/toolbox-testkit": {
|
"node_modules/@add-ideas/toolbox-testkit": {
|
||||||
"version": "0.2.3",
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://git.add-ideas.de/api/packages/lotobo/npm/%40add-ideas%2Ftoolbox-testkit/-/0.3.0/toolbox-testkit-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-4Fk+oSvZFspOMIXr8Xy040nhAaBsIQAzsGyXWSpjn3+k3yBKq7nB1r5zCHhsXzfdLzvPDAx2KcmSNOhM330D9w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3"
|
"@add-ideas/toolbox-contract": "0.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"toolbox-check": "dist/cli.js"
|
"toolbox-check": "dist/cli.js"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "format-lab",
|
"name": "format-lab",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Explore conversion paths and measure format round-trip losses locally.",
|
"description": "Explore conversion paths and measure format round-trip losses locally.",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"author": "Albrecht Degering",
|
"author": "Albrecht Degering",
|
||||||
@@ -39,14 +39,14 @@
|
|||||||
"release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force"
|
"release:artifact": "npm run check && npm run test:browser && npm run package:release -- --force"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@add-ideas/toolbox-contract": "0.2.3",
|
"@add-ideas/toolbox-contract": "0.3.0",
|
||||||
"@add-ideas/toolbox-helpers": "0.1.0",
|
"@add-ideas/toolbox-helpers": "0.2.0",
|
||||||
"@add-ideas/toolbox-shell-react": "0.2.3",
|
"@add-ideas/toolbox-shell-react": "0.3.0",
|
||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8"
|
"react-dom": "19.2.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@add-ideas/toolbox-testkit": "0.2.3",
|
"@add-ideas/toolbox-testkit": "0.3.0",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@playwright/test": "1.62.1",
|
"@playwright/test": "1.62.1",
|
||||||
"@testing-library/jest-dom": "6.9.1",
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
|
|||||||
+20
-2
@@ -15,7 +15,25 @@ export default defineConfig({
|
|||||||
timeout: 180_000,
|
timeout: 180_000,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
{
|
||||||
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
name: "chromium",
|
||||||
|
testIgnore: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "firefox",
|
||||||
|
testIgnore: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Firefox"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "webkit",
|
||||||
|
testIgnore: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Desktop Safari"] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mobile-chromium",
|
||||||
|
testMatch: /responsive\.spec\.ts/,
|
||||||
|
use: { ...devices["Pixel 5"] },
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.0 - 2026-09-02
|
||||||
|
|
||||||
|
- Add shared evidence envelopes, a reusable format catalogue and bounded local
|
||||||
|
corpus runs for comparing measured round-trip behaviour across recipes.
|
||||||
|
|
||||||
## 0.1.0 - 2026-09-01
|
## 0.1.0 - 2026-09-01
|
||||||
|
|
||||||
- Add property-aware conversion graphs for data, image, audio/video, and
|
- Add property-aware conversion graphs for data, image, audio/video, and
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
==============================================================================
|
==============================================================================
|
||||||
@add-ideas/toolbox-contract@0.2.3
|
@add-ideas/toolbox-contract@0.3.0
|
||||||
Declared licence: Apache-2.0
|
Declared licence: Apache-2.0
|
||||||
==============================================================================
|
==============================================================================
|
||||||
--- LICENSE ---
|
--- LICENSE ---
|
||||||
@@ -198,7 +198,7 @@ Declared licence: Apache-2.0
|
|||||||
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
@add-ideas/toolbox-helpers@0.1.0
|
@add-ideas/toolbox-helpers@0.2.0
|
||||||
Declared licence: GPL-3.0-or-later
|
Declared licence: GPL-3.0-or-later
|
||||||
==============================================================================
|
==============================================================================
|
||||||
--- LICENSE ---
|
--- LICENSE ---
|
||||||
@@ -879,7 +879,7 @@ Public License instead of this License. But first, please read
|
|||||||
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
@add-ideas/toolbox-shell-react@0.2.3
|
@add-ideas/toolbox-shell-react@0.3.0
|
||||||
Declared licence: Apache-2.0
|
Declared licence: Apache-2.0
|
||||||
==============================================================================
|
==============================================================================
|
||||||
--- LICENSE ---
|
--- LICENSE ---
|
||||||
|
|||||||
+9
-2
@@ -4,7 +4,7 @@ Explore conversion paths, state the expected preservation boundary, and measure
|
|||||||
what a representative round trip actually changes. Format Lab is a local-first
|
what a representative round trip actually changes. Format Lab is a local-first
|
||||||
module in the add·ideas Toolbox.
|
module in the add·ideas Toolbox.
|
||||||
|
|
||||||
## v0.1.0 capabilities
|
## v0.2.0 capabilities
|
||||||
|
|
||||||
- Browse directed format graphs for structured data, images, audio/video, and
|
- Browse directed format graphs for structured data, images, audio/video, and
|
||||||
subtitles
|
subtitles
|
||||||
@@ -20,11 +20,14 @@ module in the add·ideas Toolbox.
|
|||||||
untested when it is outside the canonical model
|
untested when it is outside the canonical model
|
||||||
- Export the target data and a deterministic evidence report containing the
|
- Export the target data and a deterministic evidence report containing the
|
||||||
catalog version, exact path, expectations, measurements, and notices
|
catalog version, exact path, expectations, measurements, and notices
|
||||||
|
- Export a versioned portable evidence contract shared with other audit tools,
|
||||||
|
inspect a broad masked-offset file-signature catalog, and download a
|
||||||
|
deterministic edge-case corpus as individually named fixtures plus manifest
|
||||||
|
|
||||||
## Honest scope
|
## Honest scope
|
||||||
|
|
||||||
The image, audio/video, and subtitle graphs are planning contracts linked to the
|
The image, audio/video, and subtitle graphs are planning contracts linked to the
|
||||||
specialized Toolbox apps; v0.1.0 does not duplicate their codec or renderer
|
specialized Toolbox apps; v0.2.0 does not duplicate their codec or renderer
|
||||||
engines. Catalog statuses are conservative expectations, not measurements of a
|
engines. Catalog statuses are conservative expectations, not measurements of a
|
||||||
particular file or encoder. The executable data lab supports records only—not
|
particular file or encoder. The executable data lab supports records only—not
|
||||||
arbitrary JSON documents, XML mixed content/namespaces, or an external CSV type
|
arbitrary JSON documents, XML mixed content/namespaces, or an external CSV type
|
||||||
@@ -34,6 +37,10 @@ Inputs are capped at 2 MiB, 10,000 records, 200 fields per record, 500,000
|
|||||||
top-level values, and 8 MiB output. CSV cells are strings and spreadsheet-formula
|
top-level values, and 8 MiB output. CSV cells are strings and spreadsheet-formula
|
||||||
prefixes are neutralized; the resulting semantic change is visible in the
|
prefixes are neutralized; the resulting semantic change is visible in the
|
||||||
round-trip measurement.
|
round-trip measurement.
|
||||||
|
Signature matches identify byte patterns and likely containers only; they do
|
||||||
|
not validate an entire file or establish that its content is safe. Parquet,
|
||||||
|
Arrow, YAML, AVIF/HEIC/TIFF/GIF, Ogg/M4A/MOV/AAC and SBV/SAMI appear in the
|
||||||
|
planning catalog without inventing executable conversion support.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
# Corresponding source
|
# Corresponding source
|
||||||
|
|
||||||
The corresponding source for Format Lab 0.1.0 is available at:
|
The corresponding source for Format Lab 0.2.0 is available at:
|
||||||
|
|
||||||
https://git.add-ideas.de/lotobo/format-lab/src/tag/v0.1.0
|
https://git.add-ideas.de/lotobo/format-lab/src/tag/v0.2.0
|
||||||
|
|
||||||
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
|
Build with Node.js 22, npm 11, `npm ci`, and `npm run release:artifact`.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Third-party notices
|
# Third-party notices
|
||||||
|
|
||||||
Format Lab 0.1.0 directly depends on these runtime packages:
|
Format Lab 0.2.0 directly depends on these runtime packages:
|
||||||
|
|
||||||
| Package | Pinned version | Declared licence |
|
| Package | Pinned version | Declared licence |
|
||||||
| -------------------------------- | -------------: | ---------------- |
|
| -------------------------------- | -------------: | ---------------- |
|
||||||
| `@add-ideas/toolbox-contract` | 0.2.3 | Apache-2.0 |
|
| `@add-ideas/toolbox-contract` | 0.3.0 | Apache-2.0 |
|
||||||
| `@add-ideas/toolbox-helpers` | 0.1.0 | GPL-3.0-or-later |
|
| `@add-ideas/toolbox-helpers` | 0.2.0 | GPL-3.0-or-later |
|
||||||
| `@add-ideas/toolbox-shell-react` | 0.2.3 | Apache-2.0 |
|
| `@add-ideas/toolbox-shell-react` | 0.3.0 | Apache-2.0 |
|
||||||
| `react` | 19.2.8 | MIT |
|
| `react` | 19.2.8 | MIT |
|
||||||
| `react-dom` | 19.2.8 | MIT |
|
| `react-dom` | 19.2.8 | MIT |
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
const CACHE_PREFIX = "format-lab-shell-";
|
const CACHE_PREFIX = "format-lab-shell-";
|
||||||
const CACHE_NAME = CACHE_PREFIX + "0.1.0";
|
const CACHE_NAME = CACHE_PREFIX + "0.2.0";
|
||||||
const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"];
|
const CORE = ["./", "./manifest.webmanifest", "./favicon.svg"];
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
|
|||||||
+16
-1
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.format-lab",
|
"id": "de.add-ideas.format-lab",
|
||||||
"name": "Format Lab",
|
"name": "Format Lab",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Explore conversion paths and measure information loss locally.",
|
"description": "Explore conversion paths and measure information loss locally.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -28,6 +28,21 @@
|
|||||||
"crossOriginIsolated": false,
|
"crossOriginIsolated": false,
|
||||||
"topLevelContext": false
|
"topLevelContext": false
|
||||||
},
|
},
|
||||||
|
"io": {
|
||||||
|
"accepts": [
|
||||||
|
{ "mediaType": "application/json", "extensions": [".json"] },
|
||||||
|
{ "mediaType": "application/x-ndjson", "extensions": [".ndjson"] },
|
||||||
|
{ "mediaType": "text/csv", "extensions": [".csv"] },
|
||||||
|
{ "mediaType": "application/xml", "extensions": [".xml"] }
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
{ "mediaType": "application/json", "extensions": [".json"] },
|
||||||
|
{ "mediaType": "application/x-ndjson", "extensions": [".ndjson"] },
|
||||||
|
{ "mediaType": "text/csv", "extensions": [".csv"] },
|
||||||
|
{ "mediaType": "application/xml", "extensions": [".xml"] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": { "required": [], "optional": [] },
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"processing": "local",
|
"processing": "local",
|
||||||
"fileUploads": true,
|
"fileUploads": true,
|
||||||
|
|||||||
+103
-13
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState, type ChangeEvent } from "react";
|
import { useMemo, useState, type ChangeEvent } from "react";
|
||||||
import {
|
import {
|
||||||
stableStringify,
|
|
||||||
triggerBlobDownload,
|
triggerBlobDownload,
|
||||||
|
triggerBlobDownloads,
|
||||||
} from "@add-ideas/toolbox-helpers";
|
} from "@add-ideas/toolbox-helpers";
|
||||||
import {
|
import {
|
||||||
domains,
|
domains,
|
||||||
@@ -19,6 +19,13 @@ import {
|
|||||||
type DataFormat,
|
type DataFormat,
|
||||||
type RoundTripResult,
|
type RoundTripResult,
|
||||||
} from "../lab/data";
|
} from "../lab/data";
|
||||||
|
import { exportCorpusFiles } from "../lab/corpus";
|
||||||
|
import { serializeEvidence } from "../lab/evidence";
|
||||||
|
import {
|
||||||
|
formatSignatures,
|
||||||
|
identifyBySignature,
|
||||||
|
type FormatSignature,
|
||||||
|
} from "../lab/signatures";
|
||||||
|
|
||||||
const SAMPLE = `[
|
const SAMPLE = `[
|
||||||
{
|
{
|
||||||
@@ -159,6 +166,10 @@ export function Workbench() {
|
|||||||
"target",
|
"target",
|
||||||
);
|
);
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
|
const [signatureMatches, setSignatureMatches] = useState<FormatSignature[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [signatureName, setSignatureName] = useState("No file inspected");
|
||||||
|
|
||||||
const domainFormats = formats.filter((format) => format.domain === domain);
|
const domainFormats = formats.filter((format) => format.domain === domain);
|
||||||
const routes = useMemo(
|
const routes = useMemo(
|
||||||
@@ -172,18 +183,7 @@ export function Workbench() {
|
|||||||
domainFormats.some((format) => format.id === edge.to),
|
domainFormats.some((format) => format.id === edge.to),
|
||||||
);
|
);
|
||||||
const report = useMemo(
|
const report = useMemo(
|
||||||
() =>
|
() => serializeEvidence(record.result.evidence),
|
||||||
`${stableStringify(
|
|
||||||
{
|
|
||||||
catalogVersion: "format-catalog-v1",
|
|
||||||
route: record.route.formats,
|
|
||||||
expected: record.route.summary,
|
|
||||||
measured: record.result.measurements,
|
|
||||||
notes: record.result.notes,
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
{ maxTextChars: 2 * 1024 * 1024, maxDepth: 16, maxNodes: 20_000 },
|
|
||||||
)}\n`,
|
|
||||||
[record],
|
[record],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -240,6 +240,24 @@ export function Workbench() {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const inspectSignature = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
event.target.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
const bytes = new Uint8Array(await file.slice(0, 64).arrayBuffer());
|
||||||
|
setSignatureName(file.name);
|
||||||
|
setSignatureMatches(identifyBySignature(bytes));
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadCorpus = () =>
|
||||||
|
triggerBlobDownloads(
|
||||||
|
exportCorpusFiles().map((file) => ({
|
||||||
|
blob: new Blob([file.content], { type: file.mediaType }),
|
||||||
|
filename: file.path.replaceAll("/", "__"),
|
||||||
|
})),
|
||||||
|
{ order: "filename", maximumFiles: 20 },
|
||||||
|
);
|
||||||
|
|
||||||
const measuredByProperty = Object.fromEntries(
|
const measuredByProperty = Object.fromEntries(
|
||||||
record.result.measurements.map((measurement) => [
|
record.result.measurements.map((measurement) => [
|
||||||
measurement.property,
|
measurement.property,
|
||||||
@@ -408,6 +426,68 @@ export function Workbench() {
|
|||||||
</details>
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="panel" aria-labelledby="signature-title">
|
||||||
|
<div className="panel-heading">
|
||||||
|
<div>
|
||||||
|
<h2 id="signature-title">
|
||||||
|
Signature catalog and regression corpus
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{formatSignatures.length} bounded byte signatures identify likely
|
||||||
|
containers; they do not claim structural validity. {signatureName}
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="action-row">
|
||||||
|
<label className="button file-button">
|
||||||
|
Inspect first 64 bytes
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
onChange={(event) => void inspectSignature(event)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="button" onClick={downloadCorpus}>
|
||||||
|
Download fixture corpus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="notice-list">
|
||||||
|
{signatureMatches.length
|
||||||
|
? signatureMatches
|
||||||
|
.map(
|
||||||
|
(match) =>
|
||||||
|
`${match.label} (${match.confidence}: ${match.note})`,
|
||||||
|
)
|
||||||
|
.join(" · ")
|
||||||
|
: "No catalog signature matched the inspected prefix."}
|
||||||
|
</p>
|
||||||
|
<details>
|
||||||
|
<summary>Browse all signature definitions</summary>
|
||||||
|
<div className="table-scroll" tabIndex={0}>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Format</th>
|
||||||
|
<th>Offset</th>
|
||||||
|
<th>Confidence</th>
|
||||||
|
<th>Boundary</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{formatSignatures.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{item.label}</td>
|
||||||
|
<td>{item.offset}</td>
|
||||||
|
<td>{item.confidence}</td>
|
||||||
|
<td>{item.note}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="panel" aria-labelledby="lab-title">
|
<section className="panel" aria-labelledby="lab-title">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
<div>
|
<div>
|
||||||
@@ -510,6 +590,16 @@ export function Workbench() {
|
|||||||
Download evidence report
|
Download evidence report
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<details>
|
||||||
|
<summary>Portable evidence JSON</summary>
|
||||||
|
<textarea
|
||||||
|
aria-label="Evidence report"
|
||||||
|
readOnly
|
||||||
|
value={report}
|
||||||
|
rows={12}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,24 @@ export const formats: FormatDefinition[] = [
|
|||||||
domain: "data",
|
domain: "data",
|
||||||
note: "Flexible tree with namespaces, attributes and mixed content.",
|
note: "Flexible tree with namespaces, attributes and mixed content.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "yaml",
|
||||||
|
label: "YAML",
|
||||||
|
domain: "data",
|
||||||
|
note: "Typed graph-like syntax whose schema, tags, aliases and parser profile affect interoperability.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "parquet",
|
||||||
|
label: "Parquet",
|
||||||
|
domain: "data",
|
||||||
|
note: "Columnar typed records with schemas, encodings and optional statistics.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "arrow",
|
||||||
|
label: "Arrow IPC",
|
||||||
|
domain: "data",
|
||||||
|
note: "Typed columnar batches; extension metadata and dictionary handling matter.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "png",
|
id: "png",
|
||||||
label: "PNG",
|
label: "PNG",
|
||||||
@@ -128,6 +146,30 @@ export const formats: FormatDefinition[] = [
|
|||||||
domain: "image",
|
domain: "image",
|
||||||
note: "Vector/XML document with text and external-resource risks.",
|
note: "Vector/XML document with text and external-resource risks.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "gif",
|
||||||
|
label: "GIF",
|
||||||
|
domain: "image",
|
||||||
|
note: "Palette raster with binary transparency and frame animation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "avif",
|
||||||
|
label: "AVIF",
|
||||||
|
domain: "image",
|
||||||
|
note: "AV1 image sequences in ISO BMFF with HDR/alpha profile variation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tiff",
|
||||||
|
label: "TIFF",
|
||||||
|
domain: "image",
|
||||||
|
note: "Flexible tagged raster container with many compression and colour variants.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "heic",
|
||||||
|
label: "HEIC",
|
||||||
|
domain: "image",
|
||||||
|
note: "HEVC image collections in ISO BMFF; browser decode support varies.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "wav",
|
id: "wav",
|
||||||
label: "WAV",
|
label: "WAV",
|
||||||
@@ -170,6 +212,30 @@ export const formats: FormatDefinition[] = [
|
|||||||
domain: "av",
|
domain: "av",
|
||||||
note: "Flexible multi-track container with attachments and chapters.",
|
note: "Flexible multi-track container with attachments and chapters.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "ogg",
|
||||||
|
label: "Ogg",
|
||||||
|
domain: "av",
|
||||||
|
note: "Page-oriented container; actual preservation depends on codec streams and chaining.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "m4a",
|
||||||
|
label: "M4A",
|
||||||
|
domain: "av",
|
||||||
|
note: "ISO BMFF audio convention, commonly AAC or ALAC with metadata.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "mov",
|
||||||
|
label: "QuickTime MOV",
|
||||||
|
domain: "av",
|
||||||
|
note: "QuickTime/ISO BMFF family with broad codec and metadata options.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "aac",
|
||||||
|
label: "AAC (ADTS)",
|
||||||
|
domain: "av",
|
||||||
|
note: "Raw framed lossy audio with limited document-level metadata.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "srt",
|
id: "srt",
|
||||||
label: "SRT",
|
label: "SRT",
|
||||||
@@ -194,6 +260,18 @@ export const formats: FormatDefinition[] = [
|
|||||||
domain: "subtitle",
|
domain: "subtitle",
|
||||||
note: "XML timing and styling profiles vary by ecosystem.",
|
note: "XML timing and styling profiles vary by ecosystem.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "sbv",
|
||||||
|
label: "SBV",
|
||||||
|
domain: "subtitle",
|
||||||
|
note: "Simple timestamp pairs and text blocks with minimal styling.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sami",
|
||||||
|
label: "SAMI",
|
||||||
|
domain: "subtitle",
|
||||||
|
note: "HTML-like timed captions with CSS and legacy parser variation.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function statuses(
|
function statuses(
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { stableStringify } from "@add-ideas/toolbox-helpers";
|
||||||
|
|
||||||
|
export interface CorpusFile {
|
||||||
|
path: string;
|
||||||
|
mediaType: string;
|
||||||
|
purpose: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FORMAT_LAB_CORPUS: readonly CorpusFile[] = [
|
||||||
|
{
|
||||||
|
path: "structured/typed.json",
|
||||||
|
mediaType: "application/json",
|
||||||
|
purpose: "Scalar types, null, nesting, Unicode and formula-like text.",
|
||||||
|
content:
|
||||||
|
'[{"id":9007199254740991,"active":true,"missing":null,"nested":{"score":1.25},"text":"Café 🚲","formula":"=2+2"}]\n',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "structured/text-cells.csv",
|
||||||
|
mediaType: "text/csv",
|
||||||
|
purpose:
|
||||||
|
"Leading zeros, empty cells, literal null and spreadsheet formula risk.",
|
||||||
|
content: 'id,empty,literal,formula\r\n001,,null,"=2+2"\r\n',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "structured/records.ndjson",
|
||||||
|
mediaType: "application/x-ndjson",
|
||||||
|
purpose: "Record framing and non-ASCII text.",
|
||||||
|
content: '{"id":1,"text":"naïve"}\n{"id":2,"text":"東京"}\n',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "structured/typed.xml",
|
||||||
|
mediaType: "application/xml",
|
||||||
|
purpose: "Typed lab XML dialect and escaped characters.",
|
||||||
|
content:
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n<records><row><field name="id" type="number">1</field><field name="text" type="string">A & B</field></row></records>\n',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function corpusManifest(): string {
|
||||||
|
return `${stableStringify(
|
||||||
|
{
|
||||||
|
schema: "https://git.add-ideas.de/lotobo/format-lab/schema/corpus-v1",
|
||||||
|
corpusVersion: 1,
|
||||||
|
generator: "de.add-ideas.format-lab",
|
||||||
|
deterministic: true,
|
||||||
|
files: FORMAT_LAB_CORPUS.map((file) => ({
|
||||||
|
path: file.path,
|
||||||
|
mediaType: file.mediaType,
|
||||||
|
purpose: file.purpose,
|
||||||
|
utf8Bytes: new TextEncoder().encode(file.content).length,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
2,
|
||||||
|
)}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportCorpusFiles(): CorpusFile[] {
|
||||||
|
return [
|
||||||
|
...FORMAT_LAB_CORPUS.map((file) => ({ ...file })),
|
||||||
|
{
|
||||||
|
path: "corpus-manifest.json",
|
||||||
|
mediaType: "application/json",
|
||||||
|
purpose: "Machine-readable deterministic corpus inventory.",
|
||||||
|
content: corpusManifest(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
+37
-1
@@ -5,6 +5,7 @@ import {
|
|||||||
stringifyCsv,
|
stringifyCsv,
|
||||||
} from "@add-ideas/toolbox-helpers";
|
} from "@add-ideas/toolbox-helpers";
|
||||||
import type { ConversionRoute } from "./catalog";
|
import type { ConversionRoute } from "./catalog";
|
||||||
|
import { createEvidenceReport, type EvidenceReport } from "./evidence";
|
||||||
|
|
||||||
export type DataFormat = "json" | "ndjson" | "csv" | "xml";
|
export type DataFormat = "json" | "ndjson" | "csv" | "xml";
|
||||||
export type MeasurementStatus =
|
export type MeasurementStatus =
|
||||||
@@ -29,6 +30,7 @@ export interface RoundTripResult {
|
|||||||
roundTrip: DataDocument;
|
roundTrip: DataDocument;
|
||||||
measurements: PropertyMeasurement[];
|
measurements: PropertyMeasurement[];
|
||||||
notes: string[];
|
notes: string[];
|
||||||
|
evidence: EvidenceReport;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
||||||
@@ -445,13 +447,47 @@ export function runRoundTrip(
|
|||||||
(note) => `Round-trip ${sourceFormat.toUpperCase()}: ${note}`,
|
(note) => `Round-trip ${sourceFormat.toUpperCase()}: ${note}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const measurements = measureData(original, roundTrip);
|
||||||
|
const evidence = createEvidenceReport({
|
||||||
|
generator: { id: "de.add-ideas.format-lab", version: "0.2.0" },
|
||||||
|
subject: {
|
||||||
|
kind: "format-round-trip",
|
||||||
|
name: route.formats.join(" → "),
|
||||||
|
},
|
||||||
|
operation: {
|
||||||
|
id: "structured-round-trip",
|
||||||
|
parameters: {
|
||||||
|
route: [...route.formats],
|
||||||
|
expected: { ...route.summary },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
maximumInputBytes: MAX_SOURCE_BYTES,
|
||||||
|
maximumOutputBytes: MAX_OUTPUT_BYTES,
|
||||||
|
maximumRows: MAX_ROWS,
|
||||||
|
maximumColumns: MAX_COLUMNS,
|
||||||
|
maximumValues: MAX_VALUES,
|
||||||
|
},
|
||||||
|
observations: measurements.map((measurement) => ({
|
||||||
|
id: `property:${measurement.property}`,
|
||||||
|
status:
|
||||||
|
measurement.status === "measured-preserved"
|
||||||
|
? "pass"
|
||||||
|
: measurement.status === "measured-changed"
|
||||||
|
? "change"
|
||||||
|
: "not-tested",
|
||||||
|
expectation: route.summary[measurement.property] ?? "unknown",
|
||||||
|
detail: measurement.detail,
|
||||||
|
})),
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
route: route.formats,
|
route: route.formats,
|
||||||
targetText,
|
targetText,
|
||||||
roundTripText,
|
roundTripText,
|
||||||
original,
|
original,
|
||||||
roundTrip,
|
roundTrip,
|
||||||
measurements: measureData(original, roundTrip),
|
measurements,
|
||||||
notes: [...new Set(notes)],
|
notes: [...new Set(notes)],
|
||||||
|
evidence,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { safeJsonParse, stableStringify } from "@add-ideas/toolbox-helpers";
|
||||||
|
|
||||||
|
export const EVIDENCE_SCHEMA =
|
||||||
|
"https://git.add-ideas.de/lotobo/format-lab/schema/evidence-v1" as const;
|
||||||
|
|
||||||
|
export type EvidenceStatus = "pass" | "change" | "not-tested" | "informational";
|
||||||
|
|
||||||
|
export interface EvidenceObservation {
|
||||||
|
id: string;
|
||||||
|
status: EvidenceStatus;
|
||||||
|
expectation?: string;
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvidenceReport {
|
||||||
|
schema: typeof EVIDENCE_SCHEMA;
|
||||||
|
contractVersion: 1;
|
||||||
|
generator: { id: string; version: string };
|
||||||
|
subject: { kind: string; name: string; mediaType?: string };
|
||||||
|
operation: { id: string; parameters: Record<string, unknown> };
|
||||||
|
limits: Record<string, number | string | boolean>;
|
||||||
|
observations: EvidenceObservation[];
|
||||||
|
provenance: {
|
||||||
|
execution: "local-browser";
|
||||||
|
networkRequired: false;
|
||||||
|
recordedAt: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEvidenceReport(
|
||||||
|
input: Omit<EvidenceReport, "schema" | "contractVersion" | "provenance"> & {
|
||||||
|
recordedAt?: string | null;
|
||||||
|
},
|
||||||
|
): EvidenceReport {
|
||||||
|
if (!input.generator.id || !input.generator.version)
|
||||||
|
throw new TypeError("Evidence generator identity is required.");
|
||||||
|
if (!input.observations.length || input.observations.length > 1_000)
|
||||||
|
throw new RangeError("Evidence reports need 1–1,000 observations.");
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const observation of input.observations) {
|
||||||
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/u.test(observation.id))
|
||||||
|
throw new TypeError(`Invalid evidence observation id ${observation.id}.`);
|
||||||
|
if (ids.has(observation.id))
|
||||||
|
throw new TypeError(
|
||||||
|
`Duplicate evidence observation id ${observation.id}.`,
|
||||||
|
);
|
||||||
|
ids.add(observation.id);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schema: EVIDENCE_SCHEMA,
|
||||||
|
contractVersion: 1,
|
||||||
|
generator: { ...input.generator },
|
||||||
|
subject: { ...input.subject },
|
||||||
|
operation: {
|
||||||
|
id: input.operation.id,
|
||||||
|
parameters: structuredClone(input.operation.parameters),
|
||||||
|
},
|
||||||
|
limits: { ...input.limits },
|
||||||
|
observations: input.observations.map((item) => ({ ...item })),
|
||||||
|
provenance: {
|
||||||
|
execution: "local-browser",
|
||||||
|
networkRequired: false,
|
||||||
|
recordedAt: input.recordedAt ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeEvidence(report: EvidenceReport): string {
|
||||||
|
return `${stableStringify(report, 2, {
|
||||||
|
maxTextChars: 2 * 1024 * 1024,
|
||||||
|
maxDepth: 24,
|
||||||
|
maxNodes: 20_000,
|
||||||
|
})}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseEvidence(source: string): EvidenceReport {
|
||||||
|
const value = safeJsonParse(source, {
|
||||||
|
maxTextChars: 2 * 1024 * 1024,
|
||||||
|
maxDepth: 24,
|
||||||
|
maxNodes: 20_000,
|
||||||
|
rejectDangerousKeys: true,
|
||||||
|
}) as unknown;
|
||||||
|
if (!value || Array.isArray(value) || typeof value !== "object")
|
||||||
|
throw new TypeError("Evidence must be a JSON object.");
|
||||||
|
const candidate = value as Partial<EvidenceReport>;
|
||||||
|
if (
|
||||||
|
candidate.schema !== EVIDENCE_SCHEMA ||
|
||||||
|
candidate.contractVersion !== 1 ||
|
||||||
|
!candidate.generator ||
|
||||||
|
!candidate.subject ||
|
||||||
|
!candidate.operation ||
|
||||||
|
!candidate.limits ||
|
||||||
|
!Array.isArray(candidate.observations) ||
|
||||||
|
!candidate.provenance ||
|
||||||
|
candidate.provenance.execution !== "local-browser" ||
|
||||||
|
candidate.provenance.networkRequired !== false
|
||||||
|
)
|
||||||
|
throw new TypeError("Unsupported or incomplete format evidence contract.");
|
||||||
|
return createEvidenceReport({
|
||||||
|
generator: candidate.generator,
|
||||||
|
subject: candidate.subject,
|
||||||
|
operation: candidate.operation,
|
||||||
|
limits: candidate.limits,
|
||||||
|
observations: candidate.observations,
|
||||||
|
recordedAt: candidate.provenance.recordedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
export interface FormatSignature {
|
||||||
|
id: string;
|
||||||
|
format: string;
|
||||||
|
label: string;
|
||||||
|
extensions: string[];
|
||||||
|
mediaTypes: string[];
|
||||||
|
offset: number;
|
||||||
|
bytes: readonly number[];
|
||||||
|
mask?: readonly number[];
|
||||||
|
confidence: "strong" | "container" | "heuristic";
|
||||||
|
note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = (
|
||||||
|
value: Omit<FormatSignature, "id"> & { id?: string },
|
||||||
|
): FormatSignature => ({
|
||||||
|
id: value.id ?? `${value.format}@${value.offset}`,
|
||||||
|
...value,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Byte signatures are identification hints, never a validation claim. */
|
||||||
|
export const formatSignatures: readonly FormatSignature[] = [
|
||||||
|
signature({
|
||||||
|
format: "png",
|
||||||
|
label: "PNG",
|
||||||
|
extensions: ["png"],
|
||||||
|
mediaTypes: ["image/png"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "Complete PNG signature.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "jpeg",
|
||||||
|
label: "JPEG",
|
||||||
|
extensions: ["jpg", "jpeg"],
|
||||||
|
mediaTypes: ["image/jpeg"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0xff, 0xd8, 0xff],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "Start-of-image plus following marker prefix.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "gif",
|
||||||
|
label: "GIF87a",
|
||||||
|
extensions: ["gif"],
|
||||||
|
mediaTypes: ["image/gif"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "GIF87a header.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
id: "gif89a@0",
|
||||||
|
format: "gif",
|
||||||
|
label: "GIF89a",
|
||||||
|
extensions: ["gif"],
|
||||||
|
mediaTypes: ["image/gif"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "GIF89a header.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "webp",
|
||||||
|
label: "WebP",
|
||||||
|
extensions: ["webp"],
|
||||||
|
mediaTypes: ["image/webp"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50],
|
||||||
|
mask: [255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "RIFF with WEBP form type; length bytes masked.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "tiff-le",
|
||||||
|
label: "TIFF little-endian",
|
||||||
|
extensions: ["tif", "tiff"],
|
||||||
|
mediaTypes: ["image/tiff"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x49, 0x49, 0x2a, 0x00],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "Classic little-endian TIFF.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "tiff-be",
|
||||||
|
label: "TIFF big-endian",
|
||||||
|
extensions: ["tif", "tiff"],
|
||||||
|
mediaTypes: ["image/tiff"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x4d, 0x4d, 0x00, 0x2a],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "Classic big-endian TIFF.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "bmp",
|
||||||
|
label: "BMP",
|
||||||
|
extensions: ["bmp"],
|
||||||
|
mediaTypes: ["image/bmp"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x42, 0x4d],
|
||||||
|
confidence: "heuristic",
|
||||||
|
note: "Bitmap file marker; structural validation still required.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "pdf",
|
||||||
|
label: "PDF",
|
||||||
|
extensions: ["pdf"],
|
||||||
|
mediaTypes: ["application/pdf"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x25, 0x50, 0x44, 0x46, 0x2d],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "%PDF- header.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "zip",
|
||||||
|
label: "ZIP-compatible container",
|
||||||
|
extensions: ["zip", "docx", "xlsx", "pptx", "epub", "odt", "ods", "odp"],
|
||||||
|
mediaTypes: ["application/zip"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x50, 0x4b, 0x03, 0x04],
|
||||||
|
confidence: "container",
|
||||||
|
note: "Container only; entries distinguish ZIP-based formats.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
id: "zip-empty@0",
|
||||||
|
format: "zip",
|
||||||
|
label: "Empty ZIP",
|
||||||
|
extensions: ["zip"],
|
||||||
|
mediaTypes: ["application/zip"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x50, 0x4b, 0x05, 0x06],
|
||||||
|
confidence: "container",
|
||||||
|
note: "Empty archive end record.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "gzip",
|
||||||
|
label: "Gzip",
|
||||||
|
extensions: ["gz", "tgz"],
|
||||||
|
mediaTypes: ["application/gzip"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x1f, 0x8b, 0x08],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "Gzip header with deflate method.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "bzip2",
|
||||||
|
label: "Bzip2",
|
||||||
|
extensions: ["bz2"],
|
||||||
|
mediaTypes: ["application/x-bzip2"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x42, 0x5a, 0x68],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "BZip2 stream prefix.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "xz",
|
||||||
|
label: "XZ",
|
||||||
|
extensions: ["xz"],
|
||||||
|
mediaTypes: ["application/x-xz"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "XZ stream header.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "7z",
|
||||||
|
label: "7-Zip",
|
||||||
|
extensions: ["7z"],
|
||||||
|
mediaTypes: ["application/x-7z-compressed"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "7z signature.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "rar4",
|
||||||
|
label: "RAR 4",
|
||||||
|
extensions: ["rar"],
|
||||||
|
mediaTypes: ["application/vnd.rar"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "RAR 1.5–4.x signature.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "rar5",
|
||||||
|
label: "RAR 5",
|
||||||
|
extensions: ["rar"],
|
||||||
|
mediaTypes: ["application/vnd.rar"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "RAR 5 signature.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "wasm",
|
||||||
|
label: "WebAssembly",
|
||||||
|
extensions: ["wasm"],
|
||||||
|
mediaTypes: ["application/wasm"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x00, 0x61, 0x73, 0x6d],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "WebAssembly magic; version follows.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "elf",
|
||||||
|
label: "ELF",
|
||||||
|
extensions: [],
|
||||||
|
mediaTypes: ["application/x-elf"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x7f, 0x45, 0x4c, 0x46],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "ELF identification.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "sqlite",
|
||||||
|
label: "SQLite 3",
|
||||||
|
extensions: ["sqlite", "db"],
|
||||||
|
mediaTypes: ["application/vnd.sqlite3"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [
|
||||||
|
0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61,
|
||||||
|
0x74, 0x20, 0x33, 0x00,
|
||||||
|
],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "SQLite 3 database header.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "flac",
|
||||||
|
label: "FLAC",
|
||||||
|
extensions: ["flac"],
|
||||||
|
mediaTypes: ["audio/flac"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x66, 0x4c, 0x61, 0x43],
|
||||||
|
confidence: "strong",
|
||||||
|
note: "Native FLAC marker.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "ogg",
|
||||||
|
label: "Ogg",
|
||||||
|
extensions: ["ogg", "oga", "ogv", "opus"],
|
||||||
|
mediaTypes: ["application/ogg"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x4f, 0x67, 0x67, 0x53],
|
||||||
|
confidence: "container",
|
||||||
|
note: "Ogg page marker; codec requires packet inspection.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "wav",
|
||||||
|
label: "WAVE",
|
||||||
|
extensions: ["wav"],
|
||||||
|
mediaTypes: ["audio/wav"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45],
|
||||||
|
mask: [255, 255, 255, 255, 0, 0, 0, 0, 255, 255, 255, 255],
|
||||||
|
confidence: "container",
|
||||||
|
note: "RIFF with WAVE form type.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "matroska",
|
||||||
|
label: "Matroska/WebM",
|
||||||
|
extensions: ["mkv", "mka", "webm"],
|
||||||
|
mediaTypes: ["video/x-matroska", "video/webm"],
|
||||||
|
offset: 0,
|
||||||
|
bytes: [0x1a, 0x45, 0xdf, 0xa3],
|
||||||
|
confidence: "container",
|
||||||
|
note: "EBML header; DocType distinguishes Matroska/WebM.",
|
||||||
|
}),
|
||||||
|
signature({
|
||||||
|
format: "iso-bmff",
|
||||||
|
label: "ISO Base Media",
|
||||||
|
extensions: ["mp4", "m4a", "mov", "avif", "heic"],
|
||||||
|
mediaTypes: ["video/mp4"],
|
||||||
|
offset: 4,
|
||||||
|
bytes: [0x66, 0x74, 0x79, 0x70],
|
||||||
|
confidence: "container",
|
||||||
|
note: "ftyp box marker; compatible brands distinguish formats.",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export function identifyBySignature(bytes: Uint8Array): FormatSignature[] {
|
||||||
|
return formatSignatures.filter((item) =>
|
||||||
|
item.bytes.every((expected, index) => {
|
||||||
|
const actual = bytes[item.offset + index];
|
||||||
|
const mask = item.mask?.[index] ?? 0xff;
|
||||||
|
return actual !== undefined && (actual & mask) === (expected & mask);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -531,6 +531,12 @@ tbody tr:last-child > * {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-grid > *,
|
||||||
|
.explorer-grid > *,
|
||||||
|
.lab-grid > * {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.selection-grid {
|
.selection-grid {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
margin-bottom: 0.85rem;
|
margin-bottom: 0.85rem;
|
||||||
@@ -642,10 +648,12 @@ tbody tr:last-child > * {
|
|||||||
|
|
||||||
.route-statuses small,
|
.route-statuses small,
|
||||||
.status {
|
.status {
|
||||||
|
max-width: 100%;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.22rem 0.42rem;
|
padding: 0.22rem 0.42rem;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expected-preserved,
|
.expected-preserved,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.format-lab",
|
"id": "de.add-ideas.format-lab",
|
||||||
"name": "Format Lab",
|
"name": "Format Lab",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Explore conversion paths and measure information loss locally.",
|
"description": "Explore conversion paths and measure information loss locally.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -28,6 +28,48 @@
|
|||||||
"crossOriginIsolated": false,
|
"crossOriginIsolated": false,
|
||||||
"topLevelContext": false
|
"topLevelContext": false
|
||||||
},
|
},
|
||||||
|
"io": {
|
||||||
|
"accepts": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"extensions": [".json"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/x-ndjson",
|
||||||
|
"extensions": [".ndjson"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "text/csv",
|
||||||
|
"extensions": [".csv"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/xml",
|
||||||
|
"extensions": [".xml"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"extensions": [".json"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/x-ndjson",
|
||||||
|
"extensions": [".ndjson"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "text/csv",
|
||||||
|
"extensions": [".csv"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/xml",
|
||||||
|
"extensions": [".xml"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"required": [],
|
||||||
|
"optional": []
|
||||||
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"processing": "local",
|
"processing": "local",
|
||||||
"fileUploads": true,
|
"fileUploads": true,
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const APP_VERSION = "0.1.0";
|
export const APP_VERSION = "0.2.0";
|
||||||
|
|||||||
@@ -83,6 +83,25 @@ test("shows CSV losses and retains evidence after invalid source", async ({
|
|||||||
await expect(output).toHaveValue(previous);
|
await expect(output).toHaveValue(previous);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("identifies bounded signatures and exposes the portable evidence contract", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/deep/nested/format-lab/");
|
||||||
|
await page
|
||||||
|
.locator('input[type="file"]')
|
||||||
|
.first()
|
||||||
|
.setInputFiles("tests/fixtures/signature.pdf");
|
||||||
|
await expect(page.getByText(/PDF \(strong: %PDF- header/u)).toBeVisible();
|
||||||
|
await page.getByText("Portable evidence JSON", { exact: true }).click();
|
||||||
|
const evidence = await page
|
||||||
|
.getByRole("textbox", { name: "Evidence report", exact: true })
|
||||||
|
.inputValue();
|
||||||
|
expect(JSON.parse(evidence)).toMatchObject({
|
||||||
|
contractVersion: 1,
|
||||||
|
provenance: { execution: "local-browser", networkRequired: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("integrates help, themes, identity, headers and offline reload", async ({
|
test("integrates help, themes, identity, headers and offline reload", async ({
|
||||||
page,
|
page,
|
||||||
request,
|
request,
|
||||||
@@ -115,7 +134,7 @@ test("integrates help, themes, identity, headers and offline reload", async ({
|
|||||||
);
|
);
|
||||||
await expect(manifest.json()).resolves.toMatchObject({
|
await expect(manifest.json()).resolves.toMatchObject({
|
||||||
id: "de.add-ideas.format-lab",
|
id: "de.add-ideas.format-lab",
|
||||||
version: "0.1.0",
|
version: "0.2.0",
|
||||||
privacy: { processing: "local", telemetry: false },
|
privacy: { processing: "local", telemetry: false },
|
||||||
});
|
});
|
||||||
await context.setOffline(true);
|
await context.setOffline(true);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("keeps the primary workspace inside a narrow viewport", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/deep/nested/format-lab/");
|
||||||
|
await expect(page.locator("main").first()).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.locator("main .loading, main .workbench-loading"),
|
||||||
|
).toHaveCount(0);
|
||||||
|
|
||||||
|
const widths = await page.evaluate(() => ({
|
||||||
|
content: document.documentElement.scrollWidth,
|
||||||
|
viewport: document.documentElement.clientWidth,
|
||||||
|
}));
|
||||||
|
expect(widths.viewport).toBeLessThanOrEqual(430);
|
||||||
|
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
|
||||||
|
});
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
%PDF-1.7
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { findRoutes, properties } from "../../src/lab/catalog";
|
import { findRoutes, properties } from "../../src/lab/catalog";
|
||||||
import { parseData, runRoundTrip, serializeData } from "../../src/lab/data";
|
import { parseData, runRoundTrip, serializeData } from "../../src/lab/data";
|
||||||
|
import { parseEvidence, serializeEvidence } from "../../src/lab/evidence";
|
||||||
|
|
||||||
const all = properties.data.map((property) => property.id);
|
const all = properties.data.map((property) => property.id);
|
||||||
const sample = JSON.stringify([
|
const sample = JSON.stringify([
|
||||||
@@ -29,6 +30,13 @@ describe("structured data lab", () => {
|
|||||||
property: "metadata",
|
property: "metadata",
|
||||||
status: "not-tested",
|
status: "not-tested",
|
||||||
});
|
});
|
||||||
|
expect(parseEvidence(serializeEvidence(result.evidence))).toEqual(
|
||||||
|
result.evidence,
|
||||||
|
);
|
||||||
|
expect(result.evidence).toMatchObject({
|
||||||
|
contractVersion: 1,
|
||||||
|
provenance: { execution: "local-browser", networkRequired: false },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes CSV type, null and nested-value changes", () => {
|
it("exposes CSV type, null and nested-value changes", () => {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { exportCorpusFiles } from "../../src/lab/corpus";
|
||||||
|
import {
|
||||||
|
formatSignatures,
|
||||||
|
identifyBySignature,
|
||||||
|
} from "../../src/lab/signatures";
|
||||||
|
|
||||||
|
describe("format signatures and corpus", () => {
|
||||||
|
it("distinguishes masked RIFF forms and container-only evidence", () => {
|
||||||
|
const wav = Uint8Array.of(
|
||||||
|
0x52,
|
||||||
|
0x49,
|
||||||
|
0x46,
|
||||||
|
0x46,
|
||||||
|
0x24,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0x57,
|
||||||
|
0x41,
|
||||||
|
0x56,
|
||||||
|
0x45,
|
||||||
|
);
|
||||||
|
expect(identifyBySignature(wav).map((item) => item.format)).toContain(
|
||||||
|
"wav",
|
||||||
|
);
|
||||||
|
expect(identifyBySignature(wav).map((item) => item.format)).not.toContain(
|
||||||
|
"webp",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
identifyBySignature(Uint8Array.of(0x50, 0x4b, 0x03, 0x04))[0],
|
||||||
|
).toMatchObject({ format: "zip", confidence: "container" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ships a broad, deterministic structured regression corpus", () => {
|
||||||
|
expect(formatSignatures.length).toBeGreaterThanOrEqual(20);
|
||||||
|
const first = exportCorpusFiles();
|
||||||
|
expect(first).toEqual(exportCorpusFiles());
|
||||||
|
expect(first.map((file) => file.path)).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"structured/typed.json",
|
||||||
|
"structured/text-cells.csv",
|
||||||
|
"corpus-manifest.json",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user