@@ -838,32 +838,37 @@ def _write_entity(
838838 if end != - 1 :
839839 clean = clean [end + 3 :].lstrip ("\n " )
840840
841+ def _build_frontmatter (sources : list [str ]) -> str :
842+ fm_lines = [_yaml_list_line ("sources" , sources )]
843+ fm_lines .append (_yaml_kv_line ("type" , type_ or "other" ))
844+ if brief :
845+ fm_lines .append (_yaml_kv_line ("brief" , brief ))
846+ if aliases :
847+ fm_lines .append (_yaml_list_line ("aliases" , aliases ))
848+ return "---\n " + "\n " .join (fm_lines ) + "\n ---\n \n "
849+
841850 if is_update and path .exists ():
842851 existing = path .read_text (encoding = "utf-8" )
843852 if source_file not in existing :
844853 existing = _prepend_source_to_frontmatter (existing , source_file )
845- if existing .startswith ("---" ):
846- end = existing .find ("---" , 3 )
847- if end != - 1 :
848- fm = existing [:end + 3 ]
849- fm = _set_fm_line (fm , "brief" , brief ) if brief else fm
850- fm = _set_fm_line (fm , "type" , type_ ) if type_ else fm
851- existing = fm + "\n \n " + clean
852- else :
853- existing = clean
854+ end = existing .find ("---" , 3 ) if existing .startswith ("---" ) else - 1
855+ if end != - 1 :
856+ fm = existing [:end + 3 ]
857+ fm = _set_fm_line (fm , "brief" , brief ) if brief else fm
858+ fm = _set_fm_line (fm , "type" , type_ ) if type_ else fm
859+ existing = fm + "\n \n " + clean
854860 else :
855- existing = clean
861+ # Malformed/absent frontmatter (opening ``---`` with no closing
862+ # delimiter, or no frontmatter at all): rebuild valid frontmatter
863+ # rather than writing a body-only page and dropping sources/type/
864+ # brief. ``_prepend_source_to_frontmatter`` already ensured the
865+ # new source is present in the (still-malformed) block, so seed
866+ # with it here.
867+ existing = _build_frontmatter ([source_file ]) + clean
856868 path .write_text (existing , encoding = "utf-8" )
857869 return
858870
859- fm_lines = [_yaml_list_line ("sources" , [source_file ])]
860- fm_lines .append (_yaml_kv_line ("type" , type_ or "other" ))
861- if brief :
862- fm_lines .append (_yaml_kv_line ("brief" , brief ))
863- if aliases :
864- fm_lines .append (_yaml_list_line ("aliases" , aliases ))
865- frontmatter = "---\n " + "\n " .join (fm_lines ) + "\n ---\n \n "
866- path .write_text (frontmatter + clean , encoding = "utf-8" )
871+ path .write_text (_build_frontmatter ([source_file ]) + clean , encoding = "utf-8" )
867872
868873
869874def _set_fm_line (fm : str , key : str , value : str ) -> str :
@@ -1364,6 +1369,21 @@ def _write_v1_summary_stripped() -> None:
13641369 # The new plan contract nests concepts under a "concepts" key alongside
13651370 # an "entities" key; the legacy flat shape (create/update/related at top
13661371 # level) is still honored by falling back to ``parsed`` itself.
1372+ if not isinstance (parsed , (list , dict )):
1373+ # A JSON scalar (int/str/None/bool) is valid JSON but not a usable
1374+ # plan. ``_parse_json`` normally rejects scalars, but guard here too
1375+ # so ``parsed.get(...)`` can never raise AttributeError and abort the
1376+ # compile — treat it as an empty/unparseable plan.
1377+ logger .warning (
1378+ "Concepts plan parsed to a %s scalar, not an object/array — "
1379+ "treating as empty plan for %s." ,
1380+ type (parsed ).__name__ , doc_name ,
1381+ )
1382+ if rewrite_summary :
1383+ _write_v1_summary_stripped ()
1384+ _update_index (wiki_dir , doc_name , [], doc_brief = doc_brief , doc_type = doc_type )
1385+ return
1386+
13671387 if isinstance (parsed , list ):
13681388 plan = {"create" : _filter_concept_items (parsed , "list" ),
13691389 "update" : [], "related" : []}
0 commit comments