diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d8eb32..c0cf765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,27 @@ jobs: - run: pnpm example - run: pnpm test:demo - run: pnpm test:comparison + - run: pnpm test:workflows - run: pnpm test:package + workflow-pdf: + name: PDF extraction and lookup + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 + with: + node-version: 24 + package-manager-cache: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 + with: + python-version: '3.14' + - run: npm install -g pnpm@11.25.0 + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm build + - run: pnpm test:workflows + - run: python -m pip install -r examples/workflows/requirements-pdf.txt + - run: python examples/workflows/pdf.py --check browsers: name: Chromium / Firefox / WebKit runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index a7ed368..0bd0b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +- Add a local PDF/phone lookup experiment comparing existing identifiers, a simple + alias and RefMint. Record 54 synthetic cases, input lengths and logical lookup calls; + include costs that favor existing identifiers and aliases. No human-performance claim. +- Verify identifiers extracted from a deterministic sample PDF against the same lookup + model; test the browser form, permitted scope and wrong-record cases. +- Clarify when RefMint is unnecessary, alpha/API versus frozen v1 format commitments, + and why checksums cannot replace authorization or regulated identifiers. +- Runtime APIs and wire formats are unchanged. No new npm runtime dependencies. + ## 0.1.0-alpha.2 — 2026-10-27 Second prerelease, including the existing-ID API, compatibility checks, comparison form and measured optimizations. Available as a GitHub package archive and on npm under the `alpha` tag. The npm publication updates installation/release documentation only; runtime, declarations, package metadata and measured evidence match the original GitHub archive byte for byte. diff --git a/README.md b/README.md index 885438b..e86a2be 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,20 @@ The image is generated from [recorded evidence](docs/evidence.json). See [measur ## Try it +**Would an extra reference help?** Try the new current-source +[PDF/phone lookup experiment](docs/workflows.md): existing number or hostname, +five-character alias, and RefMint on the same records. The sample fleet code is +49 printed characters versus an 8-character hostname; the invoice code is 21 versus +a 15-character invoice number. The checksum rejects selected typos, but no human-entry +advantage has been established. [When RefMint is unnecessary](docs/when-not-to-use.md). + Install the current alpha from npm: ```sh npm install refmint@alpha ``` -To pin this release, use `npm install --save-exact refmint@0.1.0-alpha.2`. The `alpha` tag is intentional: this is a prerelease, and no stable `latest` release is advertised. Registry installation does not require an npm account or the URL-dependency flag below. +To pin this release, use `npm install --save-exact refmint@0.1.0-alpha.2`. This is a prerelease even when the `latest` registry tag resolves to it; npm availability does not make its API stable. Registry installation does not require an npm account or the URL-dependency flag below. The GitHub package archive remains available as an alternative: diff --git a/docs/when-not-to-use.md b/docs/when-not-to-use.md new file mode 100644 index 0000000..9f2a31f --- /dev/null +++ b/docs/when-not-to-use.md @@ -0,0 +1,43 @@ +# When RefMint is unnecessary + +Start with the identifier and search workflow users already know. + +| Situation | Prefer | +| --- | --- | +| Existing invoice/quote numbers or hostnames are short and lookup works | Keep them; a second identifier creates training and maintenance cost | +| People paste a link, scan a QR code, or never type an identifier | Improve that flow first; no manual-entry benefit has been demonstrated | +| A five-character local alias handles the support case | A small unique alias mapping, if its lifecycle is manageable | +| You need compact general-purpose random IDs | An established ID generator; checksum overhead may not help | +| A regulated number or a string with leading zeros must be preserved | Keep its exact string; the uint64 codec is not a numbering-system replacement | +| Your dependency policy excludes prereleases | Wait or use a supported stable alternative | +| You need a credential, signature, unguessable access link or encrypted ID | An appropriate security design; never a public checksum | + +RefMint is worth evaluating when people repeatedly enter a **public** reference and +the application benefits from consistent namespaces, display forms and diagnostics. +There is no demonstrated human-entry speedup, reduced support cost, or confirmed need +in Octofleet/Invoice Builder from the current synthetic demo. + +## npm availability and compatibility are separate + +The package is **alpha**, even when `npm install refmint` resolves to it. Registry tags +are distribution pointers; they do not certify stability. Pin the reviewed version and +check your organization's policy before adopting it. + +The existing-ID **v1 wire contract is already frozen**: canonical encoding and accepted +presentation forms must remain compatible; incompatible formats require a new version +and reader. That commitment is described in [format-v1.md](format-v1.md) and checked +with frozen vectors. It does not promise a stable alpha API, operational support, or +that an application can safely remove old readers. Keep version/kind/namespace choices +with your schema and test migrations. npm publication alone adds no compatibility promise. + +For the separate random-reference API, persist the configured prefix and payload length; +do not recalculate them from the current row count. See [SPEC.md](../SPEC.md). + +## A checksum is not authorization + +Anyone can generate or recompute a checksum, including for another valid existing ID. +The codec is reversible and does not hide sequential IDs. Always enforce authorization +on the server and confirm the intended record before sensitive actions. Error detection +does not prove the sender, document authenticity, record existence or permission. + +[Evaluate all three routes](workflows.md) before adding another reference to a product. diff --git a/docs/workflow-results.md b/docs/workflow-results.md new file mode 100644 index 0000000..951d4a2 --- /dev/null +++ b/docs/workflow-results.md @@ -0,0 +1,72 @@ +# Local lookup results + +Synthetic fixtures, recorded 28 October 2026. No production support cases or human timing data. + +| Domain | Route | Canonical characters | Printed characters | +| --- | --- | ---: | ---: | +| fleet | existing | 8 | 8 | +| fleet | alias | 5 | 5 | +| fleet | refmint | 41 | 49 | +| invoice | existing | 15 | 15 | +| invoice | alias | 5 | 5 | +| invoice | refmint | 19 | 21 | + +| Domain | Route | Case | Result | Logical lookups | Different record from intended? | +| --- | --- | --- | --- | ---: | --- | +| fleet | existing | Exact | found | 1 | no | +| fleet | existing | Printed / pasted | found | 1 | no | +| fleet | existing | One-symbol change | found | 1 | yes | +| fleet | existing | Truncated | ambiguous | 1 | no | +| fleet | existing | Wrong record type | not_found | 1 | no | +| fleet | existing | Valid but unknown | not_found | 1 | no | +| fleet | existing | Outside allowed scope | not_found | 1 | no | +| fleet | existing | Another valid code | found | 1 | yes | +| fleet | existing | Empty | invalid (input) | 0 | no | +| fleet | alias | Exact | found | 1 | no | +| fleet | alias | Printed / pasted | found | 1 | no | +| fleet | alias | One-symbol change | found | 1 | yes | +| fleet | alias | Truncated | not_found | 1 | no | +| fleet | alias | Wrong record type | not_found | 1 | no | +| fleet | alias | Valid but unknown | not_found | 1 | no | +| fleet | alias | Outside allowed scope | not_found | 1 | no | +| fleet | alias | Another valid code | found | 1 | yes | +| fleet | alias | Empty | invalid (input) | 0 | no | +| fleet | refmint | Exact | found | 1 | no | +| fleet | refmint | Printed / pasted | found | 1 | no | +| fleet | refmint | One-symbol change | invalid (checksum) | 0 | no | +| fleet | refmint | Truncated | invalid (checksum) | 0 | no | +| fleet | refmint | Wrong record type | invalid (prefix) | 0 | no | +| fleet | refmint | Valid but unknown | not_found | 1 | no | +| fleet | refmint | Outside allowed scope | not_found | 1 | no | +| fleet | refmint | Another valid code | found | 1 | yes | +| fleet | refmint | Empty | invalid (input) | 0 | no | +| invoice | existing | Exact | found | 1 | no | +| invoice | existing | Printed / pasted | found | 1 | no | +| invoice | existing | One-symbol change | found | 1 | yes | +| invoice | existing | Truncated | not_found | 1 | no | +| invoice | existing | Wrong record type | not_found | 1 | no | +| invoice | existing | Valid but unknown | not_found | 1 | no | +| invoice | existing | Outside allowed scope | not_found | 1 | no | +| invoice | existing | Another valid code | found | 1 | yes | +| invoice | existing | Empty | invalid (input) | 0 | no | +| invoice | alias | Exact | found | 1 | no | +| invoice | alias | Printed / pasted | found | 1 | no | +| invoice | alias | One-symbol change | found | 1 | yes | +| invoice | alias | Truncated | not_found | 1 | no | +| invoice | alias | Wrong record type | not_found | 1 | no | +| invoice | alias | Valid but unknown | not_found | 1 | no | +| invoice | alias | Outside allowed scope | not_found | 1 | no | +| invoice | alias | Another valid code | found | 1 | yes | +| invoice | alias | Empty | invalid (input) | 0 | no | +| invoice | refmint | Exact | found | 1 | no | +| invoice | refmint | Printed / pasted | found | 1 | no | +| invoice | refmint | One-symbol change | invalid (checksum) | 0 | no | +| invoice | refmint | Truncated | invalid (checksum) | 0 | no | +| invoice | refmint | Wrong record type | invalid (prefix) | 0 | no | +| invoice | refmint | Valid but unknown | not_found | 1 | no | +| invoice | refmint | Outside allowed scope | not_found | 1 | no | +| invoice | refmint | Another valid code | found | 1 | yes | +| invoice | refmint | Empty | invalid (input) | 0 | no | + +A valid code for another record still passes its checksum. Scope restrictions apply equally to all routes. +The one-symbol cases were selected to expose a failure mode, not sampled to estimate its frequency. diff --git a/docs/workflows.json b/docs/workflows.json new file mode 100644 index 0000000..7e74145 --- /dev/null +++ b/docs/workflows.json @@ -0,0 +1,640 @@ +{ + "schema": 1, + "recorded": "2026-10-28", + "sample": "synthetic local fixtures, not production support cases", + "measured": "logical lookup calls and identifier lengths, not latency or human error rates", + "sources": { + "examples/workflows/model.mjs": "66c23da94a3fbd33ff33aebad4546a263d719bfd01c76d3543305a0a2939b24b", + "scripts/workflows.mjs": "0b2b59802c8fcec2bb300efe91c550441e920b8d279a2d62461965966e6c924e", + "dist/codec.js": "00fa6a5f15b53f69c421a6a36c9c37bf5125cb7390adced5073b8da8956e9ab5", + "dist/presentation.js": "c80609e88d8d10226e105599f2d57c4eeedde979a960d283b49070f765b91e25", + "dist/bech32m.js": "8541415a0ca6dafede62789c1471afd39dcf91d034fb5aad897b8a1c1219dff2" + }, + "lengths": [ + { + "domain": "fleet", + "route": "existing", + "canonical": 8, + "printed": 8 + }, + { + "domain": "fleet", + "route": "alias", + "canonical": 5, + "printed": 5 + }, + { + "domain": "fleet", + "route": "refmint", + "canonical": 41, + "printed": 49 + }, + { + "domain": "invoice", + "route": "existing", + "canonical": 15, + "printed": 15 + }, + { + "domain": "invoice", + "route": "alias", + "canonical": 5, + "printed": 5 + }, + { + "domain": "invoice", + "route": "refmint", + "canonical": 19, + "printed": 21 + } + ], + "rows": [ + { + "domain": "fleet", + "route": "existing", + "case": "Exact", + "input": "press-07", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "label": "press-07" + }, + "different_record": false + }, + { + "domain": "fleet", + "route": "existing", + "case": "Printed / pasted", + "input": " press-07 ", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "label": "press-07" + }, + "different_record": false + }, + { + "domain": "fleet", + "route": "existing", + "case": "One-symbol change", + "input": "press-08", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "label": "press-08" + }, + "different_record": true + }, + { + "domain": "fleet", + "route": "existing", + "case": "Truncated", + "input": "press-0", + "status": "ambiguous", + "queries": 1, + "matches": 2, + "different_record": false + }, + { + "domain": "fleet", + "route": "existing", + "case": "Wrong record type", + "input": "INV-2026-000042", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "existing", + "case": "Valid but unknown", + "input": "missing-99", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "existing", + "case": "Outside allowed scope", + "input": "press-09", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "existing", + "case": "Another valid code", + "input": "press-08", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "label": "press-08" + }, + "different_record": true + }, + { + "domain": "fleet", + "route": "existing", + "case": "Empty", + "input": "", + "status": "invalid", + "reason": "input", + "queries": 0, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "Exact", + "input": "A1042", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "label": "press-07" + }, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "Printed / pasted", + "input": " A1042 ", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "label": "press-07" + }, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "One-symbol change", + "input": "A1043", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "label": "press-08" + }, + "different_record": true + }, + { + "domain": "fleet", + "route": "alias", + "case": "Truncated", + "input": "A104", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "Wrong record type", + "input": "D1042", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "Valid but unknown", + "input": "Z9999", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "Outside allowed scope", + "input": "A1044", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "alias", + "case": "Another valid code", + "input": "A1043", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "label": "press-08" + }, + "different_record": true + }, + { + "domain": "fleet", + "route": "alias", + "case": "Empty", + "input": "", + "status": "invalid", + "reason": "input", + "queries": 0, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Exact", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqqq0l8ugf", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "label": "press-07" + }, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Printed / pasted", + "input": " ASSET_1PPZ4-P6ZQ-PC5M-G822-W9JY-VE25-GQQQ-0L8U-GF ", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440000", + "label": "press-07" + }, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "One-symbol change", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqqq0l8ugq", + "status": "invalid", + "reason": "checksum", + "queries": 0, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Truncated", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqqq0l8ug", + "status": "invalid", + "reason": "checksum", + "queries": 0, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Wrong record type", + "input": "invoice_1pqp2uywucw", + "status": "invalid", + "reason": "prefix", + "queries": 0, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Valid but unknown", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqq0sj5qh3", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Outside allowed scope", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqqzu6xlmy", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Another valid code", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqqpjfnf4m", + "status": "found", + "queries": 1, + "record": { + "id": "550e8400-e29b-41d4-a716-446655440001", + "label": "press-08" + }, + "different_record": true + }, + { + "domain": "fleet", + "route": "refmint", + "case": "Empty", + "input": "", + "status": "invalid", + "reason": "input", + "queries": 0, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "Exact", + "input": "INV-2026-000042", + "status": "found", + "queries": 1, + "record": { + "id": "42", + "label": "INV-2026-000042" + }, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "Printed / pasted", + "input": " INV-2026-000042 ", + "status": "found", + "queries": 1, + "record": { + "id": "42", + "label": "INV-2026-000042" + }, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "One-symbol change", + "input": "INV-2026-000043", + "status": "found", + "queries": 1, + "record": { + "id": "43", + "label": "INV-2026-000043" + }, + "different_record": true + }, + { + "domain": "invoice", + "route": "existing", + "case": "Truncated", + "input": "INV-2026-00004", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "Wrong record type", + "input": "press-07", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "Valid but unknown", + "input": "missing-99", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "Outside allowed scope", + "input": "INV-2026-000044", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "existing", + "case": "Another valid code", + "input": "INV-2026-000043", + "status": "found", + "queries": 1, + "record": { + "id": "43", + "label": "INV-2026-000043" + }, + "different_record": true + }, + { + "domain": "invoice", + "route": "existing", + "case": "Empty", + "input": "", + "status": "invalid", + "reason": "input", + "queries": 0, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "Exact", + "input": "D1042", + "status": "found", + "queries": 1, + "record": { + "id": "42", + "label": "INV-2026-000042" + }, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "Printed / pasted", + "input": " D1042 ", + "status": "found", + "queries": 1, + "record": { + "id": "42", + "label": "INV-2026-000042" + }, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "One-symbol change", + "input": "D1043", + "status": "found", + "queries": 1, + "record": { + "id": "43", + "label": "INV-2026-000043" + }, + "different_record": true + }, + { + "domain": "invoice", + "route": "alias", + "case": "Truncated", + "input": "D104", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "Wrong record type", + "input": "A1042", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "Valid but unknown", + "input": "Z9999", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "Outside allowed scope", + "input": "D1044", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "alias", + "case": "Another valid code", + "input": "D1043", + "status": "found", + "queries": 1, + "record": { + "id": "43", + "label": "INV-2026-000043" + }, + "different_record": true + }, + { + "domain": "invoice", + "route": "alias", + "case": "Empty", + "input": "", + "status": "invalid", + "reason": "input", + "queries": 0, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Exact", + "input": "invoice_1pqp2uywucw", + "status": "found", + "queries": 1, + "record": { + "id": "42", + "label": "INV-2026-000042" + }, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Printed / pasted", + "input": " INVOICE_1PQP2-UYWU-CW ", + "status": "found", + "queries": 1, + "record": { + "id": "42", + "label": "INV-2026-000042" + }, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "One-symbol change", + "input": "invoice_1pqp2uywucq", + "status": "invalid", + "reason": "checksum", + "queries": 0, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Truncated", + "input": "invoice_1pqp2uywuc", + "status": "invalid", + "reason": "checksum", + "queries": 0, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Wrong record type", + "input": "asset_1ppz4p6zqpc5mg822w9jyve25gqqq0l8ugf", + "status": "invalid", + "reason": "prefix", + "queries": 0, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Valid but unknown", + "input": "invoice_1pqrrp4qcdd", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Outside allowed scope", + "input": "invoice_1pqpvqtdeye", + "status": "not_found", + "queries": 1, + "different_record": false + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Another valid code", + "input": "invoice_1pqptpj6f9u", + "status": "found", + "queries": 1, + "record": { + "id": "43", + "label": "INV-2026-000043" + }, + "different_record": true + }, + { + "domain": "invoice", + "route": "refmint", + "case": "Empty", + "input": "", + "status": "invalid", + "reason": "input", + "queries": 0, + "different_record": false + } + ] +} diff --git a/docs/workflows.md b/docs/workflows.md new file mode 100644 index 0000000..045b392 --- /dev/null +++ b/docs/workflows.md @@ -0,0 +1,106 @@ +# Is another reference useful? + +**Current-source experiment, 28 October 2026. No production integration or user study.** +The [Octofleet question](https://github.com/BenediktSchackenberg/octofleet/issues/166#issuecomment-6443864726) +asks whether phone lookup is a recurring problem and whether a simple alias suffices. +The [Invoice Builder question](https://github.com/piratuks/invoice-builder/issues/331#issuecomment-6447823012) +asks for a PDF-to-lookup demo and evidence beyond existing invoice/quote numbers. +Neither conversation establishes a need to adopt RefMint. + +## Try the workflow + +From current source, with Node 22+ and pnpm 11.25.0: + +```sh +pnpm install --frozen-lockfile --ignore-scripts +pnpm build +pnpm test:workflows +pnpm demo +``` + +Open **http://127.0.0.1:4173/workflows/**. Download the sample PDF, copy each of its +three identifiers into the matching field, and compare the results. Switch to the +phone-support fixture to compare hostname search, a short alias and a UUID reference. +Try exact input, a one-symbol change, truncation, an inaccessible record and another +complete valid code. The form supports manual input as well as preset cases. + +[Demo source](https://github.com/PerchAnton/refmint/tree/main/examples/workflows) · +[Sample PDF](https://github.com/PerchAnton/refmint/blob/main/examples/workflows/sample-invoice.pdf) · +[All recorded outcomes](workflow-results.md) · [Raw evidence and source hashes](workflows.json). +The example is newer than the alpha.2 source tag and is not part of the npm runtime. + +## What is compared + +All routes use the same six synthetic records and the same allowed scope. One logical +lookup means one call to this in-memory fixture, **not** a measured database request. +The ordinary baselines are intentionally small: hostname substring search, exact +invoice-number lookup, and exact alias lookup. No fuzzy matching or autocorrection +is applied; ambiguity is returned for multiple permitted matches. + +Octofleet already has a [search UI](https://github.com/BenediktSchackenberg/octofleet/blob/821d5e8049a0d82ea5bbdd474ac6b443ce9d15e9/frontend/src/components/GlobalSearch.tsx) +and a node-search API; the fixture does not recreate its debounce, backend, OS fields +or ranking. Invoice Builder's existing invoice/quote lookup is acknowledged in the +maintainer's reply. This PDF is an original demo, not an Invoice Builder export, and +we test one invoice rather than claim coverage of every quote/export format. + +| Route | Cost and possible benefit | +| --- | --- | +| Existing identifier | No extra reference; hostname search can work with partial input; invoice number preserves leading zeros | +| Simple alias | Shorter to read; requires allocation, uniqueness and a stored alias-to-record mapping | +| RefMint existing-ID codec | Reversible representation of the original public UUID/uint64; no alias table; namespace and checksum checks; longer and adds a dependency | + +The sample fleet reference has **49 printed characters**, versus **8** for the hostname +and **5** for the alias. The invoice reference has **21**, versus **15** for its number +and **5** for the alias. RefMint is not the shortest phone identifier in this comparison. + +The 54 checks are **2 workflows × 3 routes × 9 selected cases**, not 54 users or a random +error sample. The one-symbol case intentionally lands on a neighboring plain identifier; +the changed RefMint symbol fails its checksum. This exposes a possible silent wrong-record +failure but does not estimate its frequency. A different complete valid RefMint code still +finds the other record. A checksum cannot establish that the user meant that record. + +Valid-but-unknown and out-of-scope references both return `not_found` after one logical +lookup. Every route uses the same scope rule. Browser fixtures are public: they only +illustrate the decision; real authorization must run on the server. + +## Preserve existing identifiers + +- An invoice number such as `INV-2026-000042` remains a string and stays on the PDF. + Encode a separate internal public numeric key only if that is appropriate; do not + strip leading zeros or replace a regulated document, tax or accounting identifier. +- Fleet UUIDs remain the record keys. Enrollment IDs, tokens and credentials are not + inputs to RefMint and are not changed by this experiment. No enrollment API is called. +- RefMint decoding reveals the original ID; it is not encryption. Public references + must never be treated as secrets, authorization, or proof of document authenticity. + +## Reproduce PDF → text → lookup + +The PDF is generated from the same fixture. Verification extracts text from the actual +PDF, checks the original invoice number and all three identifiers, and runs them through +the lookup functions. It also checks the local form link and a mistyped extracted code. +No OCR or human transcription performance is measured. + +```sh +pnpm workflows +python -m pip install -r examples/workflows/requirements-pdf.txt +python examples/workflows/pdf.py --check +``` + +Use a Python virtual environment. Omit `--check` to regenerate the deterministic PDF. +These Python packages are development tools, not npm/runtime dependencies. CI checks +the PDF and its extracted identifiers separately from browser tests. Regenerate evidence +with `pnpm workflows`; use `pnpm test:workflows` to reject stale outcomes or source hashes. + +## What would justify integration? + +Real support transcripts and user outcomes have not been supplied. Keep this an external +prototype until the owner can compare their current workflow, a plain alias and RefMint +on the **same consented, anonymized tasks**. Record completion, wrong-record selections, +corrections, time to the intended record, partial-search usefulness, and setup cost. +Rotate route order and include easy copy/paste cases, not just hand-picked typos. +Keep raw identifiers/credentials private; publish only aggregate results and sample size. + +If ordinary lookup already succeeds reliably, or the alias works as well with less +effort, keep it. Adopt RefMint only if a recurring need and a meaningful improvement +outweigh longer input, maintenance and alpha-policy costs. Passing this fixture alone +is not an integration recommendation. [When not to use RefMint](when-not-to-use.md). diff --git a/examples/workflows/app.mjs b/examples/workflows/app.mjs new file mode 100644 index 0000000..8841853 --- /dev/null +++ b/examples/workflows/app.mjs @@ -0,0 +1,31 @@ +import { routes, records, identifier, lookup, scenarios } from './model.mjs'; + +const select = document.querySelector('#domain'); +function run() { + for (const route of routes) { + const result = lookup(select.value, route, document.getElementById(route).value); + const output = document.getElementById(`${route}-result`); + output.dataset.state = result.status; + const message = result.status === 'found' ? `Found: ${result.record.label}. Original ID: ${result.record.id}.` + : result.status === 'invalid' ? `Invalid input (${result.reason}).` + : result.status === 'ambiguous' ? `${result.matches} permitted records match. Refine the search.` + : 'No accessible record found.'; + output.textContent = `${message} Logical lookups: ${result.queries}.`; + } +} +function useCase(name) { + for (const route of routes) document.getElementById(route).value = scenarios(select.value, route).find(s => s.name === name).input; + run(); +} +function update() { + const record = records.find(r => r.domain === select.value); + document.querySelector('#context').textContent = select.value === 'invoice' + ? 'Same PDF, same permitted invoice. The invoice number is a string: its leading zeros and prefix are preserved.' + : 'A caller describes press-07. Compare the existing hostname search with a five-character alias and a UUID-based reference. Enrollment and credentials stay separate.'; + for (const route of routes) document.getElementById(`${route}-length`).textContent = `${identifier(record, route).length} canonical characters / ${identifier(record, route, true).length} printed characters`; + useCase('Exact'); +} +document.querySelector('#lookup').addEventListener('submit', event => { event.preventDefault(); run(); }); +select.addEventListener('change', update); +for (const button of document.querySelectorAll('[data-case]')) button.addEventListener('click', () => useCase(button.dataset.case)); +update(); diff --git a/examples/workflows/index.html b/examples/workflows/index.html new file mode 100644 index 0000000..6c7ff8a --- /dev/null +++ b/examples/workflows/index.html @@ -0,0 +1,29 @@ + + + + +Do you need another reference? · RefMint + +
+

