@json-to-office/jto-ops 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FontRuntimeOpts, ServicesConfig, GenerationWarning, RendererStatus, PptxBatchRasterizer, PptxRasterizer, ResolvedFont } from '@json-to-office/shared';
2
- import { QualityProfile, QualityPolicy, PreparedDocument, QualityAnalysis, QualityDiagnostic } from '@json-to-office/quality';
2
+ import { QualityProfile, QualityPolicy, PreparedDocument, QualityAnalysis, QualityRulePack, QualityDiagnostic, QualityAnalyzeOptions } from '@json-to-office/quality';
3
3
 
4
4
  type FormatName = 'docx' | 'pptx';
5
5
  interface GeneratorOptions {
@@ -401,6 +401,20 @@ declare function extractPdfFonts(pdfPath: string, options?: {
401
401
  timeoutMs?: number;
402
402
  }): Promise<PdfFontInfo[]>;
403
403
 
404
+ /**
405
+ * The rendered pass as a quality rule pack (#344).
406
+ *
407
+ * `draftRenderedFindings` measures; these rules are how the measurement
408
+ * enters the quality contract. Each rule owns one code, one description the
409
+ * design guide prints, and the defaults a profile or policy can move. All of
410
+ * them read a single `rendered/geometry` fact — the PDF's word boxes, its
411
+ * embedded fonts and the document's text inventory — and share one matching
412
+ * pass over it, memoised per fact, so eight rules cost one search.
413
+ */
414
+
415
+ type RenderedRuleId = 'rendered/clip' | 'rendered/spill' | 'rendered/overlap' | 'rendered/text-missing' | 'rendered/font-substituted' | 'rendered/empty-page' | 'rendered/heading-stranded' | 'rendered/paragraph-split';
416
+ declare const RENDERED_QUALITY_RULES: QualityRulePack;
417
+
404
418
  /**
405
419
  * Locate authored text in rendered PDF geometry.
406
420
  *
@@ -462,6 +476,12 @@ interface InventoryEntry {
462
476
  text: string;
463
477
  /** Header/footer text repeats per page: every occurrence is legitimate. */
464
478
  repeats?: boolean;
479
+ /**
480
+ * Painted by the renderer from other authored text — a contents entry —
481
+ * so it claims its occurrence when there is one and is `skipped`, never
482
+ * `missing`, when there is not.
483
+ */
484
+ optional?: boolean;
465
485
  }
466
486
  type MappingStatus = 'mapped' | 'ambiguous' | 'missing' | 'skipped';
