@@ -335,16 +335,33 @@ def _normalize_section_heading(name: str) -> str:
335335 return re .sub (r"\s+" , " " , name .strip ().casefold ()).rstrip (":" ).strip ()
336336
337337
338+ CANONICAL_HANDOFF_SECTION_HEADINGS : tuple [str , ...] = (
339+ "Type" ,
340+ "Title" ,
341+ "Summary" ,
342+ "Durable facts" ,
343+ "Evidence" ,
344+ "Recommended memory action" ,
345+ "Target card" ,
346+ "Suggested card content" ,
347+ "Target document" ,
348+ "Suggested document content" ,
349+ )
350+
351+
338352_SECTION_SYNONYMS : dict [str , str ] = {
339353 "type" : "Type" ,
340354 "handoff type" : "Type" ,
341355 "kind" : "Type" ,
356+ "category" : "Type" ,
342357 "title" : "Title" ,
343358 "name" : "Title" ,
359+ "subject" : "Title" ,
344360 "summary" : "Summary" ,
345361 "tldr" : "Summary" ,
346362 "tl;dr" : "Summary" ,
347363 "overview" : "Summary" ,
364+ "what changed" : "Summary" ,
348365 "durable facts" : "Durable facts" ,
349366 "facts" : "Durable facts" ,
350367 "evidence" : "Evidence" ,
@@ -364,15 +381,44 @@ def _normalize_section_heading(name: str) -> str:
364381}
365382
366383
367- def _canonicalize_sections (sections : dict [str , str ]) -> tuple [dict [str , str ], list [str ]]:
384+ def _exact_canonical_heading (raw_name : str ) -> str | None :
385+ normalized = _normalize_section_heading (raw_name )
386+ for canonical in CANONICAL_HANDOFF_SECTION_HEADINGS :
387+ if _normalize_section_heading (canonical ) == normalized :
388+ return canonical
389+ return None
390+
391+
392+ def _resolve_section_canonical (raw_name : str ) -> str | None :
393+ key = _normalize_section_heading (raw_name )
394+ canonical = _SECTION_SYNONYMS .get (key )
395+ if canonical is not None :
396+ return canonical
397+ return _exact_canonical_heading (raw_name )
398+
399+
400+ def _canonicalize_sections (sections : dict [str , str ]) -> tuple [dict [str , str ], list [str ], list [tuple [str , str ]]]:
368401 resolved : dict [str , str ] = {}
369402 owners : dict [str , str ] = {}
370403 errors : list [str ] = []
404+ noncanonical : list [tuple [str , str ]] = []
371405 for raw_name , body in sections .items ():
372406 key = _normalize_section_heading (raw_name )
373407 canonical = _SECTION_SYNONYMS .get (key )
374408 if canonical is None :
375- resolved [raw_name ] = body
409+ exact = _exact_canonical_heading (raw_name )
410+ if exact is not None :
411+ prior = owners .get (exact )
412+ if prior is not None and prior != raw_name :
413+ errors .append (f"ambiguous section headings for { exact } : { prior !r} , { raw_name !r} " )
414+ continue
415+ owners [exact ] = raw_name
416+ if exact not in resolved or (body and not resolved [exact ]):
417+ resolved [exact ] = body
418+ if raw_name != exact :
419+ noncanonical .append ((raw_name , exact ))
420+ else :
421+ resolved [raw_name ] = body
376422 continue
377423 prior = owners .get (canonical )
378424 if prior is not None and prior != raw_name :
@@ -381,7 +427,46 @@ def _canonicalize_sections(sections: dict[str, str]) -> tuple[dict[str, str], li
381427 owners [canonical ] = raw_name
382428 if canonical not in resolved or (body and not resolved [canonical ]):
383429 resolved [canonical ] = body
384- return resolved , errors
430+ if raw_name != canonical :
431+ noncanonical .append ((raw_name , canonical ))
432+ return resolved , errors , noncanonical
433+
434+
435+ def _canonical_sections_in_file_order (sections : dict [str , str ]) -> list [str ]:
436+ seen : set [str ] = set ()
437+ ordered : list [str ] = []
438+ for raw_name in sections :
439+ canonical = _resolve_section_canonical (raw_name )
440+ if canonical is None or canonical not in CANONICAL_HANDOFF_SECTION_HEADINGS :
441+ continue
442+ if canonical in seen :
443+ continue
444+ seen .add (canonical )
445+ ordered .append (canonical )
446+ return ordered
447+
448+
449+ def _lint_noncanonical_heading_messages (
450+ noncanonical : list [tuple [str , str ]],
451+ ) -> tuple [tuple [str , ...], tuple [str , ...]]:
452+ if not noncanonical :
453+ return (), ()
454+ warnings = tuple (f"noncanonical section heading { raw !r} ; use ## { canonical } " for raw , canonical in noncanonical )
455+ hints = tuple (f"use ## { canonical } instead of ## { raw } " for raw , canonical in noncanonical )
456+ return warnings , hints
457+
458+
459+ def _lint_section_order_messages (
460+ sections : dict [str , str ],
461+ ) -> tuple [tuple [str , ...], tuple [str , ...]]:
462+ ordered = _canonical_sections_in_file_order (sections )
463+ if len (ordered ) < 2 :
464+ return (), ()
465+ indices = [CANONICAL_HANDOFF_SECTION_HEADINGS .index (name ) for name in ordered ]
466+ if indices == sorted (indices ):
467+ return (), ()
468+ expected = ", " .join (f"## { name } " for name in CANONICAL_HANDOFF_SECTION_HEADINGS )
469+ return ("sections are out of canonical order" ,), (f"Expected section order: { expected } " ,)
385470
386471
387472def _section_value (sections : dict [str , str ], name : str ) -> str :
0 commit comments