REFMINT / WORKFLOW LAB

Do you need another reference?

+

Compare an existing identifier, a simple local alias, and a checksummed reference on the same records.

+ +
+

Download sample invoice PDF +

The PDF includes all three identifiers. Copy one into its field below. The original invoice number remains unchanged.

+
+
+

Existing identifier

Keep the number or hostname people already use.

+

Simple alias

A short label with an application-owned mapping.

+

RefMint

Encode a public database ID and validate its checksum.

+
+

What the result can tell you

“Logical lookups” count calls in this in-memory fixture, not network requests or database latency. No enrollment ID, credential, invoice number or tax identifier is replaced.

+ +
+ diff --git a/examples/workflows/invoice-data.json b/examples/workflows/invoice-data.json new file mode 100644 index 0000000..97792ab --- /dev/null +++ b/examples/workflows/invoice-data.json @@ -0,0 +1,8 @@ +{ + "invoice_number": "INV-2026-000042", + "database_id": "42", + "alias": "D1042", + "reference": "INVOICE_1PQP2-UYWU-CW", + "lookup_url": "http://127.0.0.1:4173/workflows/", + "warning": "DEMONSTRATION ONLY - NOT A TAX INVOICE" +} diff --git a/examples/workflows/model.mjs b/examples/workflows/model.mjs new file mode 100644 index 0000000..a36efe0 --- /dev/null +++ b/examples/workflows/model.mjs @@ -0,0 +1,75 @@ +// Local fixture, not Octofleet/Invoice Builder's implementation or production data. +import { createReferenceCodec } from '../../dist/codec.js'; + +export const domains = ['fleet', 'invoice']; +export const routes = ['existing', 'alias', 'refmint']; +export const records = Object.freeze([ + Object.freeze({ domain: 'fleet', id: '550e8400-e29b-41d4-a716-446655440000', existing: 'press-07', alias: 'A1042', owner: 'demo-team' }), + Object.freeze({ domain: 'fleet', id: '550e8400-e29b-41d4-a716-446655440001', existing: 'press-08', alias: 'A1043', owner: 'demo-team' }), + Object.freeze({ domain: 'fleet', id: '550e8400-e29b-41d4-a716-446655440002', existing: 'press-09', alias: 'A1044', owner: 'other-team' }), + Object.freeze({ domain: 'invoice', id: '42', existing: 'INV-2026-000042', alias: 'D1042', owner: 'demo-team' }), + Object.freeze({ domain: 'invoice', id: '43', existing: 'INV-2026-000043', alias: 'D1043', owner: 'demo-team' }), + Object.freeze({ domain: 'invoice', id: '44', existing: 'INV-2026-000044', alias: 'D1044', owner: 'other-team' }) +]); + +export function codec(domain) { + if (!domains.includes(domain)) throw new Error('unknown domain'); + return createReferenceCodec({ prefix: domain === 'fleet' ? 'asset' : 'invoice', kind: domain === 'fleet' ? 'uuid' : 'uint64', version: 1 }); +} + +export function identifier(record, route, printed = false) { + if (!routes.includes(route)) throw new Error('unknown route'); + if (route !== 'refmint') return record[route]; + const c = codec(record.domain); + const value = record.domain === 'fleet' ? record.id : BigInt(record.id); + const id = c.encode(value); + return printed ? c.format(id).toUpperCase() : id; +} + +export function lookup(domain, route, input, actor = 'demo-team') { + const c = codec(domain); + if (!routes.includes(route)) throw new Error('unknown route'); + if (typeof input !== 'string' || input.length > 128 || !input.trim()) return { status: 'invalid', reason: 'input', queries: 0 }; + let matches; + if (route === 'refmint') { + const result = c.diagnose(input); + if (!result.ok) return { status: 'invalid', reason: result.error, queries: 0 }; + matches = r => r.id === String(result.value); + } else { + const query = input.trim().toLowerCase(); + // Fleet's existing baseline supports hostname substring search; invoice/alias + // use exact strings. This deliberately does not recreate either upstream app. + matches = r => route === 'existing' && domain === 'fleet' + ? r.existing.toLowerCase().includes(query) : r[route].toLowerCase() === query; + } + // Apply the same scope to every route. In production this belongs on the server. + const found = records.filter(r => r.domain === domain && r.owner === actor && matches(r)); + if (!found.length) return { status: 'not_found', queries: 1 }; + if (found.length > 1) return { status: 'ambiguous', queries: 1, matches: found.length }; + return { status: 'found', queries: 1, record: { id: found[0].id, label: found[0].existing } }; +} + +export function changedSymbol(input) { + return input.slice(0, -1) + (input.endsWith('q') ? 'p' : 'q'); +} + +export function scenarios(domain, route) { + const [target, neighbor, denied] = records.filter(r => r.domain === domain); + const original = identifier(target, route); + const otherDomain = records.find(r => r.domain !== domain); + const unknown = { ...target, id: domain === 'fleet' ? target.id.slice(0, -1) + 'f' : '99', existing: 'missing-99', alias: 'Z9999' }; + // Select a one-symbol neighbor for the plain identifiers. The corresponding + // RefMint case changes one symbol WITHOUT recomputing its checksum. + const typo = route === 'refmint' ? changedSymbol(original) : identifier(neighbor, route); + return [ + { name: 'Exact', input: original, expected: 'found', intended: target.id }, + { name: 'Printed / pasted', input: ` ${identifier(target, route, true)} `, expected: 'found', intended: target.id }, + { name: 'One-symbol change', input: typo, expected: route === 'refmint' ? 'invalid' : 'found', intended: target.id }, + { name: 'Truncated', input: original.slice(0, -1), expected: route === 'refmint' ? 'invalid' : domain === 'fleet' && route === 'existing' ? 'ambiguous' : 'not_found', intended: target.id }, + { name: 'Wrong record type', input: identifier(otherDomain, route), expected: route === 'refmint' ? 'invalid' : 'not_found' }, + { name: 'Valid but unknown', input: identifier(unknown, route), expected: 'not_found' }, + { name: 'Outside allowed scope', input: identifier(denied, route), expected: 'not_found' }, + { name: 'Another valid code', input: identifier(neighbor, route), expected: 'found', intended: target.id }, + { name: 'Empty', input: '', expected: 'invalid' } + ]; +} diff --git a/examples/workflows/pdf.py b/examples/workflows/pdf.py new file mode 100644 index 0000000..0dcb3ea --- /dev/null +++ b/examples/workflows/pdf.py @@ -0,0 +1,102 @@ +"""Build/check a deterministic one-page demo; replay identifiers extracted from its PDF.""" +import argparse +from io import BytesIO +import json +from pathlib import Path +import subprocess +import tempfile + +from pypdf import PdfReader +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib.colors import HexColor + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + + +def generate(data): + stream = BytesIO() + c = Canvas(stream, pagesize=(595.28, 841.89), invariant=1) + c.setTitle('RefMint - synthetic PDF lookup example') + c.setAuthor('RefMint') + c.setFillColor(HexColor('#17323d')) + c.rect(0, 675, 596, 168, fill=1, stroke=0) + c.setFillColor(HexColor('#8bdbbf')) + c.setFont('Helvetica-Bold', 10) + c.drawString(42, 793, 'REFMINT / WORKFLOW LAB') + c.setFillColor(HexColor('#ffffff')) + c.setFont('Helvetica-Bold', 29) + c.drawString(42, 745, 'From PDF to the right record') + c.setFont('Helvetica', 11) + c.drawString(42, 712, 'Three lookup routes. One unchanged invoice number.') + c.setFillColor(HexColor('#a9680e')) + c.setFont('Helvetica-Bold', 10) + c.drawString(42, 642, data['warning']) + fields = [('01 / EXISTING NUMBER', 'Invoice number', data['invoice_number']), + ('02 / SIMPLE LOCAL ALIAS', 'Local alias', data['alias']), + ('03 / CHECKSUMMED REFERENCE', 'RefMint reference', data['reference'])] + for y, (heading, label, value) in zip([600, 487, 374], fields): + c.setFillColor(HexColor('#edf4f2')) + c.roundRect(32, y - 68, 531, 92, 8, fill=1, stroke=0) + c.setFillColor(HexColor('#086b59')) + c.setFont('Helvetica-Bold', 10) + c.drawString(46, y, heading) + c.setFillColor(HexColor('#17323d')) + c.setFont('Helvetica', 12) + c.drawString(46, y - 32, f'{label}: {value}') + c.setFillColor(HexColor('#17323d')) + c.setFont('Helvetica-Bold', 13) + c.drawString(42, 269, 'Try all three in the local form') + c.setFont('Helvetica', 11) + for y, line in [(245, 'Start the repository demo with pnpm demo, then open:'), + (220, data['lookup_url']), + (192, 'Copy an identifier above into its matching field. Try a one-symbol change.'), + (170, 'Compare found, invalid, ambiguous and inaccessible results.')]: + c.drawString(42, y, line) + c.linkURL(data['lookup_url'], (42, 216, 360, 233), relative=0) + c.setStrokeColor(HexColor('#c5d5ce')) + c.line(42, 133, 553, 133) + c.setFont('Helvetica', 10) + for y, line in [(112, 'Original number and regulated identifiers stay unchanged.'), + (96, 'A checksum is public. It does not authorize access or prove authenticity.'), + (80, 'Alpha API; frozen existing-ID v1 wire format. No human-entry study.')]: + c.drawString(42, y, line) + c.showPage() + c.save() + return stream.getvalue() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--check', action='store_true') + args = parser.parse_args() + data = json.loads((HERE / 'invoice-data.json').read_text(encoding='utf-8')) + pdf = HERE / 'sample-invoice.pdf' + content = generate(data) + if args.check: + assert pdf.read_bytes() == content, 'PDF fixture is stale; regenerate with pinned dependencies' + else: + pdf.write_bytes(content) + reader = PdfReader(pdf) + assert len(reader.pages) == 1 + text = reader.pages[0].extract_text() + extracted = {} + for route, label, expected in [('existing', 'Invoice number', data['invoice_number']), + ('alias', 'Local alias', data['alias']), + ('refmint', 'RefMint reference', data['reference'])]: + lines = [line.removeprefix(label + ': ') for line in text.splitlines() if line.startswith(label + ': ')] + assert lines == [expected], (route, lines) + extracted[route] = lines[0] + assert data['warning'] in text + assert any(str(a.get_object().get('/A', {}).get('/URI', '')) == data['lookup_url'] + for a in reader.pages[0].get('/Annots', [])) + # Exercise lookup using actual extracted PDF text, not just its source JSON. + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'extracted.json' + path.write_text(json.dumps(extracted), encoding='utf-8') + subprocess.run(['node', str(HERE / 'verify-extracted.mjs'), str(path)], cwd=ROOT, check=True) + print('One-page PDF: three extracted identifiers, unchanged invoice number, link and lookups verified.') + + +if __name__ == '__main__': + main() diff --git a/examples/workflows/requirements-pdf.txt b/examples/workflows/requirements-pdf.txt new file mode 100644 index 0000000..fd684c3 --- /dev/null +++ b/examples/workflows/requirements-pdf.txt @@ -0,0 +1,3 @@ +# Development-only PDF fixture generation and extraction; not npm dependencies. +reportlab==4.4.9 +pypdf==6.10.0 diff --git a/examples/workflows/sample-invoice.pdf b/examples/workflows/sample-invoice.pdf new file mode 100644 index 0000000..377bd6f Binary files /dev/null and b/examples/workflows/sample-invoice.pdf differ diff --git a/examples/workflows/style.css b/examples/workflows/style.css new file mode 100644 index 0000000..6744957 --- /dev/null +++ b/examples/workflows/style.css @@ -0,0 +1 @@ +:root{font:16px/1.6 system-ui,sans-serif;color:#17323d;background:#f1f5f4}*{box-sizing:border-box}body{margin:0}main{max-width:1180px;margin:auto;padding:40px 24px}header{max-width:850px}.eyebrow{letter-spacing:.14em;font-size:.78rem;font-weight:700;color:#086b59}h1{font-size:clamp(2rem,5vw,3.4rem);line-height:1.12;letter-spacing:-.04em;margin:16px 0}h2{line-height:1.3;font-size:1.3rem}aside{margin:30px 0;background:#fff3d9;border-left:4px solid #ad7921;padding:18px 22px}.controls{padding-bottom:22px}label{display:block;font-weight:650;font-size:.9rem}select,input{font:inherit;color:inherit;background:white;border:1px solid #a7b9b5;border-radius:7px;padding:10px;width:100%}select{max-width:360px}.cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:18px}article{background:white;padding:22px;border:1px solid #d4dfdc;border-radius:12px}article>p{min-height:52px}input{font:14px/1.6 ui-monospace,monospace}article p.length{min-height:0;color:#51686e;font-size:.82rem}output{display:block;padding:12px;background:#edf4f2;min-height:110px;overflow-wrap:anywhere;border-radius:7px}output[data-state=invalid]{background:#fff3d9}output[data-state=found]{border-left:3px solid #12846a}button{font:inherit;cursor:pointer;border:1px solid #b4c7c0;background:white;color:#17323d;border-radius:7px;padding:9px 14px}.presets{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 18px}.primary{background:#0c6857;color:white;border:0;margin:18px 0 24px;padding:12px 22px}a{color:#086b59}footer{margin-top:36px;padding-top:20px;border-top:1px solid #c5d5ce;font-size:.85rem;color:#536b71}:focus-visible{outline:3px solid #a87522;outline-offset:3px}@media(max-width:820px){.cards{grid-template-columns:1fr}main{padding:24px 16px}article>p{min-height:0}} diff --git a/examples/workflows/verify-extracted.mjs b/examples/workflows/verify-extracted.mjs new file mode 100644 index 0000000..8b52d8a --- /dev/null +++ b/examples/workflows/verify-extracted.mjs @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { lookup, changedSymbol } from './model.mjs'; + +const inputs = JSON.parse(await readFile(process.argv[2], 'utf8')); +for (const route of ['existing', 'alias', 'refmint']) { + const result = lookup('invoice', route, inputs[route]); + assert.equal(result.status, 'found'); + assert.equal(result.record.id, '42'); + assert.equal(result.record.label, 'INV-2026-000042'); +} +assert.deepEqual(lookup('invoice', 'refmint', changedSymbol(inputs.refmint.toLowerCase())), { status: 'invalid', reason: 'checksum', queries: 0 }); diff --git a/package.json b/package.json index 55c05b7..35357a7 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,8 @@ "demo": "node scripts/serve.mjs", "test:browser": "playwright test", "test:comparison": "node scripts/compare.mjs --check", + "workflows": "node scripts/workflows.mjs", + "test:workflows": "node scripts/workflows.mjs --check", "prepack": "pnpm build" }, "packageManager": "pnpm@11.25.0", diff --git a/scripts/demo-server.mjs b/scripts/demo-server.mjs index cbb76d6..0141cda 100644 --- a/scripts/demo-server.mjs +++ b/scripts/demo-server.mjs @@ -12,6 +12,9 @@ for (const name of ['index.html', 'app.mjs', 'style.css', 'bech32-adapter.mjs', files.set('/comparison/' + (name === 'index.html' ? '' : name), [`../examples/comparison/${name}`, `${type}; charset=utf-8`]); } files.set('/docs/comparison.json', ['../docs/comparison.json', 'application/json; charset=utf-8']); +for (const [name, type] of [['index.html', 'text/html; charset=utf-8'], ['style.css', 'text/css; charset=utf-8'], ['app.mjs', 'text/javascript; charset=utf-8'], ['model.mjs', 'text/javascript; charset=utf-8'], ['sample-invoice.pdf', 'application/pdf']]) { + files.set('/workflows/' + (name === 'index.html' ? '' : name), [`../examples/workflows/${name}`, type]); +} export function createDemoServer() { return createServer(async (req, res) => { diff --git a/scripts/workflows.mjs b/scripts/workflows.mjs new file mode 100644 index 0000000..d8ed557 --- /dev/null +++ b/scripts/workflows.mjs @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { domains, routes, records, identifier, lookup, scenarios } from '../examples/workflows/model.mjs'; + +const root = new URL('../', import.meta.url); +const rows = []; +for (const domain of domains) for (const route of routes) for (const scenario of scenarios(domain, route)) { + const result = lookup(domain, route, scenario.input); + assert.equal(result.status, scenario.expected, `${domain}/${route}/${scenario.name}`); + rows.push({ domain, route, case: scenario.name, input: scenario.input, ...result, + different_record: result.status === 'found' && scenario.intended !== undefined && result.record.id !== scenario.intended }); +} +const lengths = domains.flatMap(domain => routes.map(route => { + const record = records.find(r => r.domain === domain); + return { domain, route, canonical: identifier(record, route).length, printed: identifier(record, route, true).length }; +})); +const sources = ['examples/workflows/model.mjs', 'scripts/workflows.mjs', 'dist/codec.js', 'dist/presentation.js', 'dist/bech32m.js']; +const hashes = {}; +for (const path of sources) hashes[path] = createHash('sha256').update((await readFile(new URL(path, root), 'utf8')).replaceAll('\r\n', '\n')).digest('hex'); +const evidence = { schema: 1, recorded: '2026-10-28', sample: 'synthetic local fixtures, not production support cases', + measured: 'logical lookup calls and identifier lengths, not latency or human error rates', sources: hashes, lengths, rows }; +const invoice = records.find(r => r.domain === 'invoice'); +const pdfData = { invoice_number: invoice.existing, database_id: invoice.id, alias: invoice.alias, + reference: identifier(invoice, 'refmint', true), lookup_url: 'http://127.0.0.1:4173/workflows/', + warning: 'DEMONSTRATION ONLY - NOT A TAX INVOICE' }; +const outputs = new Map([ + ['docs/workflows.json', JSON.stringify(evidence, null, 2) + '\n'], + ['examples/workflows/invoice-data.json', JSON.stringify(pdfData, null, 2) + '\n'] +]); +const table = ['# Local lookup results', '', 'Synthetic fixtures, recorded 28 October 2026. No production support cases or human timing data.', '', + '| Domain | Route | Canonical characters | Printed characters |', '| --- | --- | ---: | ---: |', + ...lengths.map(r => `| ${r.domain} | ${r.route} | ${r.canonical} | ${r.printed} |`), '', + '| Domain | Route | Case | Result | Logical lookups | Different record from intended? |', '| --- | --- | --- | --- | ---: | --- |', + ...rows.map(r => `| ${r.domain} | ${r.route} | ${r.case} | ${r.status}${r.reason ? ` (${r.reason})` : ''} | ${r.queries} | ${r.different_record ? 'yes' : 'no'} |`), '', + 'A valid code for another record still passes its checksum. Scope restrictions apply equally to all routes.', + 'The one-symbol cases were selected to expose a failure mode, not sampled to estimate its frequency.', ''].join('\n'); +outputs.set('docs/workflow-results.md', table); +for (const [path, content] of outputs) { + const url = new URL(path, root); + if (process.argv.includes('--check')) assert.equal((await readFile(url, 'utf8')).replaceAll('\r\n', '\n'), content, `stale ${path}`); + else await writeFile(url, content); +} +console.log(`${rows.length} fixture cases checked; ${fileURLToPath(new URL('docs/workflows.json', root))}`); diff --git a/tests/browser/workflows.spec.mjs b/tests/browser/workflows.spec.mjs new file mode 100644 index 0000000..46e8035 --- /dev/null +++ b/tests/browser/workflows.spec.mjs @@ -0,0 +1,36 @@ +import { test, expect } from '@playwright/test'; + +test('three routes show successes, typo trade-offs and equal authorization outcomes', async ({ page, request }) => { + const errors = []; + page.on('pageerror', error => errors.push(error.message)); + await page.goto('/workflows/'); + for (const domain of ['invoice', 'fleet']) { + await page.getByLabel('Workflow', { exact: true }).selectOption(domain); + for (const route of ['existing', 'alias', 'refmint']) await expect(page.locator(`#${route}-result`)).toHaveAttribute('data-state', 'found'); + await page.getByRole('button', { name: 'One-symbol change', exact: true }).click(); + await expect(page.locator('#refmint-result')).toContainText('Logical lookups: 0'); + await expect(page.locator('#alias-result')).toHaveAttribute('data-state', 'found'); + await page.getByRole('button', { name: 'Outside allowed scope', exact: true }).click(); + for (const route of ['existing', 'alias', 'refmint']) await expect(page.locator(`#${route}-result`)).toContainText('No accessible record found'); + await page.getByRole('button', { name: 'Another valid code', exact: true }).click(); + await expect(page.locator('#refmint-result')).toHaveAttribute('data-state', 'found'); + } + const pdf = await request.get('/workflows/sample-invoice.pdf'); + expect(pdf.status()).toBe(200); + expect(pdf.headers()['content-type']).toBe('application/pdf'); + expect((await pdf.body()).subarray(0, 5).toString()).toBe('%PDF-'); + expect(errors).toEqual([]); +}); + +test('manual pasted identifiers work on a phone viewport', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await page.goto('/workflows/'); + const reference = await page.locator('#refmint').inputValue(); + await page.getByLabel('RefMint reference', { exact: true }).fill(` ${reference.toUpperCase()} `); + await page.getByRole('button', { name: 'Look up all three', exact: true }).click(); + await expect(page.locator('#refmint-result')).toContainText('INV-2026-000042'); + await page.getByLabel('Local alias', { exact: true }).fill(''); + await page.getByRole('button', { name: 'Look up all three', exact: true }).click(); + await expect(page.locator('#alias-result')).toHaveAttribute('data-state', 'not_found'); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); +}); diff --git a/tests/workflows.test.mjs b/tests/workflows.test.mjs new file mode 100644 index 0000000..6c2b9a3 --- /dev/null +++ b/tests/workflows.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { domains, routes, records, identifier, lookup, scenarios } from '../examples/workflows/model.mjs'; + +test('lookup alternatives preserve identifiers and scope across every case', () => { + const before = JSON.stringify(records); + for (const domain of domains) for (const route of routes) { + for (const item of scenarios(domain, route)) assert.equal(lookup(domain, route, item.input).status, item.expected); + const denied = records.find(r => r.domain === domain && r.owner === 'other-team'); + assert.deepEqual(lookup(domain, route, identifier(denied, route)), { status: 'not_found', queries: 1 }); + assert.equal(lookup(domain, route, identifier(denied, route), 'other-team').status, 'found'); + } + assert.equal(JSON.stringify(records), before); + assert.equal(records.find(r => r.domain === 'invoice').existing, 'INV-2026-000042'); +}); + +test('checksum cannot authenticate or detect a different complete valid code', () => { + for (const domain of domains) { + const [, neighbor] = records.filter(r => r.domain === domain); + assert.equal(lookup(domain, 'refmint', identifier(neighbor, 'refmint')).record.id, neighbor.id); + } +}); + +test('existing hostname search can be more useful than an exact reference', () => { + assert.deepEqual(lookup('fleet', 'existing', 'press'), { status: 'ambiguous', queries: 1, matches: 2 }); + assert.equal(lookup('fleet', 'refmint', 'press').status, 'invalid'); + assert.ok(identifier(records[0], 'refmint', true).length > identifier(records[0], 'alias').length); +});