467
487
  interface InventoryMatch<T extends InventoryEntry = InventoryEntry> {
@@ -493,8 +513,8 @@ interface InventoryAssignment<T extends InventoryEntry> {
493
513
  * stream without those words; each claims the first unclaimed occurrence at
494
514
  * or after the previous claim, which is how "Total" in the second table
495
515
  * finds the second "Total". An entry with no occurrence is `missing` —
496
- * fully clipped, or never set — and one whose every occurrence was already
497
- * claimed is `ambiguous`.
516
+ * fully clipped, or never set — unless it is optional, and one whose every
517
+ * occurrence was already claimed is `ambiguous`.
498
518
  */
499
519
  declare function assignInventory<T extends InventoryEntry>(pages: readonly PdfTextPage[], inventory: readonly T[]): InventoryAssignment<T>;
500
520
 
@@ -518,11 +538,17 @@ declare function assignInventory<T extends InventoryEntry>(pages: readonly PdfTe
518
538
  * Pure: geometry and fonts are extracted by the caller (see
519
539
  * `extractPdfTextGeometry`, `extractPdfFonts`), so every rule here is
520
540
  * testable from captured fixtures with no converter on the host.
541
+ *
542
+ * The checks draft findings; the quality engine turns them into
543
+ * diagnostics. Each check is one rule of `RENDERED_QUALITY_RULES`, so a
544
+ * profile can switch one off or move its severity, a policy can suppress
545
+ * one at a pointer and a gate can make one blocking — the same levers every
546
+ * static rule answers to, applied to the pass that measures.
521
547
  */
522
548
 
523
549
  /** One authored string the pass can match rendered words back to. */
524
550
  interface RenderedTextEntry extends InventoryEntry {
525
- role: 'heading' | 'body' | 'list-item' | 'table-header' | 'table-cell' | 'statistic' | 'caption' | 'chrome' | 'slide-text';
551
+ role: 'heading' | 'body' | 'list-item' | 'table-header' | 'table-cell' | 'statistic' | 'caption' | 'toc-entry' | 'chrome' | 'slide-text';
526
552
  level?: number;
527
553
  /** A declared box the text must fit: a docx frame, a pptx text box. */
528
554
  box?: {
@@ -544,6 +570,8 @@ interface RequestedFont {
544
570
  }
545
571
  interface RenderedAnalysisInput {
546
572
  format: 'docx' | 'pptx';
573
+ /** Renderer identity, for a profile that declares renderer targets. */
574
+ renderer?: string;
547
575
  pages: readonly PdfTextPage[];
548
576
  inventory: readonly RenderedTextEntry[];
549
577
  /** Fonts the PDF carries; `undefined` when the host could not inspect them. */
@@ -557,21 +585,37 @@ interface RenderedAnalysisSummary {
557
585
  words: number;
558
586
  /** Inventory entries by mapping outcome. */
559
587
  inventory: Record<MappingStatus, number>;
560
- /** Findings by mapping outcome. */
588
+ /** Findings by mapping outcome, after suppressions. */
561
589
  findings: Record<RenderedMapping, number>;
562
590
  fonts?: {
563
591
  requested: number;
564
592
  substituted: number;
565
593
  };
594
+ /** Findings a policy suppression removed. */
595
+ suppressed: number;
596
+ /** Whether a policy gate made any finding blocking. */
597
+ blocked: boolean;
598
+ /** Whether a policy `maxDiagnostics` budget cut the findings returned. */
599
+ truncated: boolean;
600
+ /** The quality profile the pass ran under, when one applied. */
601
+ profileId?: string;
566
602
  }
567
603
  interface RenderedAnalysis {
568
- findings: QualityDiagnostic[];
604
+ /** The pass's diagnostics, as the quality engine finalised them. */
605
+ findings: readonly QualityDiagnostic[];
569
606
  summary: RenderedAnalysisSummary;
607
+ /** The engine's own account: evaluated rules, rule errors, truncation. */
608
+ analysis: QualityAnalysis;
570
609
  }
571
610
  /** Spill below this is sub-visual: renderer rounding, descender fuzz. */
572
611
  declare const VISIBLE_SPILL_PT = 2;
573
- /** Run the pass. */
574
- declare function analyzeRenderedDocument(input: RenderedAnalysisInput): RenderedAnalysis;
612
+ /**
613
+ * Run the pass. Geometry, fonts and inventory become one `rendered/geometry`
614
+ * fact; the rendered rule pack reads it under the caller's profile and
615
+ * policy, so the findings come back suppressed, re-severed and gated the
616
+ * way `jto_validate`'s would.
617
+ */
618
+ declare function analyzeRenderedDocument(input: RenderedAnalysisInput, options?: QualityAnalyzeOptions): RenderedAnalysis;
575
619
 
576
620
  /**
577
621
  * Make resolved fonts visible to the LibreOffice child process for the
@@ -764,4 +808,4 @@ declare function emitDiagnostic(text: string, tone?: DiagnosticTone): void;
764
808
  */
765
809
  declare const stderrDiagnosticSink: DiagnosticSink;
766
810
 
767
- export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, type InventoryEntry, type InventoryMatch, MacOSCoreTextStager, type MappingStatus, NoopFontStager, type PdfFontInfo, type PdfTextLine, type PdfTextPage, type PdfTextWord, PptxFormatAdapter, type RasterizerCacheStats, type RenderedAnalysis, type RenderedAnalysisInput, type RenderedAnalysisSummary, type RenderedMapping, type RenderedTextEntry, type RequestedFont, type TextOccurrence, VISIBLE_SPILL_PT, WindowsFontStager, analyzeRenderedDocument, assignInventory, authoredTextForMatch, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, extractPdfFonts, extractPdfTextGeometry, familyRendered, getFontStager, getRasterizerCacheStats, normalizeForMatch, parsePdfFonts, parsePdfTextBbox, pdffontsAvailable, pdftotextAvailable, runWithDiagnosticSink, stderrDiagnosticSink };
811
+ export { type DiagnosticSink, type DiagnosticTone, DocxFormatAdapter, type FontStageHandle, type FontStageOptions, type FontStager, FontconfigStager, type FormatAdapter, type FormatName, type GeneratorOptions, type GeneratorResult, type InventoryEntry, type InventoryMatch, MacOSCoreTextStager, type MappingStatus, NoopFontStager, type PdfFontInfo, type PdfTextLine, type PdfTextPage, type PdfTextWord, PptxFormatAdapter, RENDERED_QUALITY_RULES, type RasterizerCacheStats, type RenderedAnalysis, type RenderedAnalysisInput, type RenderedAnalysisSummary, type RenderedMapping, type RenderedRuleId, type RenderedTextEntry, type RequestedFont, type TextOccurrence, VISIBLE_SPILL_PT, WindowsFontStager, analyzeRenderedDocument, assignInventory, authoredTextForMatch, clearRasterizerCache, createAdapter, createLibreOfficePptxBatchRasterizer, createLibreOfficePptxRasterizer, emitDiagnostic, extractPdfFonts, extractPdfTextGeometry, familyRendered, getFontStager, getRasterizerCacheStats, normalizeForMatch, parsePdfFonts, parsePdfTextBbox, pdffontsAvailable, pdftotextAvailable, runWithDiagnosticSink, stderrDiagnosticSink };
package/dist/index.js CHANGED
@@ -1947,9 +1947,93 @@ async function extractPdfFonts(pdfPath, options = {}) {
1947
1947
  }
1948
1948
 
1949
1949
  // src/rendered-analysis.ts
1950
+ import {
1951
+ QualityEngine
1952
+ } from "@json-to-office/quality";
1953
+
1954
+ // src/rendered-rules.ts
1950
1955
  import {
1951
1956
  QUALITY_CODES
1952
1957
  } from "@json-to-office/quality";
1958
+ function renderedRule(spec) {
1959
+ return {
1960
+ id: spec.id,
1961
+ code: spec.code,
1962
+ description: spec.description,
1963
+ category: spec.category,
1964
+ defaultSeverity: spec.defaultSeverity,
1965
+ defaultCertainty: "rendered",
1966
+ ...spec.formats && { formats: spec.formats },
1967
+ evaluate({ facts }) {
1968
+ const fact = facts.find(isRenderedGeometryFact);
1969
+ if (!fact) return [];
1970
+ return renderedDraftFor(fact).byRule.get(spec.id) ?? [];
1971
+ }
1972
+ };
1973
+ }
1974
+ var RENDERED_QUALITY_RULES = {
1975
+ id: "rendered",
1976
+ rules: [
1977
+ renderedRule({
1978
+ id: "rendered/clip",
1979
+ code: QUALITY_CODES.RENDERED_CLIP,
1980
+ description: "Words rendered past the page edge by more than 2 pt, or an authored string of which only a leading part rendered (`kind: truncated`, with the share found).",
1981
+ category: "integrity",
1982
+ defaultSeverity: "warning"
1983
+ }),
1984
+ renderedRule({
1985
+ id: "rendered/spill",
1986
+ code: QUALITY_CODES.RENDERED_SPILL,
1987
+ description: "A framed paragraph or slide text box drawn wider (DOCX) or larger (PPTX) than the box it declared.",
1988
+ category: "integrity",
1989
+ defaultSeverity: "warning"
1990
+ }),
1991
+ renderedRule({
1992
+ id: "rendered/overlap",
1993
+ code: QUALITY_CODES.RENDERED_OVERLAP,
1994
+ description: "Two words from different lines whose boxes intersect by more than 30% of the smaller.",
1995
+ category: "integrity",
1996
+ defaultSeverity: "warning"
1997
+ }),
1998
+ renderedRule({
1999
+ id: "rendered/text-missing",
2000
+ code: QUALITY_CODES.RENDERED_TEXT_MISSING,
2001
+ description: "An authored string that appears nowhere in the PDF: fully clipped, hidden, or dropped by the renderer.",
2002
+ category: "integrity",
2003
+ defaultSeverity: "warning"
2004
+ }),
2005
+ renderedRule({
2006
+ id: "rendered/font-substituted",
2007
+ code: QUALITY_CODES.RENDERED_FONT_SUBSTITUTED,
2008
+ description: "A requested family the PDF embeds no face of; a warning when the document declared a source for it, information when the host simply lacks it.",
2009
+ category: "brand",
2010
+ defaultSeverity: "info"
2011
+ }),
2012
+ renderedRule({
2013
+ id: "rendered/empty-page",
2014
+ code: QUALITY_CODES.RENDERED_EMPTY_PAGE,
2015
+ description: "A page with no text at all: a full-page figure is fine, a blank page from a stray break is not.",
2016
+ category: "integrity",
2017
+ defaultSeverity: "info"
2018
+ }),
2019
+ renderedRule({
2020
+ id: "rendered/heading-stranded",
2021
+ code: QUALITY_CODES.RENDERED_HEADING_STRANDED,
2022
+ description: "A heading that is the last body line on its page.",
2023
+ category: "composition",
2024
+ defaultSeverity: "warning",
2025
+ formats: ["docx"]
2026
+ }),
2027
+ renderedRule({
2028
+ id: "rendered/paragraph-split",
2029
+ code: QUALITY_CODES.RENDERED_PARAGRAPH_SPLIT,
2030
+ description: "A paragraph leaving one line alone on either side of a page break (`kind: orphan` or `widow`).",
2031
+ category: "composition",
2032
+ defaultSeverity: "info",
2033
+ formats: ["docx"]
2034
+ })
2035
+ ]
2036
+ };
1953
2037
 
1954
2038
  // src/rendered-text-match.ts
1955
2039
  function normalizeForMatch(value) {
@@ -2142,7 +2226,7 @@ function assignInventory(pages, inventory) {
2142
2226
  });
2143
2227
  });
2144
2228
  const body = indexDocument(pages, chromeWords);
2145
- const claimed = /* @__PURE__ */ new Set();
2229
+ const claimed = /* @__PURE__ */ new Map();
2146
2230
  let cursor = 0;
2147
2231
  for (const entry of inventory) {
2148
2232
  if (entry.repeats) continue;
@@ -2154,7 +2238,7 @@ function assignInventory(pages, inventory) {
2154
2238
  }
2155
2239
  let all = findOccurrences(body, segments);
2156
2240
  let partial;
2157
- if (all.length === 0 && segments.length === 1) {
2241
+ if (all.length === 0 && segments.length === 1 && !entry.optional) {
2158
2242
  const prefix = longestRenderedPrefix(body, needle);
2159
2243
  if (prefix) {
2160
2244
  all = [occurrence(body, prefix.at, prefix.at + prefix.length)];
@@ -2162,10 +2246,29 @@ function assignInventory(pages, inventory) {
2162
2246
  }
2163
2247
  }
2164
2248
  if (all.length === 0) {
2165
- results.set(entry, { entry, needle, status: "missing", occurrences: [] });
2249
+ results.set(entry, {
2250
+ entry,
2251
+ needle,
2252
+ status: entry.optional ? "skipped" : "missing",
2253
+ occurrences: []
2254
+ });
2166
2255
  continue;
2167
2256
  }
2168
- const free = all.filter((o) => !claimed.has(o.at));
2257
+ let free = all.filter((o) => !claimed.has(o.at));
2258
+ if (free.length === 0 && !entry.optional) {
2259
+ const held = all.find((o) => claimed.get(o.at)?.optional);
2260
+ if (held) {
2261
+ const holder = claimed.get(held.at);
2262
+ claimed.delete(held.at);
2263
+ results.set(holder, {
2264
+ entry: holder,
2265
+ needle: results.get(holder)?.needle ?? "",
2266
+ status: "skipped",
2267
+ occurrences: []
2268
+ });
2269
+ free = [held];
2270
+ }
2271
+ }
2169
2272
  if (free.length === 0) {
2170
2273
  results.set(entry, {
2171
2274
  entry,
@@ -2176,7 +2279,7 @@ function assignInventory(pages, inventory) {
2176
2279
  continue;
2177
2280
  }
2178
2281
  const chosen = free.find((o) => o.at >= cursor) ?? free[0];
2179
- claimed.add(chosen.at);
2282
+ claimed.set(chosen.at, entry);
2180
2283
  cursor = chosen.at;
2181
2284
  results.set(entry, {
2182
2285
  entry,
@@ -2193,15 +2296,23 @@ function assignInventory(pages, inventory) {
2193
2296
  }
2194
2297
 
2195
2298
  // src/rendered-analysis.ts
2299
+ function isRenderedGeometryFact(fact) {
2300
+ return fact.kind === "rendered/geometry";
2301
+ }
2302
+ var drafts = /* @__PURE__ */ new WeakMap();
2303
+ function renderedDraftFor(fact) {
2304
+ let draft = drafts.get(fact);
2305
+ if (!draft) {
2306
+ draft = draftRenderedFindings(fact.input);
2307
+ drafts.set(fact, draft);
2308
+ }
2309
+ return draft;
2310
+ }
2196
2311
  var VISIBLE_SPILL_PT = 2;
2197
2312
  var OVERLAP_FRACTION = 0.3;
2198
2313
  function finding(draft) {
2199
- const { ruleId, mapping, page, context, ...rest } = draft;
2314
+ const { mapping, page, context, ...rest } = draft;
2200
2315
  return {
2201
- source: "quality",
2202
- ruleId,
2203
- certainty: "rendered",
2204
- blocking: false,
2205
2316
  ...rest,
2206
2317
  context: {
2207
2318
  ...context,
@@ -2259,7 +2370,7 @@ function unionBox(parts) {
2259
2370
  { xMin: Infinity, yMin: Infinity, xMax: -Infinity, yMax: -Infinity }
2260
2371
  );
2261
2372
  }
2262
- function analyzeRenderedDocument(input) {
2373
+ function draftRenderedFindings(input) {
2263
2374
  const { pages, format } = input;
2264
2375
  const { matches, chromeWords } = assignInventory(pages, input.inventory);
2265
2376
  const owners = wordOwners(matches);
@@ -2293,9 +2404,6 @@ function analyzeRenderedDocument(input) {
2293
2404
  findings.push(
2294
2405
  finding({
2295
2406
  ruleId: "rendered/clip",
2296
- code: QUALITY_CODES.RENDERED_CLIP,
2297
- category: "integrity",
2298
- severity: "warning",
2299
2407
  mapping: bucket.match ? "mapped" : "unmapped",
2300
2408
  page: bucket.page,
2301
2409
  path: bucket.match?.entry.path ?? "",
@@ -2320,9 +2428,6 @@ function analyzeRenderedDocument(input) {
2320
2428
  findings.push(
2321
2429
  finding({
2322
2430
  ruleId: "rendered/clip",
2323
- code: QUALITY_CODES.RENDERED_CLIP,
2324
- category: "integrity",
2325
- severity: "warning",
2326
2431
  mapping: "mapped",
2327
2432
  page: o.endPageIndex + 1,
2328
2433
  path: match.entry.path,
@@ -2359,9 +2464,6 @@ function analyzeRenderedDocument(input) {
2359
2464
  findings.push(
2360
2465
  finding({
2361
2466
  ruleId: "rendered/spill",
2362
- code: QUALITY_CODES.RENDERED_SPILL,
2363
- category: "integrity",
2364
- severity: "warning",
2365
2467
  mapping: "mapped",
2366
2468
  page: o.pageIndex + 1,
2367
2469
  path: match.entry.path,
@@ -2407,9 +2509,6 @@ function analyzeRenderedDocument(input) {
2407
2509
  findings.push(
2408
2510
  finding({
2409
2511
  ruleId: "rendered/overlap",
2410
- code: QUALITY_CODES.RENDERED_OVERLAP,
2411
- category: "integrity",
2412
- severity: "warning",
2413
2512
  mapping,
2414
2513
  page: pageIndex + 1,
2415
2514
  path: pathA || pathB,
@@ -2432,9 +2531,6 @@ function analyzeRenderedDocument(input) {
2432
2531
  findings.push(
2433
2532
  finding({
2434
2533
  ruleId: "rendered/text-missing",
2435
- code: QUALITY_CODES.RENDERED_TEXT_MISSING,
2436
- category: "integrity",
2437
- severity: "warning",
2438
2534
  mapping: "mapped",
2439
2535
  path: match.entry.path,
2440
2536
  message: `"${excerpt(match.entry.text)}" does not appear anywhere in the rendered pages.`,
@@ -2456,8 +2552,6 @@ function analyzeRenderedDocument(input) {
2456
2552
  findings.push(
2457
2553
  finding({
2458
2554
  ruleId: "rendered/font-substituted",
2459
- code: QUALITY_CODES.RENDERED_FONT_SUBSTITUTED,
2460
- category: "brand",
2461
2555
  severity: requested.declared ? "warning" : "info",
2462
2556
  mapping: requested.path ? "mapped" : "unmapped",
2463
2557
  path: requested.path ?? "",
@@ -2478,9 +2572,6 @@ function analyzeRenderedDocument(input) {
2478
2572
  findings.push(
2479
2573
  finding({
2480
2574
  ruleId: "rendered/empty-page",
2481
- code: QUALITY_CODES.RENDERED_EMPTY_PAGE,
2482
- category: "integrity",
2483
- severity: "info",
2484
2575
  mapping: "unmapped",
2485
2576
  page: pageIndex + 1,
2486
2577
  path: "",
@@ -2503,9 +2594,6 @@ function analyzeRenderedDocument(input) {
2503
2594
  findings.push(
2504
2595
  finding({
2505
2596
  ruleId: "rendered/heading-stranded",
2506
- code: QUALITY_CODES.RENDERED_HEADING_STRANDED,
2507
- category: "composition",
2508
- severity: "warning",
2509
2597
  mapping: "mapped",
2510
2598
  page: part.pageIndex + 1,
2511
2599
  path: match.entry.path,
@@ -2539,9 +2627,6 @@ function analyzeRenderedDocument(input) {
2539
2627
  findings.push(
2540
2628
  finding({
2541
2629
  ruleId: "rendered/paragraph-split",
2542
- code: QUALITY_CODES.RENDERED_PARAGRAPH_SPLIT,
2543
- category: "composition",
2544
- severity: "info",
2545
2630
  mapping: "mapped",
2546
2631
  page: (which === "orphan" ? first.pageIndex : last.pageIndex) + 1,
2547
2632
  path: match.entry.path,
@@ -2563,27 +2648,80 @@ function analyzeRenderedDocument(input) {
2563
2648
  skipped: 0
2564
2649
  };
2565
2650
  for (const match of matches) inventory[match.status] += 1;
2651
+ const byRule = /* @__PURE__ */ new Map();
2652
+ for (const draft of findings) {
2653
+ const bucket = byRule.get(draft.ruleId);
2654
+ if (bucket) bucket.push(draft);
2655
+ else byRule.set(draft.ruleId, [draft]);
2656
+ }
2657
+ return {
2658
+ drafts: findings,
2659
+ byRule,
2660
+ inventory,
2661
+ // The same gate the check itself runs under: an empty font list
2662
+ // verified nothing, and must not read as "nothing was substituted".
2663
+ ...input.fonts && input.fonts.length > 0 && input.requestedFonts && {
2664
+ fonts: { requested: input.requestedFonts.length, substituted }
2665
+ }
2666
+ };
2667
+ }
2668
+ var engine;
2669
+ function renderedEngine() {
2670
+ engine ??= new QualityEngine(RENDERED_QUALITY_RULES.rules);
2671
+ return engine;
2672
+ }
2673
+ function analyzeRenderedDocument(input, options = {}) {
2674
+ const fact = {
2675
+ id: "rendered:geometry",
2676
+ kind: "rendered/geometry",
2677
+ path: "",
2678
+ input
2679
+ };
2680
+ const analysis = renderedEngine().analyzeSync(
2681
+ {
2682
+ format: input.format,
2683
+ model: void 0,
2684
+ facts: [fact],
2685
+ provenance: {},
2686
+ ...input.renderer !== void 0 && { renderer: input.renderer }
2687
+ },
2688
+ options
2689
+ );
2690
+ let draft;
2691
+ try {
2692
+ draft = renderedDraftFor(fact);
2693
+ } catch {
2694
+ draft = void 0;
2695
+ }
2566
2696
  const byMapping = {
2567
2697
  mapped: 0,
2568
2698
  ambiguous: 0,
2569
2699
  unmapped: 0
2570
2700
  };
2571
- for (const f of findings) {
2701
+ for (const f of analysis.diagnostics) {
2572
2702
  byMapping[f.context?.mapping ?? "unmapped"] += 1;
2573
2703
  }
2574
2704
  return {
2575
- findings,
2705
+ findings: analysis.diagnostics,
2576
2706
  summary: {
2577
- pages: pages.length,
2578
- words: pages.reduce((n, p) => n + p.words.length, 0),
2579
- inventory,
2707
+ pages: input.pages.length,
2708
+ words: input.pages.reduce((n, p) => n + p.words.length, 0),
2709
+ inventory: draft?.inventory ?? {
2710
+ mapped: 0,
2711
+ ambiguous: 0,
2712
+ missing: 0,
2713
+ skipped: 0
2714
+ },
2580
2715
  findings: byMapping,
2581
- // The same gate the check itself runs under: an empty font list
2582
- // verified nothing, and must not read as "nothing was substituted".
2583
- ...input.fonts && input.fonts.length > 0 && input.requestedFonts && {
2584
- fonts: { requested: input.requestedFonts.length, substituted }
2716
+ ...draft?.fonts && { fonts: draft.fonts },
2717
+ suppressed: analysis.suppressedCount,
2718
+ blocked: analysis.blocked,
2719
+ truncated: analysis.truncated,
2720
+ ...analysis.profileId !== void 0 && {
2721
+ profileId: analysis.profileId
2585
2722
  }
2586
- }
2723
+ },
2724
+ analysis
2587
2725
  };
2588
2726
  }
2589
2727
  export {
@@ -2592,6 +2730,7 @@ export {
2592
2730
  MacOSCoreTextStager,
2593
2731
  NoopFontStager,
2594
2732
  PptxFormatAdapter,
2733
+ RENDERED_QUALITY_RULES,
2595
2734
  VISIBLE_SPILL_PT,
2596
2735
  WindowsFontStager,
2597
2736
  analyzeRenderedDocument,