Fix CP14 heading detection via RoleMap + add manual pass support

- enterprise_pdf_checker.py: resolve custom tag names through PDF RoleMap
  in _check_headings so PDFs using /Heading1-style tags (mapped to /H1)
  are correctly detected; add depth guard to walk_tree
- js/results.js: add CP14 (Heading Structure) to CP_TO_CHECK; relax
  H-type restriction so M-type CPs with a linked check also get
  Mark as Passed / Undo buttons
- api.php: add 'Heading Structure' => ['14'] to $check_to_cp for
  server-side recalculate score with heading override

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-18 13:37:19 +00:00
parent 2a8db06f0d
commit dafef834d2
3 changed files with 30 additions and 10 deletions

View file

@ -1372,6 +1372,7 @@ function handleSaveAdjustedResult() {
$check_to_cp = [
'Color Contrast' => ['04'],
'Image Accessibility' => ['13'],
'Heading Structure' => ['14'],
];
$cp_to_check = [];
foreach ($check_to_cp as $checkName => $cpIds) {

View file

@ -1311,22 +1311,41 @@ Respond in JSON format:
return
struct_tree = catalog["/StructTreeRoot"]
headings = []
if hasattr(struct_tree, 'get_object'):
struct_tree = struct_tree.get_object()
def walk_tree(element):
# Load RoleMap so custom tag names (e.g. /Heading1) resolve to standard ones (/H1)
role_map = {}
if "/RoleMap" in struct_tree:
rm = struct_tree["/RoleMap"]
if hasattr(rm, 'get_object'):
rm = rm.get_object()
try:
for key, value in rm.items():
role_map[str(key)] = str(value)
except (AttributeError, TypeError):
pass
headings = []
HEADING_TAGS = {"/H1", "/H2", "/H3", "/H4", "/H5", "/H6"}
def walk_tree(element, depth=0):
if depth > 100:
return
try:
if hasattr(element, 'get_object'):
element = element.get_object()
if isinstance(element, dict):
tag = str(element.get("/S", ""))
if tag in ["/H1", "/H2", "/H3", "/H4", "/H5", "/H6"]:
headings.append(int(tag[2]))
mapped_tag = role_map.get(tag, tag)
if mapped_tag in HEADING_TAGS:
headings.append(int(mapped_tag[2]))
kids = element.get("/K", [])
if isinstance(kids, list):
for kid in kids:
walk_tree(kid)
walk_tree(kid, depth + 1)
elif kids:
walk_tree(kids)
walk_tree(kids, depth + 1)
except (AttributeError, TypeError, KeyError):
pass

View file

@ -446,7 +446,7 @@ function displayScoreBreakdown(breakdown) {
}
// Maps H-type Matterhorn checkpoint IDs to the Score Breakdown check names that drive them
const CP_TO_CHECK = { '04': 'Color Contrast', '13': 'Image Accessibility' };
const CP_TO_CHECK = { '04': 'Color Contrast', '13': 'Image Accessibility', '14': 'Heading Structure' };
function displayMatterhorn(summary) {
const card = document.getElementById('matterhornCard');
@ -459,9 +459,9 @@ function displayMatterhorn(summary) {
const cpMap = {};
summary.checkpoints.forEach(cp => { cpMap[cp.id] = cp; });
// Compute effective status: H-type FAIL → MANUAL_PASS if linked check is overridden
// Compute effective status: FAIL → MANUAL_PASS if linked check is overridden
function effectiveStatus(cp) {
if (cp.how === 'H' && cp.status === 'FAIL') {
if (cp.status === 'FAIL') {
const linked = CP_TO_CHECK[cp.id];
if (linked && overriddenChecks.has(linked)) return 'MANUAL_PASS';
}
@ -503,7 +503,7 @@ function displayMatterhorn(summary) {
<button class="btn-unoverride" onclick="unoverrideCheck('${escapeAttr(linked)}')">&#x21A9; Undo</button>`;
} else if (effStatus === 'PASS') {
statusHtml = `<span class="mh-pass">&#x2713; PASS</span>`;
} else if (effStatus === 'FAIL' && cp.how === 'H' && CP_TO_CHECK[cp.id]) {
} else if (effStatus === 'FAIL' && CP_TO_CHECK[cp.id]) {
const linked = CP_TO_CHECK[cp.id];
statusHtml = `<span class="mh-fail">&#x2717; FAIL</span>
<button class="btn-mark-passed" onclick="overrideCheck('${escapeAttr(linked)}')">&#x2713; Mark as Passed</button>`;