@@ -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 stable retry voting, multi-file cases, adapter-defined predicates and
|
||||||
|
exported reproducibility evidence for flaky or structured failures.
|
||||||
|
|
||||||
## 0.1.0 - 2026-09-01
|
## 0.1.0 - 2026-09-01
|
||||||
|
|
||||||
- Add bounded structure-aware JSON/XML and textual delta debugging
|
- Add bounded structure-aware JSON/XML and textual delta debugging
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Reduce a failing input while repeatedly proving that the selected failure still
|
|||||||
occurs. Minimize Tools is a local-first delta-debugging module in the add·ideas
|
occurs. Minimize Tools is a local-first delta-debugging module in the add·ideas
|
||||||
Toolbox.
|
Toolbox.
|
||||||
|
|
||||||
## v0.1.0 capabilities
|
## v0.2.0 capabilities
|
||||||
|
|
||||||
- Minimize plain text by lines, lexical tokens, and Unicode code points
|
- Minimize plain text by lines, lexical tokens, and Unicode code points
|
||||||
- Apply structure-aware reductions before textual delta debugging for bounded
|
- Apply structure-aware reductions before textual delta debugging for bounded
|
||||||
@@ -18,6 +18,11 @@ Toolbox.
|
|||||||
timeout; cancel any run and retain the last complete result
|
timeout; cancel any run and retain the last complete result
|
||||||
- Enforce explicit predicate-test and wall-time budgets and show every accepted
|
- Enforce explicit predicate-test and wall-time budgets and show every accepted
|
||||||
size reduction
|
size reduction
|
||||||
|
- Sample predicates with single, majority or strict stability profiles and
|
||||||
|
report observed flaky candidates rather than silently trusting one run
|
||||||
|
- Minimize bounded multi-file bundles first by file membership and then by
|
||||||
|
structure-aware file content, using deterministic JSON-map, delimited or
|
||||||
|
primary-file adapter profiles
|
||||||
- Export the minimized case and a deterministic report containing the predicate,
|
- Export the minimized case and a deterministic report containing the predicate,
|
||||||
engine version, limits, and reduction trace
|
engine version, limits, and reduction trace
|
||||||
|
|
||||||
@@ -34,6 +39,9 @@ Minimization finds a locally minimal case under the attempted transformations
|
|||||||
and budget, not a mathematically globally shortest input. Regex timing depends on
|
and budget, not a mathematically globally shortest input. Regex timing depends on
|
||||||
the browser and device. Inputs are capped at 2 MiB, runs at 2,000 tests and 30
|
the browser and device. Inputs are capped at 2 MiB, runs at 2,000 tests and 30
|
||||||
seconds, regex worker execution at one second, and stylesheets at 128 KiB.
|
seconds, regex worker execution at one second, and stylesheets at 128 KiB.
|
||||||
|
Bundles are limited to 100 files, 4 MiB total and 2 MiB per file. Adapter
|
||||||
|
profiles serialize bundle candidates for an existing predicate; they do not
|
||||||
|
emulate a filesystem or execute a target program.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Corresponding source
|
# Corresponding source
|
||||||
|
|
||||||
The corresponding source for Minimize Tools 0.1.0 is available at:
|
The corresponding source for Minimize Tools 0.2.0 is available at:
|
||||||
|
|
||||||
https://git.add-ideas.de/lotobo/minimize-tools/src/tag/v0.1.0
|
https://git.add-ideas.de/lotobo/minimize-tools/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
|
||||||
|
|
||||||
Minimize Tools 0.1.0 directly depends on these runtime packages:
|
Minimize Tools 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": "minimize-tools",
|
"name": "minimize-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "minimize-tools",
|
"name": "minimize-tools",
|
||||||
"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": "minimize-tools",
|
"name": "minimize-tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Reduce failing inputs while preserving a bounded local failure predicate.",
|
"description": "Reduce failing inputs while preserving a bounded local failure predicate.",
|
||||||
"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 stable retry voting, multi-file cases, adapter-defined predicates and
|
||||||
|
exported reproducibility evidence for flaky or structured failures.
|
||||||
|
|
||||||
## 0.1.0 - 2026-09-01
|
## 0.1.0 - 2026-09-01
|
||||||
|
|
||||||
- Add bounded structure-aware JSON/XML and textual delta debugging
|
- Add bounded structure-aware JSON/XML and textual delta debugging
|
||||||
|
|||||||
@@ -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
-1
@@ -4,7 +4,7 @@ Reduce a failing input while repeatedly proving that the selected failure still
|
|||||||
occurs. Minimize Tools is a local-first delta-debugging module in the add·ideas
|
occurs. Minimize Tools is a local-first delta-debugging module in the add·ideas
|
||||||
Toolbox.
|
Toolbox.
|
||||||
|
|
||||||
## v0.1.0 capabilities
|
## v0.2.0 capabilities
|
||||||
|
|
||||||
- Minimize plain text by lines, lexical tokens, and Unicode code points
|
- Minimize plain text by lines, lexical tokens, and Unicode code points
|
||||||
- Apply structure-aware reductions before textual delta debugging for bounded
|
- Apply structure-aware reductions before textual delta debugging for bounded
|
||||||
@@ -18,6 +18,11 @@ Toolbox.
|
|||||||
timeout; cancel any run and retain the last complete result
|
timeout; cancel any run and retain the last complete result
|
||||||
- Enforce explicit predicate-test and wall-time budgets and show every accepted
|
- Enforce explicit predicate-test and wall-time budgets and show every accepted
|
||||||
size reduction
|
size reduction
|
||||||
|
- Sample predicates with single, majority or strict stability profiles and
|
||||||
|
report observed flaky candidates rather than silently trusting one run
|
||||||
|
- Minimize bounded multi-file bundles first by file membership and then by
|
||||||
|
structure-aware file content, using deterministic JSON-map, delimited or
|
||||||
|
primary-file adapter profiles
|
||||||
- Export the minimized case and a deterministic report containing the predicate,
|
- Export the minimized case and a deterministic report containing the predicate,
|
||||||
engine version, limits, and reduction trace
|
engine version, limits, and reduction trace
|
||||||
|
|
||||||
@@ -34,6 +39,9 @@ Minimization finds a locally minimal case under the attempted transformations
|
|||||||
and budget, not a mathematically globally shortest input. Regex timing depends on
|
and budget, not a mathematically globally shortest input. Regex timing depends on
|
||||||
the browser and device. Inputs are capped at 2 MiB, runs at 2,000 tests and 30
|
the browser and device. Inputs are capped at 2 MiB, runs at 2,000 tests and 30
|
||||||
seconds, regex worker execution at one second, and stylesheets at 128 KiB.
|
seconds, regex worker execution at one second, and stylesheets at 128 KiB.
|
||||||
|
Bundles are limited to 100 files, 4 MiB total and 2 MiB per file. Adapter
|
||||||
|
profiles serialize bundle candidates for an existing predicate; they do not
|
||||||
|
emulate a filesystem or execute a target program.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
# Corresponding source
|
# Corresponding source
|
||||||
|
|
||||||
The corresponding source for Minimize Tools 0.1.0 is available at:
|
The corresponding source for Minimize Tools 0.2.0 is available at:
|
||||||
|
|
||||||
https://git.add-ideas.de/lotobo/minimize-tools/src/tag/v0.1.0
|
https://git.add-ideas.de/lotobo/minimize-tools/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
|
||||||
|
|
||||||
Minimize Tools 0.1.0 directly depends on these runtime packages:
|
Minimize Tools 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 = "minimize-tools-shell-";
|
const CACHE_PREFIX = "minimize-tools-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(
|
||||||
|
|||||||
+19
-2
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.minimize-tools",
|
"id": "de.add-ideas.minimize-tools",
|
||||||
"name": "Minimize Tools",
|
"name": "Minimize Tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Reduce failing inputs while preserving the failure locally.",
|
"description": "Reduce failing inputs while preserving the failure locally.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -23,11 +23,28 @@
|
|||||||
},
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"secureContext": false,
|
"secureContext": false,
|
||||||
"workers": true,
|
"workers": false,
|
||||||
"indexedDb": false,
|
"indexedDb": false,
|
||||||
"crossOriginIsolated": false,
|
"crossOriginIsolated": false,
|
||||||
"topLevelContext": false
|
"topLevelContext": false
|
||||||
},
|
},
|
||||||
|
"io": {
|
||||||
|
"accepts": [
|
||||||
|
{ "mediaType": "text/plain", "extensions": [".txt"] },
|
||||||
|
{ "mediaType": "application/json", "extensions": [".json"] },
|
||||||
|
{
|
||||||
|
"mediaType": "application/xml",
|
||||||
|
"extensions": [".xml", ".xsl", ".xslt"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
{ "mediaType": "text/plain", "extensions": [".txt"] },
|
||||||
|
{ "mediaType": "application/json", "extensions": [".json"] },
|
||||||
|
{ "mediaType": "application/xml", "extensions": [".xml"] },
|
||||||
|
{ "mediaType": "application/zip", "extensions": [".zip"] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": { "required": [], "optional": ["workers"] },
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"processing": "local",
|
"processing": "local",
|
||||||
"fileUploads": true,
|
"fileUploads": true,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMemo, useRef, useState, type ChangeEvent } from "react";
|
|||||||
import {
|
import {
|
||||||
stableStringify,
|
stableStringify,
|
||||||
triggerBlobDownload,
|
triggerBlobDownload,
|
||||||
|
triggerBlobDownloads,
|
||||||
} from "@add-ideas/toolbox-helpers";
|
} from "@add-ideas/toolbox-helpers";
|
||||||
import {
|
import {
|
||||||
byteLength,
|
byteLength,
|
||||||
@@ -9,7 +10,15 @@ import {
|
|||||||
type InputStructure,
|
type InputStructure,
|
||||||
type MinimizeProgress,
|
type MinimizeProgress,
|
||||||
type MinimizeResult,
|
type MinimizeResult,
|
||||||
|
type PredicateStability,
|
||||||
} from "../minimize/engine";
|
} from "../minimize/engine";
|
||||||
|
import {
|
||||||
|
bundlePredicateAdapter,
|
||||||
|
minimizeBundle,
|
||||||
|
type BundleAdapterProfile,
|
||||||
|
type BundleFile,
|
||||||
|
type BundleMinimizeResult,
|
||||||
|
} from "../minimize/bundle";
|
||||||
import {
|
import {
|
||||||
createPredicate,
|
createPredicate,
|
||||||
type PredicateKind,
|
type PredicateKind,
|
||||||
@@ -159,6 +168,13 @@ export function Workbench() {
|
|||||||
const [preserveSignature, setPreserveSignature] = useState(true);
|
const [preserveSignature, setPreserveSignature] = useState(true);
|
||||||
const [maxTests, setMaxTests] = useState(500);
|
const [maxTests, setMaxTests] = useState(500);
|
||||||
const [maxSeconds, setMaxSeconds] = useState(10);
|
const [maxSeconds, setMaxSeconds] = useState(10);
|
||||||
|
const [stabilityProfile, setStabilityProfile] = useState<
|
||||||
|
"single" | "majority" | "strict"
|
||||||
|
>("majority");
|
||||||
|
const [bundleFiles, setBundleFiles] = useState<BundleFile[]>([]);
|
||||||
|
const [bundleAdapter, setBundleAdapter] =
|
||||||
|
useState<BundleAdapterProfile["id"]>("concatenated");
|
||||||
|
const [bundleResult, setBundleResult] = useState<BundleMinimizeResult>();
|
||||||
const [result, setResult] = useState<MinimizeResult>();
|
const [result, setResult] = useState<MinimizeResult>();
|
||||||
const [resultRecipe, setResultRecipe] = useState<PredicateRecipe>();
|
const [resultRecipe, setResultRecipe] = useState<PredicateRecipe>();
|
||||||
const [resultLimits, setResultLimits] = useState<{
|
const [resultLimits, setResultLimits] = useState<{
|
||||||
@@ -183,6 +199,12 @@ export function Workbench() {
|
|||||||
stylesheet,
|
stylesheet,
|
||||||
preserveFailureSignature: preserveSignature,
|
preserveFailureSignature: preserveSignature,
|
||||||
};
|
};
|
||||||
|
const stability: PredicateStability =
|
||||||
|
stabilityProfile === "single"
|
||||||
|
? { samples: 1, requiredPasses: 1 }
|
||||||
|
: stabilityProfile === "strict"
|
||||||
|
? { samples: 3, requiredPasses: 3 }
|
||||||
|
: { samples: 3, requiredPasses: 2 };
|
||||||
|
|
||||||
const report = useMemo(
|
const report = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -199,6 +221,8 @@ export function Workbench() {
|
|||||||
accepted: result.accepted,
|
accepted: result.accepted,
|
||||||
exhausted: result.exhausted,
|
exhausted: result.exhausted,
|
||||||
steps: result.steps,
|
steps: result.steps,
|
||||||
|
stability: result.stability,
|
||||||
|
unstableCandidates: result.unstableCandidates,
|
||||||
},
|
},
|
||||||
2,
|
2,
|
||||||
{ maxTextChars: 2 * 1024 * 1024, maxDepth: 16, maxNodes: 20_000 },
|
{ maxTextChars: 2 * 1024 * 1024, maxDepth: 16, maxNodes: 20_000 },
|
||||||
@@ -219,6 +243,19 @@ export function Workbench() {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
try {
|
try {
|
||||||
const predicate = createPredicate(recipe, runner.evaluate);
|
const predicate = createPredicate(recipe, runner.evaluate);
|
||||||
|
if (bundleFiles.length) {
|
||||||
|
const next = await minimizeBundle(
|
||||||
|
bundleFiles,
|
||||||
|
{ maxTests, maxSeconds, stability },
|
||||||
|
bundlePredicateAdapter(predicate, bundleAdapter),
|
||||||
|
nextController.signal,
|
||||||
|
);
|
||||||
|
setBundleResult(next);
|
||||||
|
setResult(undefined);
|
||||||
|
setResultRecipe(structuredClone(recipe));
|
||||||
|
setResultLimits({ maxTests, maxSeconds });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const next = await minimizeInput(
|
const next = await minimizeInput(
|
||||||
source,
|
source,
|
||||||
{
|
{
|
||||||
@@ -229,12 +266,14 @@ export function Workbench() {
|
|||||||
? Math.min(maxTests, 200)
|
? Math.min(maxTests, 200)
|
||||||
: maxTests,
|
: maxTests,
|
||||||
maxSeconds,
|
maxSeconds,
|
||||||
|
stability,
|
||||||
},
|
},
|
||||||
predicate,
|
predicate,
|
||||||
nextController.signal,
|
nextController.signal,
|
||||||
setProgress,
|
setProgress,
|
||||||
);
|
);
|
||||||
setResult(next);
|
setResult(next);
|
||||||
|
setBundleResult(undefined);
|
||||||
setResultRecipe(structuredClone(recipe));
|
setResultRecipe(structuredClone(recipe));
|
||||||
setResultLimits({
|
setResultLimits({
|
||||||
maxTests:
|
maxTests:
|
||||||
@@ -270,6 +309,7 @@ export function Workbench() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSource(await file.text());
|
setSource(await file.text());
|
||||||
|
setBundleFiles([]);
|
||||||
setSourceName(file.name);
|
setSourceName(file.name);
|
||||||
setStructure(
|
setStructure(
|
||||||
file.name.toLowerCase().endsWith(".json")
|
file.name.toLowerCase().endsWith(".json")
|
||||||
@@ -281,6 +321,27 @@ export function Workbench() {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openBundle = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = [...(event.target.files ?? [])];
|
||||||
|
event.target.value = "";
|
||||||
|
try {
|
||||||
|
if (files.length > 100)
|
||||||
|
throw new RangeError("Bundles are limited to 100 files.");
|
||||||
|
const loaded = await Promise.all(
|
||||||
|
files.map(async (file) => ({
|
||||||
|
path: file.webkitRelativePath || file.name,
|
||||||
|
content: await file.text(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
setBundleFiles(loaded);
|
||||||
|
setError(undefined);
|
||||||
|
} catch (reason) {
|
||||||
|
setError(
|
||||||
|
reason instanceof Error ? reason.message : "Bundle loading failed.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applyPreset = (name: "json" | "xml" | "regex") => {
|
const applyPreset = (name: "json" | "xml" | "regex") => {
|
||||||
if (name === "json") {
|
if (name === "json") {
|
||||||
setSource(JSON_SAMPLE);
|
setSource(JSON_SAMPLE);
|
||||||
@@ -349,6 +410,14 @@ export function Workbench() {
|
|||||||
onChange={(event) => void openFile(event)}
|
onChange={(event) => void openFile(event)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<label className="button file-button">
|
||||||
|
Open file bundle
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
onChange={(event) => void openBundle(event)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<label>
|
||||||
Structure
|
Structure
|
||||||
@@ -370,6 +439,15 @@ export function Workbench() {
|
|||||||
rows={22}
|
rows={22}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
{bundleFiles.length ? (
|
||||||
|
<div className="bundle-summary">
|
||||||
|
<strong>{bundleFiles.length} bundle files active</strong>
|
||||||
|
<p>{bundleFiles.map((file) => file.path).join(" · ")}</p>
|
||||||
|
<button type="button" onClick={() => setBundleFiles([])}>
|
||||||
|
Return to single input
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel predicate-panel">
|
<article className="panel predicate-panel">
|
||||||
@@ -483,6 +561,37 @@ export function Workbench() {
|
|||||||
</label>
|
</label>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="compact-grid limits">
|
<div className="compact-grid limits">
|
||||||
|
<label>
|
||||||
|
Predicate stability
|
||||||
|
<select
|
||||||
|
value={stabilityProfile}
|
||||||
|
onChange={(event) =>
|
||||||
|
setStabilityProfile(
|
||||||
|
event.target.value as typeof stabilityProfile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="single">Single observation</option>
|
||||||
|
<option value="majority">Majority (2 of 3)</option>
|
||||||
|
<option value="strict">Strict (3 of 3)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Bundle predicate adapter
|
||||||
|
<select
|
||||||
|
value={bundleAdapter}
|
||||||
|
disabled={!bundleFiles.length}
|
||||||
|
onChange={(event) =>
|
||||||
|
setBundleAdapter(
|
||||||
|
event.target.value as BundleAdapterProfile["id"],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="concatenated">Delimited concatenation</option>
|
||||||
|
<option value="json-map">JSON path map</option>
|
||||||
|
<option value="primary-file">Primary file only</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Maximum predicate tests
|
Maximum predicate tests
|
||||||
<input
|
<input
|
||||||
@@ -591,6 +700,45 @@ export function Workbench() {
|
|||||||
</details>
|
</details>
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{bundleResult ? (
|
||||||
|
<section className="panel result-panel" aria-live="polite">
|
||||||
|
<div className="panel-heading">
|
||||||
|
<div>
|
||||||
|
<h2>Minimized bundle</h2>
|
||||||
|
<p>
|
||||||
|
{bundleResult.original.length} → {bundleResult.minimized.length}{" "}
|
||||||
|
files · {bundleResult.tests} predicate observations ·{" "}
|
||||||
|
{bundleResult.unstableCandidates} flaky candidates observed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
triggerBlobDownloads(
|
||||||
|
bundleResult.minimized.map((file) => ({
|
||||||
|
blob: new Blob([file.content], {
|
||||||
|
type: "text/plain;charset=utf-8",
|
||||||
|
}),
|
||||||
|
filename: file.path.replaceAll("/", "__"),
|
||||||
|
})),
|
||||||
|
{ order: "filename", maximumFiles: 100 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Download retained files
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul className="step-list">
|
||||||
|
{bundleResult.minimized.map((file) => (
|
||||||
|
<li key={file.path}>
|
||||||
|
<code>{file.path}</code> ·{" "}
|
||||||
|
{byteLength(file.content).toLocaleString()} bytes
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,296 @@
|
|||||||
|
import { stableStringify } from "@add-ideas/toolbox-helpers";
|
||||||
|
import {
|
||||||
|
MAX_TESTS,
|
||||||
|
byteLength,
|
||||||
|
minimizeInput,
|
||||||
|
type FailurePredicate,
|
||||||
|
type InputStructure,
|
||||||
|
type PredicateStability,
|
||||||
|
type ReductionStep,
|
||||||
|
} from "./engine";
|
||||||
|
|
||||||
|
export const MAX_BUNDLE_FILES = 100;
|
||||||
|
export const MAX_BUNDLE_BYTES = 4 * 1024 * 1024;
|
||||||
|
|
||||||
|
export interface BundleFile {
|
||||||
|
path: string;
|
||||||
|
content: string;
|
||||||
|
structure?: InputStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleMinimizeOptions {
|
||||||
|
maxTests: number;
|
||||||
|
maxSeconds: number;
|
||||||
|
stability?: PredicateStability;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BundlePredicate = (
|
||||||
|
files: readonly BundleFile[],
|
||||||
|
signal: AbortSignal,
|
||||||
|
) => boolean | Promise<boolean>;
|
||||||
|
|
||||||
|
export interface BundleReductionStep {
|
||||||
|
stage: "files" | "content";
|
||||||
|
path?: string;
|
||||||
|
beforeFiles: number;
|
||||||
|
afterFiles: number;
|
||||||
|
beforeBytes: number;
|
||||||
|
afterBytes: number;
|
||||||
|
test: number;
|
||||||
|
contentStep?: ReductionStep["stage"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleMinimizeResult {
|
||||||
|
version: "bundle-ddmin-v1";
|
||||||
|
original: BundleFile[];
|
||||||
|
minimized: BundleFile[];
|
||||||
|
tests: number;
|
||||||
|
accepted: number;
|
||||||
|
exhausted: boolean;
|
||||||
|
stability: PredicateStability;
|
||||||
|
unstableCandidates: number;
|
||||||
|
steps: BundleReductionStep[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleAdapterProfile {
|
||||||
|
id: "json-map" | "concatenated" | "primary-file";
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
adapt(files: readonly BundleFile[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BUNDLE_ADAPTER_PROFILES: readonly BundleAdapterProfile[] = [
|
||||||
|
{
|
||||||
|
id: "json-map",
|
||||||
|
label: "JSON path map",
|
||||||
|
description: "Presents a deterministic path-to-content JSON object.",
|
||||||
|
adapt: (files) =>
|
||||||
|
stableStringify(
|
||||||
|
Object.fromEntries(files.map((file) => [file.path, file.content])),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "concatenated",
|
||||||
|
label: "Delimited concatenation",
|
||||||
|
description:
|
||||||
|
"Presents all files with explicit, deterministic path markers.",
|
||||||
|
adapt: (files) =>
|
||||||
|
files
|
||||||
|
.map((file) => `\n--- toolbox-file:${file.path} ---\n${file.content}`)
|
||||||
|
.join(""),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "primary-file",
|
||||||
|
label: "Primary file",
|
||||||
|
description: "Presents only the lexicographically first retained file.",
|
||||||
|
adapt: (files) => files[0]?.content ?? "",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function bundlePredicateAdapter(
|
||||||
|
predicate: FailurePredicate,
|
||||||
|
profileId: BundleAdapterProfile["id"],
|
||||||
|
): BundlePredicate {
|
||||||
|
const profile = BUNDLE_ADAPTER_PROFILES.find((item) => item.id === profileId);
|
||||||
|
if (!profile) throw new TypeError(`Unknown bundle adapter ${profileId}.`);
|
||||||
|
return (files, signal) => predicate(profile.adapt(files), signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function minimizeBundle(
|
||||||
|
input: readonly BundleFile[],
|
||||||
|
options: BundleMinimizeOptions,
|
||||||
|
predicate: BundlePredicate,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<BundleMinimizeResult> {
|
||||||
|
const original = normalizeBundle(input);
|
||||||
|
if (
|
||||||
|
!Number.isInteger(options.maxTests) ||
|
||||||
|
options.maxTests < 1 ||
|
||||||
|
options.maxTests > MAX_TESTS
|
||||||
|
)
|
||||||
|
throw new RangeError(`Bundle test budget must be 1–${MAX_TESTS}.`);
|
||||||
|
if (
|
||||||
|
!Number.isFinite(options.maxSeconds) ||
|
||||||
|
options.maxSeconds <= 0 ||
|
||||||
|
options.maxSeconds > 30
|
||||||
|
)
|
||||||
|
throw new RangeError(
|
||||||
|
"Bundle time budget must be above 0 and at most 30 seconds.",
|
||||||
|
);
|
||||||
|
const stability = options.stability ?? { samples: 1, requiredPasses: 1 };
|
||||||
|
if (
|
||||||
|
!Number.isInteger(stability.samples) ||
|
||||||
|
stability.samples < 1 ||
|
||||||
|
stability.samples > 9 ||
|
||||||
|
!Number.isInteger(stability.requiredPasses) ||
|
||||||
|
stability.requiredPasses < 1 ||
|
||||||
|
stability.requiredPasses > stability.samples
|
||||||
|
)
|
||||||
|
throw new RangeError("Bundle stability settings are invalid.");
|
||||||
|
const deadline = Date.now() + options.maxSeconds * 1_000;
|
||||||
|
let tests = 0;
|
||||||
|
let accepted = 0;
|
||||||
|
let exhausted = false;
|
||||||
|
let unstableCandidates = 0;
|
||||||
|
let current = original.map((file) => ({ ...file }));
|
||||||
|
const steps: BundleReductionStep[] = [];
|
||||||
|
const evaluate = async (candidate: readonly BundleFile[]) => {
|
||||||
|
let passes = 0;
|
||||||
|
let observations = 0;
|
||||||
|
while (observations < stability.samples) {
|
||||||
|
signal.throwIfAborted();
|
||||||
|
if (tests >= options.maxTests || Date.now() >= deadline) {
|
||||||
|
exhausted = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tests += 1;
|
||||||
|
if (await predicate(candidate, signal)) passes += 1;
|
||||||
|
observations += 1;
|
||||||
|
}
|
||||||
|
if (passes > 0 && passes < observations) unstableCandidates += 1;
|
||||||
|
return passes >= stability.requiredPasses;
|
||||||
|
};
|
||||||
|
if (!(await evaluate(current)))
|
||||||
|
throw new TypeError(
|
||||||
|
"The original bundle does not stably satisfy the selected predicate.",
|
||||||
|
);
|
||||||
|
|
||||||
|
let granularity = 2;
|
||||||
|
while (current.length && !exhausted) {
|
||||||
|
const chunk = Math.ceil(current.length / granularity);
|
||||||
|
let changed = false;
|
||||||
|
for (let start = 0; start < current.length; start += chunk) {
|
||||||
|
const candidate = current
|
||||||
|
.slice(0, start)
|
||||||
|
.concat(current.slice(start + chunk));
|
||||||
|
if (await evaluate(candidate)) {
|
||||||
|
const before = bundleBytes(current);
|
||||||
|
const beforeFiles = current.length;
|
||||||
|
current = candidate;
|
||||||
|
accepted += 1;
|
||||||
|
steps.push({
|
||||||
|
stage: "files",
|
||||||
|
beforeFiles,
|
||||||
|
afterFiles: current.length,
|
||||||
|
beforeBytes: before,
|
||||||
|
afterBytes: bundleBytes(current),
|
||||||
|
test: tests,
|
||||||
|
});
|
||||||
|
granularity = Math.max(2, granularity - 1);
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (exhausted) break;
|
||||||
|
}
|
||||||
|
if (!changed) {
|
||||||
|
if (granularity >= current.length) break;
|
||||||
|
granularity = Math.min(current.length, granularity * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < current.length && !exhausted; index += 1) {
|
||||||
|
const remainingTests = options.maxTests - tests;
|
||||||
|
const remainingSeconds = (deadline - Date.now()) / 1_000;
|
||||||
|
if (remainingTests < 1 || remainingSeconds <= 0) {
|
||||||
|
exhausted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const file = current[index]!;
|
||||||
|
const result = await minimizeInput(
|
||||||
|
file.content,
|
||||||
|
{
|
||||||
|
structure: file.structure ?? inferStructure(file.path),
|
||||||
|
maxTests: remainingTests,
|
||||||
|
maxSeconds: Math.min(30, remainingSeconds),
|
||||||
|
stability,
|
||||||
|
},
|
||||||
|
(candidate, childSignal) =>
|
||||||
|
predicate(
|
||||||
|
current.map((entry, candidateIndex) =>
|
||||||
|
candidateIndex === index ? { ...entry, content: candidate } : entry,
|
||||||
|
),
|
||||||
|
childSignal,
|
||||||
|
),
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
tests += result.tests;
|
||||||
|
exhausted ||= result.exhausted;
|
||||||
|
if (result.minimized !== file.content) {
|
||||||
|
const before = bundleBytes(current);
|
||||||
|
current[index] = { ...file, content: result.minimized };
|
||||||
|
accepted += result.accepted;
|
||||||
|
for (const contentStep of result.steps)
|
||||||
|
steps.push({
|
||||||
|
stage: "content",
|
||||||
|
path: file.path,
|
||||||
|
beforeFiles: current.length,
|
||||||
|
afterFiles: current.length,
|
||||||
|
beforeBytes: before,
|
||||||
|
afterBytes: bundleBytes(current),
|
||||||
|
test: tests - result.tests + contentStep.test,
|
||||||
|
contentStep: contentStep.stage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
version: "bundle-ddmin-v1",
|
||||||
|
original,
|
||||||
|
minimized: current,
|
||||||
|
tests,
|
||||||
|
accepted,
|
||||||
|
exhausted,
|
||||||
|
stability,
|
||||||
|
unstableCandidates,
|
||||||
|
steps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBundle(input: readonly BundleFile[]): BundleFile[] {
|
||||||
|
if (!input.length) throw new TypeError("A bundle needs at least one file.");
|
||||||
|
if (input.length > MAX_BUNDLE_FILES)
|
||||||
|
throw new RangeError(`Bundles are limited to ${MAX_BUNDLE_FILES} files.`);
|
||||||
|
const paths = new Set<string>();
|
||||||
|
const result = input.map((file) => {
|
||||||
|
const path = normalizePath(file.path);
|
||||||
|
if (paths.has(path)) throw new TypeError(`Duplicate bundle path ${path}.`);
|
||||||
|
paths.add(path);
|
||||||
|
if (byteLength(file.content) > 2 * 1024 * 1024)
|
||||||
|
throw new RangeError(`${path} exceeds the 2 MiB per-file limit.`);
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
content: file.content,
|
||||||
|
...(file.structure ? { structure: file.structure } : {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
result.sort((left, right) =>
|
||||||
|
left.path < right.path ? -1 : left.path > right.path ? 1 : 0,
|
||||||
|
);
|
||||||
|
if (bundleBytes(result) > MAX_BUNDLE_BYTES)
|
||||||
|
throw new RangeError("Bundle contents exceed 4 MiB.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path: string): string {
|
||||||
|
const normalized = path.normalize("NFC");
|
||||||
|
if (
|
||||||
|
!normalized ||
|
||||||
|
normalized.includes("\\") ||
|
||||||
|
normalized.startsWith("/") ||
|
||||||
|
normalized.split("/").some((part) => !part || part === "." || part === "..")
|
||||||
|
)
|
||||||
|
throw new TypeError(`Unsafe bundle path ${path}.`);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundleBytes(files: readonly BundleFile[]): number {
|
||||||
|
return files.reduce((sum, file) => sum + byteLength(file.content), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferStructure(path: string): InputStructure {
|
||||||
|
const lower = path.toLowerCase();
|
||||||
|
return lower.endsWith(".json")
|
||||||
|
? "json"
|
||||||
|
: lower.endsWith(".xml")
|
||||||
|
? "xml"
|
||||||
|
: "text";
|
||||||
|
}
|
||||||
+40
-2
@@ -11,6 +11,14 @@ export interface MinimizeOptions {
|
|||||||
structure: InputStructure;
|
structure: InputStructure;
|
||||||
maxTests: number;
|
maxTests: number;
|
||||||
maxSeconds: number;
|
maxSeconds: number;
|
||||||
|
stability?: PredicateStability;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PredicateStability {
|
||||||
|
/** Independent observations per candidate (1–9). */
|
||||||
|
samples: number;
|
||||||
|
/** Required reproductions within those observations (1–samples). */
|
||||||
|
requiredPasses: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReductionStep {
|
export interface ReductionStep {
|
||||||
@@ -35,6 +43,8 @@ export interface MinimizeResult {
|
|||||||
accepted: number;
|
accepted: number;
|
||||||
exhausted: boolean;
|
exhausted: boolean;
|
||||||
steps: ReductionStep[];
|
steps: ReductionStep[];
|
||||||
|
stability: PredicateStability;
|
||||||
|
unstableCandidates: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FailurePredicate = (
|
export type FailurePredicate = (
|
||||||
@@ -76,6 +86,18 @@ function assertOptions(options: MinimizeOptions): void {
|
|||||||
throw new RangeError(
|
throw new RangeError(
|
||||||
`Time budget must be above 0 and at most ${MAX_SECONDS} seconds.`,
|
`Time budget must be above 0 and at most ${MAX_SECONDS} seconds.`,
|
||||||
);
|
);
|
||||||
|
const stability = options.stability ?? { samples: 1, requiredPasses: 1 };
|
||||||
|
if (
|
||||||
|
!Number.isInteger(stability.samples) ||
|
||||||
|
stability.samples < 1 ||
|
||||||
|
stability.samples > 9 ||
|
||||||
|
!Number.isInteger(stability.requiredPasses) ||
|
||||||
|
stability.requiredPasses < 1 ||
|
||||||
|
stability.requiredPasses > stability.samples
|
||||||
|
)
|
||||||
|
throw new RangeError(
|
||||||
|
"Predicate stability needs 1–9 samples and 1–samples required passes.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitLines(value: string): string[] {
|
function splitLines(value: string): string[] {
|
||||||
@@ -256,10 +278,12 @@ export async function minimizeInput(
|
|||||||
let tests = 0;
|
let tests = 0;
|
||||||
let accepted = 0;
|
let accepted = 0;
|
||||||
let exhausted = false;
|
let exhausted = false;
|
||||||
|
let unstableCandidates = 0;
|
||||||
let current = input;
|
let current = input;
|
||||||
let stage: ReductionStep["stage"] = "normalize";
|
let stage: ReductionStep["stage"] = "normalize";
|
||||||
const steps: ReductionStep[] = [];
|
const steps: ReductionStep[] = [];
|
||||||
const cache = new Map<string, boolean>();
|
const cache = new Map<string, boolean>();
|
||||||
|
const stability = options.stability ?? { samples: 1, requiredPasses: 1 };
|
||||||
const preserveValidSyntax =
|
const preserveValidSyntax =
|
||||||
options.structure === "json"
|
options.structure === "json"
|
||||||
? (() => {
|
? (() => {
|
||||||
@@ -282,7 +306,6 @@ export async function minimizeInput(
|
|||||||
}
|
}
|
||||||
const cached = cache.get(candidate);
|
const cached = cache.get(candidate);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
tests += 1;
|
|
||||||
if (
|
if (
|
||||||
preserveValidSyntax &&
|
preserveValidSyntax &&
|
||||||
((options.structure === "json" &&
|
((options.structure === "json" &&
|
||||||
@@ -299,7 +322,20 @@ export async function minimizeInput(
|
|||||||
cache.set(candidate, false);
|
cache.set(candidate, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const result = await predicate(candidate, signal);
|
let passes = 0;
|
||||||
|
let observations = 0;
|
||||||
|
while (observations < stability.samples) {
|
||||||
|
if (tests >= options.maxTests || Date.now() >= deadline) {
|
||||||
|
exhausted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tests += 1;
|
||||||
|
if (await predicate(candidate, signal)) passes += 1;
|
||||||
|
observations += 1;
|
||||||
|
}
|
||||||
|
const result =
|
||||||
|
observations === stability.samples && passes >= stability.requiredPasses;
|
||||||
|
if (passes > 0 && passes < observations) unstableCandidates += 1;
|
||||||
cache.set(candidate, result);
|
cache.set(candidate, result);
|
||||||
if (tests % 20 === 0)
|
if (tests % 20 === 0)
|
||||||
onProgress?.({ candidate: current, tests, accepted, stage });
|
onProgress?.({ candidate: current, tests, accepted, stage });
|
||||||
@@ -407,5 +443,7 @@ export async function minimizeInput(
|
|||||||
accepted,
|
accepted,
|
||||||
exhausted,
|
exhausted,
|
||||||
steps,
|
steps,
|
||||||
|
stability,
|
||||||
|
unstableCandidates,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "de.add-ideas.minimize-tools",
|
"id": "de.add-ideas.minimize-tools",
|
||||||
"name": "Minimize Tools",
|
"name": "Minimize Tools",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"description": "Reduce failing inputs while preserving the failure locally.",
|
"description": "Reduce failing inputs while preserving the failure locally.",
|
||||||
"entry": "./",
|
"entry": "./",
|
||||||
"icon": "./favicon.svg",
|
"icon": "./favicon.svg",
|
||||||
@@ -23,11 +23,49 @@
|
|||||||
},
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"secureContext": false,
|
"secureContext": false,
|
||||||
"workers": true,
|
"workers": false,
|
||||||
"indexedDb": false,
|
"indexedDb": false,
|
||||||
"crossOriginIsolated": false,
|
"crossOriginIsolated": false,
|
||||||
"topLevelContext": false
|
"topLevelContext": false
|
||||||
},
|
},
|
||||||
|
"io": {
|
||||||
|
"accepts": [
|
||||||
|
{
|
||||||
|
"mediaType": "text/plain",
|
||||||
|
"extensions": [".txt"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"extensions": [".json"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/xml",
|
||||||
|
"extensions": [".xml", ".xsl", ".xslt"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
{
|
||||||
|
"mediaType": "text/plain",
|
||||||
|
"extensions": [".txt"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/json",
|
||||||
|
"extensions": [".json"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/xml",
|
||||||
|
"extensions": [".xml"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/zip",
|
||||||
|
"extensions": [".zip"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"required": [],
|
||||||
|
"optional": ["workers"]
|
||||||
|
},
|
||||||
"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";
|
||||||
|
|||||||
@@ -81,6 +81,25 @@ test("downloads the minimized case and exact predicate report", async ({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("minimizes a deterministic multi-file bundle with stability sampling", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto("/deep/nested/minimize/");
|
||||||
|
await page.getByLabel("Failure predicate").selectOption("contains");
|
||||||
|
const bundleInput = page.locator('input[type="file"][multiple]');
|
||||||
|
await bundleInput.setInputFiles([
|
||||||
|
"tests/fixtures/noise.txt",
|
||||||
|
"tests/fixtures/case.txt",
|
||||||
|
]);
|
||||||
|
await expect(page.getByText("2 bundle files active")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Minimize reproducer" }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Minimized bundle" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText(/2 → 1 files/u)).toBeVisible();
|
||||||
|
await expect(page.getByText(/flaky candidates observed/u)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("integrates help, themes, identity, headers and offline reload", async ({
|
test("integrates help, themes, identity, headers and offline reload", async ({
|
||||||
page,
|
page,
|
||||||
request,
|
request,
|
||||||
@@ -111,7 +130,7 @@ test("integrates help, themes, identity, headers and offline reload", async ({
|
|||||||
const manifest = await request.get("/deep/nested/minimize/toolbox-app.json");
|
const manifest = await request.get("/deep/nested/minimize/toolbox-app.json");
|
||||||
await expect(manifest.json()).resolves.toMatchObject({
|
await expect(manifest.json()).resolves.toMatchObject({
|
||||||
id: "de.add-ideas.minimize-tools",
|
id: "de.add-ideas.minimize-tools",
|
||||||
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/minimize/");
|
||||||
|
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 @@
|
|||||||
|
prefix BOOM suffix
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
discard this entire file
|
||||||
@@ -4,6 +4,10 @@ import {
|
|||||||
createPredicate,
|
createPredicate,
|
||||||
type RegexEvaluator,
|
type RegexEvaluator,
|
||||||
} from "../../src/minimize/predicates";
|
} from "../../src/minimize/predicates";
|
||||||
|
import {
|
||||||
|
bundlePredicateAdapter,
|
||||||
|
minimizeBundle,
|
||||||
|
} from "../../src/minimize/bundle";
|
||||||
|
|
||||||
const unusedRegex: RegexEvaluator = async () => ({
|
const unusedRegex: RegexEvaluator = async () => ({
|
||||||
matched: false,
|
matched: false,
|
||||||
@@ -125,4 +129,47 @@ describe("minimization engine", () => {
|
|||||||
),
|
),
|
||||||
).toThrow(/include/u);
|
).toThrow(/include/u);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("samples a flaky predicate according to an explicit stability policy", async () => {
|
||||||
|
const observations = new Map<string, number>();
|
||||||
|
const result = await minimizeInput(
|
||||||
|
"noise BOOM noise",
|
||||||
|
{
|
||||||
|
structure: "text",
|
||||||
|
maxTests: 300,
|
||||||
|
maxSeconds: 5,
|
||||||
|
stability: { samples: 3, requiredPasses: 2 },
|
||||||
|
},
|
||||||
|
async (candidate) => {
|
||||||
|
const count = (observations.get(candidate) ?? 0) + 1;
|
||||||
|
observations.set(candidate, count);
|
||||||
|
return candidate.includes("BOOM") && count % 3 !== 0;
|
||||||
|
},
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
expect(result.minimized).toContain("BOOM");
|
||||||
|
expect(result.stability).toEqual({ samples: 3, requiredPasses: 2 });
|
||||||
|
expect(result.unstableCandidates).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes files and then minimizes retained bundle content", async () => {
|
||||||
|
const predicate = bundlePredicateAdapter(
|
||||||
|
createPredicate({ kind: "contains", needle: "BOOM" }, unusedRegex),
|
||||||
|
"concatenated",
|
||||||
|
);
|
||||||
|
const result = await minimizeBundle(
|
||||||
|
[
|
||||||
|
{ path: "noise.txt", content: "discard all of this" },
|
||||||
|
{ path: "case/input.txt", content: "prefix BOOM suffix" },
|
||||||
|
],
|
||||||
|
{ maxTests: 400, maxSeconds: 5 },
|
||||||
|
predicate,
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
expect(result.minimized).toHaveLength(1);
|
||||||
|
expect(result.minimized[0]).toMatchObject({ path: "case/input.txt" });
|
||||||
|
expect(result.minimized[0]?.content).toContain("BOOM");
|
||||||
|
expect(result.steps.some((step) => step.stage === "files")).toBe(true);
|
||||||
|
expect(result.steps.some((step) => step.stage === "content")).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user