From c514a5f9c549d43bfa4eed54a8c2312e340b1f0d Mon Sep 17 00:00:00 2001 From: Gati Varshney <171050892+gativarshney@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:05:52 +0530 Subject: [PATCH 1/2] docs(assistant): add printer assistant documentation --- docs/foomatic-assistant-queries.md | 33 ++++++++ docs/foomatic-assistant.md | 132 +++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 docs/foomatic-assistant-queries.md create mode 100644 docs/foomatic-assistant.md diff --git a/docs/foomatic-assistant-queries.md b/docs/foomatic-assistant-queries.md new file mode 100644 index 00000000..4e15f02c --- /dev/null +++ b/docs/foomatic-assistant-queries.md @@ -0,0 +1,33 @@ +# Foomatic Printer Assistant — Supported Queries + +What the assistant can answer, with representative phrasings. Architecture and semantics: [foomatic-assistant.md](./foomatic-assistant.md). + +## Look up a printer + +> "HP 2500C" · "tell me about the HP LaserJet 4" · "Brother HL-1050" · "hl 1050" + +Punctuation, spacing, and casing variants unify; "Hewlett Packard" is understood as HP. Partial names ("hp deskjet 5") list candidates to choose from; models that are not in the database ("Brother HL-2270DW") get an honest not-found answer with the closest real models. + +## Filter by capabilities + +> "find a colour laser printer" · "monochrome laser with PCL 5" · "printers with good Linux support" · "600 dpi inkjet" · "postscript 3 printers" · "show me Canon printers" · "colour laser with duplex and PCL" + +Filterable: colour vs monochrome, mechanism type (laser / inkjet / dot-matrix), resolution ("600 dpi", "high resolution" = at least 1200 dpi, disclosed), PostScript / PCL (with optional levels), manufacturer, and Linux support grade ("good" = Perfect or Mostly; "perfect" = Perfect only). Results state their ordering and totals. Fields the database doesn't record for a printer exclude it from matches but are reported as missing data, never as a "no". Duplex is recorded for zero printers, so duplex requests are answered with that fact instead of fake results. Constraints outside the data model ("purple", "cheap", "wireless") are named as unfilterable, never silently dropped. + +## Similar printers and explanations + +> "what printers are similar to this?" (on a printer page) · "printers similar to HP 2500C" · "why was HP LaserJet 4P recommended?" · "what are better alternatives?" + +Similarity answers come from the site's precomputed recommendation data, in its original order, with each candidate's similarity percentage, confidence tier, and its own Linux support grade. "Better alternatives" asks which dimension to compare (Linux support, overall similarity, resolution, driver options) unless one was named. + +## Support, drivers, comparison + +> "how good is the Linux support?" (on a printer page) · "does the HP LaserJet 4 work on Linux?" · "which driver does this printer use?" · "which printers use hplip?" · "which printers use the same driver?" · "compare HP 2500C and HP DeskJet 560C" + +Driver questions distinguish the recommended driver from the full listed set; "same driver" uses the recommended driver and says so. Comparisons show recorded fields side by side, with "Not recorded" for gaps, and never declare an overall winner. + +## Meta + +> "what does Perfect mean?" · "what does the similarity score mean?" · "what can you do?" + +These three topics are answered from fixed text grounded in the site's own definitions. Definition questions the database cannot answer ("what is PostScript?") get an honest scope statement rather than a printer list — the data records which printers support such things, not what they are. Anything else outside the printer database — weather, general Linux help, documentation content — gets a clear scope statement; the assistant is not a general documentation chatbot. diff --git a/docs/foomatic-assistant.md b/docs/foomatic-assistant.md new file mode 100644 index 00000000..ab7f264e --- /dev/null +++ b/docs/foomatic-assistant.md @@ -0,0 +1,132 @@ +# Foomatic Printer Assistant — Architecture + +The assistant is a site-wide, fully client-side natural-language interface to the Foomatic data this site already generates. There is no external service, no machine-learning model, and no server: every answer is computed in the browser from the same static JSON artifacts the rest of the site fetches, so every claim the assistant makes is traceable to a record in `public/foomatic-db/`. + +Related docs: [foomatic-assistant-queries.md](./foomatic-assistant-queries.md) (supported query shapes), [foomatic-data-formats.md](./foomatic-data-formats.md) (artifact schemas), [foomatic-recommendation-quality.md](./foomatic-recommendation-quality.md) (similarity score semantics). + +## Pipeline + +``` +free text + → normalize.ts lowercase / punctuation-fold / fused forms ("HL-1050" ≡ "hl 1050") + → entities.ts printer & driver references from the real catalogue + → lexicon.ts capability phrases → typed filters + → parse.ts ordered rule cascade → typed AssistantQuery + → execute.ts the only I/O layer: catalogue filter / shard fetch + → respond.ts typed ResponsePlan blocks (pure templates) + → components/assistant/ React rendering (no HTML strings anywhere) +``` + +`engine.ts` composes the stages; the UI, the unit tests, and the eval harness all call the same `runAssistant()`, so what is tested is exactly what ships. Natural language never reaches execution: the executor operates only on the typed `AssistantQuery` produced by the parser. + +## Intent precedence + +Classification is an ordered deterministic rule cascade (`parse.ts`); earlier rules are more specific so they can never be swallowed by generic vocabulary: + +1. COMPARISON — two printer refs + compare/vs/difference +2. EXPLANATION — "why" + recommendation vocabulary +3. SIMILAR_PRINTERS — similar/alternatives/"better …" +4. DRIVER_SEARCH — printers-for-driver phrasing, "same driver", "this driver" +5. DRIVER_LOOKUP — driver-of-printer phrasing +6. GENERAL_INFO — closed topics: support grades, similarity, help. Sits above + SUPPORT_QUERY so meaning-questions ("what does perfect support mean?") are + answered generally — page context must never capture a meaning-question + and answer it with the current printer's own values. Definition-shaped + questions without an explicit reference ("what is PostScript?", "what is a + printer driver?") get the grades definition when they are about Linux + support and an honest scope statement otherwise — never a printer list and + never a context-captured answer, because the data records which printers + support these things, not what they are. +7. SUPPORT_QUERY — support question about one specific printer +8. CAPABILITY_SEARCH — recommend/best or any capability filters +9. PRINTER_LOOKUP — a printer reference and nothing more specific +10. UNSUPPORTED — unclear-but-domain, or out-of-domain + +Suggestion chips obey a strict rule: a chip's query is the exact text of its +label, it must parse to a supported intent (tested mechanically for every +chip the system can produce), and contextual phrasing ("this printer", +"this driver") stays in the message and resolves through the typed page +context — current-page values are never substituted into the question. + +## Semantics that must not regress + +- **Unknown never means false.** Filters select on recorded values only. A printer whose `color` is `"unknown"` matches neither "colour" nor "monochrome", and is counted separately (`excludedUnknown`) so responses can say "N more printers don't record this" instead of implying absence. PDL evidence is positive-only: no recorded PostScript level or token means *unknown*, never "no PostScript". +- **Duplex.** The current dataset records duplex for zero printers. The lexicon understands duplex vocabulary, but execution reports `INSUFFICIENT_DATA` (and still answers any other constraints in the query). No result may ever imply a printer does or does not support duplex. +- **Similarity is PR #224's, verbatim.** SIMILAR_PRINTERS/EXPLANATION read `recommendations/.json` shards: shard order is preserved, scores are never recomputed or re-ranked, tiers come from the imported `confidenceTier()` in `lib/foomatic/scoring.ts`, and `sharedFeatures` strings are quoted verbatim. The score is described as "N% similarity", never as a probability or a compatibility promise, and the candidate's own Foomatic Linux support grade is always stated separately. +- **"Best" never ranks.** A criteria-free "best printer"/"recommend a printer" produces a clarification — the database ranks nothing overall, and no hidden default score exists. +- **"Better" never assumes a dimension.** "Better alternatives" without a named dimension asks "Better in what way?" with four defined dimensions (Linux support, overall similarity, resolution, driver options). Each dimension is a disclosed predicate over the existing recommendation shard (grade rank strictly higher with Unknown excluded; recorded `maxDpi` strictly higher; `driverCount` strictly higher; or the plain shard). Shard order is preserved in all cases. +- **Search ordering is disclosed, not scored.** Results are ordered by explicitly requested gradable criteria first (resolution when a dpi constraint was given), then the documented default: support-status rank, then listed-driver count, then id (the same deterministic tie-break convention as `scoreThenIdComparator`). Every search response states its ordering. +- **Unrecognized constraints are surfaced.** "purple laser printer" runs the laser filter and explicitly says that "purple" cannot be filtered — constraints are never silently dropped, and an unanswerable constraint never fabricates a NO_MATCHES claim. + +## Data access + +All artifacts are static JSON under `public/`, fetched with `withBasePath()` and cached per tab with the promise-cache pattern from `RecommendedPrintersSection.tsx` (404 ⇒ empty, network error ⇒ cache eviction so retries refetch). Measured sizes at the current snapshot (raw / gzip): + +| Artifact | When loaded | Size | +|---|---|---| +| `printersMap.json` (catalogue) | first assistant query | 1,396,087 B / 71,147 B | +| `driversMap.json` | first assistant query | 85,064 B / 5,519 B | +| `printers/.json` | per printer asked about | e.g. HP-LaserJet_4: 21,318 B / 5,901 B | +| `recommendations/.json` | per similarity question | e.g. HP-LaserJet_4: 3,342 B / 402 B | +| `drivers/.json` | per driver question | 0.5–268 KB raw | + +Nothing loads until the assistant is opened; the launcher itself carries no data. The 44 MB `printers.json`, the feature matrix, and the combined recommendations file are never fetched. Query latency measured by the harness (Node, warm caches, 468 samples): median 0.08 ms, p95 4.46 ms — network transfer of the lazy artifacts dominates, not computation. + +JavaScript cost (measured on a clean `next build`): the shared First Load JS is unchanged by the assistant (103 kB before and after — the launcher's increment is below the build output's rounding, inside the layout chunk), and the entire lazy assistant chunk (panel UI + engine) is roughly 62 KB raw / 18 KB gzip, loaded only on first open. Exact byte counts drift with every patch; re-measure on a clean build when precision matters. + +### Catalogue capability fields + +To make capability queries answerable without extra fetches, the `printersMap.json` projection carries five optional fields beyond the original directory schema. Records are built by `toPrinterSummary()` in [`lib/foomatic/catalog.ts`](../lib/foomatic/catalog.ts) — the single source of truth for this schema — and the file is written minified. + +```ts +maxDpi?: number // from Printer.maxDpi (when a number) +psLevel?: number // from Printer.psLevel (when a number) +pclLevel?: number // from Printer.pclLevel (when a number) +cs?: string[] // Printer.commandsetTokens (when non-empty) +rd?: string // recommended driver family: Printer.recommended_driver + // collapsed via normalizeDriverFamily() (when present) +``` + +Each field is OMITTED when the source data does not establish it — an absent key means "not recorded in Foomatic", never "the printer lacks this capability". The pre-existing tri-state fields (`color`, `type`) keep their explicit `"unknown"` value because existing consumers branch on it; both conventions mean the same thing: absence of evidence, not evidence of absence. + +`duplex` is deliberately **not** part of the projection: upstream Foomatic data records duplex capability for zero printers at the current snapshot, so there is nothing to store or filter on. If upstream ever starts recording it, adding it here is a deliberate schema decision (see `lib/foomatic/__tests__/catalog.test.ts`). + +The generated artifact is validated record-by-record against `printers.json` by `lib/foomatic/__tests__/catalog-artifact.test.ts` (skipped automatically when the generated data is absent, e.g. in CI's pre-generate test run). + +## Tunables + +Values live in `lib/assistant/constants.ts`; `yarn assistant:eval` fails if this table drifts from the code. + +| Constant | Value | Meaning | +|---|---|---| +| `MAX_QUERY_LENGTH` | 300 | input truncation bound before parsing | +| `SCORE_EXACT` | 4 | entity score: exact id / exact "make model" match | +| `SCORE_MODEL_EXACT` | 3 | entity score: exact model match (resolves only when unique) | +| `SCORE_MODEL_PREFIX` | 1 | entity score: prefix match (always surfaces as ambiguity) | +| `RESOLVE_MIN_SCORE` | 3 | minimum score for silent resolution | +| `NEAR_MISS_MAX_LENGTH` | 20 | longest fused leftover treated as a near-miss model number | +| `MAX_CANDIDATES` | 5 | candidates carried by an ambiguity/clarification | +| `MAX_RESULT_CARDS` | 5 | result cards per response (totals are stated) | +| `HIGH_RES_MIN_DPI` | 1200 | disclosed interpretation of "high resolution" | +| `UNKNOWN_REPORT_RATIO` | 0.25 | unknown-excluded share that must be reported | +| `MIN_COMFORTABLE_RESULTS` | 3 | below this, one-filter relaxations are offered | + +## Testing + +- `lib/assistant/__tests__/` — always-run fixture suites: a 187-utterance natural-language corpus (`corpus.ts`) pinning intents and parse details, entity-resolution cases, execution-state semantics (unknown-never-false, duplex, relaxations, better-dimensions), and grounding property tests over rendered responses (every card id and similarity percentage must trace to input data; banned terminology can never appear). +- `lib/assistant/__tests__/assistant-artifact.test.ts` — the same engine against the real generated artifacts (skipped automatically when they are absent, e.g. CI's pre-generate test run). +- `tools/assistant-eval/run.ts` (`yarn assistant:eval`) — corpus accuracy, a full grounding sweep, the approved end-to-end flows, latency and artifact-size measurement, and the tunables/wording drift gates. Requires generated data, like `tools/eval/`. + +## Extending the assistant + +- **New capability filter:** add the field to the catalogue projection (`lib/foomatic/catalog.ts` + docs), a lexicon row (`lexicon.ts`), a filter spec (`execute.ts` `buildSpecs`), and corpus cases. Preserve known/pass separation so unknowns stay countable. +- **New intent:** add a cascade rule in `parse.ts` (mind the precedence list above), an executor arm, a response builder, and corpus cases. +- **New synonym:** one lexicon row plus a corpus case. Only add vocabulary grounded in the data's own terminology or clearly common phrasing. + +## Known limitations + +- Understanding is bounded by the lexicon and rule cascade; unanticipated phrasing falls back to a clarification with examples rather than a guess. +- Clarifications resolve through suggestion chips (each chip is a complete query); free-text follow-ups like "the second one" are not interpreted against prior candidates. +- Purely numeric model queries ("4019") need the manufacturer ("IBM 4019") — bare numbers are treated as capability values, not names. +- English-only vocabulary. +- Data limitations pass through honestly: duplex is unrecorded, `connectivity` is empty upstream, and 634 printers synthesized from driver back-references have thin records. From 1c7164c8f2ab894eccd3a696665f840e9a1b12bb Mon Sep 17 00:00:00 2001 From: Gati Varshney <171050892+gativarshney@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:32:10 +0530 Subject: [PATCH 2/2] docs(foomatic): document the catalogue capability fields in printersMap.json --- docs/foomatic-data-formats.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/foomatic-data-formats.md b/docs/foomatic-data-formats.md index 1ccb063c..991b6744 100644 --- a/docs/foomatic-data-formats.md +++ b/docs/foomatic-data-formats.md @@ -64,8 +64,8 @@ interface Driver { ## `public/foomatic-db/printersMap.json` -**Produced by:** `split-printers.ts` -**Consumed by:** the directory listing page (`app/foomatic/printers/page.tsx`) and `generateStaticParams()` in `app/foomatic/printer/[make]/[id]/page.tsx` +**Produced by:** `split-printers.ts`, one `toPrinterSummary()` call per printer (`lib/foomatic/catalog.ts` is the single source of truth for this shape); written minified +**Consumed by:** the directory listing page (`app/foomatic/printers/page.tsx`), `generateStaticParams()` in `app/foomatic/printer/[make]/[id]/page.tsx`, and the printer assistant catalogue (see [foomatic-assistant.md](./foomatic-assistant.md)) A lightweight projection of `printers.json`, one entry per printer, used so the directory page doesn't need to download every printer's full driver/PPD detail just to render a list: @@ -78,10 +78,19 @@ interface PrinterSummary { status?: string driverCount?: number functionality?: string + color?: boolean | "unknown" + // Capability fields, omitted when Foomatic does not record them + maxDpi?: number + psLevel?: number + pclLevel?: number + cs?: string[] // commandsetTokens from the full record + rd?: string // recommended driver family } ``` -`driverCount` is `drivers.length` from the full record; `type`/`status`/`functionality` default to `"unknown"`/`"Unknown"`/`"?"` respectively if absent. +`driverCount` is `drivers.length` from the full record; `type`/`status`/`functionality`/`color` default to `"unknown"`/`"Unknown"`/`"?"`/`"unknown"` respectively if absent. + +The five capability fields exist so the assistant can answer capability queries from this one file. They are **omitted** when the source data does not establish them, never emitted as `null`, `"unknown"` or `[]`: an absent key means "not recorded in Foomatic", not "the printer lacks this". `duplex` is deliberately not projected because upstream records it for no printer. ### Usable drivers and support status