Source code for archledger.config.render

from __future__ import annotations

import json
from pathlib import Path
from uuid import uuid4

from archledger.config.model import (
    DEFAULT_ID_SEGMENT,
    DEFAULT_ID_SEGMENT_MAP,
    DEFAULT_TRACKING_EXCLUDE,
    DEFAULT_TRACKING_INCLUDE,
    VALID_BUILD_CONVERTERS,
    VALID_DIAGRAM_IMAGE_FORMATS,
    VALID_DIAGRAM_RENDERERS,
    VALID_DIAGRAM_TYPES,
    VALID_TRACKING_SCANNERS,
    ProjectConfig,
    normalize_project_name,
    validate_uuid,
)
from archledger.config.schema import TableSpec
from archledger.errors import ConfigError
from archledger.ids import (
    DEFAULT_ID_PREFIX,
    DEFAULT_ID_SEGMENT_MODE,
    DEFAULT_ID_WIDTH,
    validate_id_prefix,
    validate_id_segment,
    validate_id_segment_mode,
    validate_id_width,
)
from archledger.model import (
    VALID_SOURCE_FORMATS,
    default_document_filename_for_output_format,
    default_extension_for_source_format,
    native_output_format_for_source_format,
)


[docs] def build_default_project_config( workspace_root: Path, *, archledger_dir: str, source_format: str = "markdown", id_prefix: str = DEFAULT_ID_PREFIX, id_width: int = DEFAULT_ID_WIDTH, id_segment_mode: str = DEFAULT_ID_SEGMENT_MODE, id_default_segment: str = DEFAULT_ID_SEGMENT, id_segment_map: dict[str, str] | None = None, project_name: str | None = None, project_uuid: str | None = None, # Build options build_default_format: str | None = None, build_default_output: str | None = None, build_default_output_dir: str | None = None, build_include_draft: bool = False, build_include_superseded: bool = False, build_strict: bool = False, build_keep_intermediate: bool = False, build_converter: str = "auto", build_pdf_engine: str = "", build_reference_docx: str = "", # Diagram options diagram_enabled: bool = False, diagram_renderer: str = "pass-through", diagram_default_type: str = "text", diagram_output_dir: str = "diagrams", diagram_image_format: str = "svg", diagram_kroki_url: str = "", # arc42 options arc42_template_version: str = "9.0-EN", arc42_language: str = "en", arc42_title: str = "Architecture Documentation", arc42_include_help: bool = False, # Tracking options tracking_enabled: bool = True, tracking_scanner: str = "auto", tracking_state_file: str = "source-state.json", tracking_max_file_bytes: int = 1_000_000, tracking_include: tuple[str, ...] | None = None, tracking_exclude: tuple[str, ...] | None = None, ) -> ProjectConfig: normalized_source_format = source_format.strip().lower() if normalized_source_format not in VALID_SOURCE_FORMATS: raise ConfigError( "source_format must be one of: " + ", ".join(sorted(VALID_SOURCE_FORMATS)) + "." ) # Validate enum-like values before writing config _validate_enum(diagram_renderer, VALID_DIAGRAM_RENDERERS, "diagrams.renderer") _validate_enum(diagram_default_type, VALID_DIAGRAM_TYPES, "diagrams.default_type") _validate_enum( diagram_image_format, VALID_DIAGRAM_IMAGE_FORMATS, "diagrams.image_format" ) _validate_enum(build_converter, VALID_BUILD_CONVERTERS, "build.converter") _validate_enum(tracking_scanner, VALID_TRACKING_SCANNERS, "tracking.scanner") validated_id_prefix = validate_id_prefix(id_prefix) validated_id_width = validate_id_width(id_width) validated_id_segment_mode = validate_id_segment_mode(id_segment_mode) validated_id_default_segment = validate_id_segment(id_default_segment) resolved_segment_map = dict(DEFAULT_ID_SEGMENT_MAP) if id_segment_map is not None: for key, value in id_segment_map.items(): resolved_segment_map[key] = validate_id_segment(value) default_extension = default_extension_for_source_format(normalized_source_format) native_format = native_output_format_for_source_format(normalized_source_format) resolved_default_format = build_default_format or native_format resolved_default_output = ( build_default_output or default_document_filename_for_output_format(resolved_default_format) ) normalized_project_name = normalize_project_name( workspace_root.name if project_name is None else project_name ) normalized_uuid = ( str(uuid4()) if project_uuid is None else validate_uuid(project_uuid) ) return ProjectConfig( config_version=7, archledger_dir=archledger_dir, project_uuid=normalized_uuid, project_name=normalized_project_name, id_prefix=validated_id_prefix, id_width=validated_id_width, id_segment_mode=validated_id_segment_mode, id_default_segment=validated_id_default_segment, id_segment_map=resolved_segment_map, source_format=normalized_source_format, front_matter="yaml", section_extension=default_extension, record_extension=default_extension, build_default_output=resolved_default_output, build_default_format=resolved_default_format, build_output_dir=build_default_output_dir or "build", build_include_draft=build_include_draft, build_include_superseded=build_include_superseded, build_strict=build_strict, build_keep_intermediate=build_keep_intermediate, build_converter=build_converter, build_pdf_engine=build_pdf_engine, build_reference_docx=build_reference_docx, arc42_template_version=arc42_template_version, arc42_language=arc42_language, arc42_title=arc42_title, arc42_include_help=arc42_include_help, skill_installed=False, skill_path="skills/archledger/SKILL.md", tracking_enabled=tracking_enabled, tracking_state_file=tracking_state_file, tracking_scanner=tracking_scanner, tracking_include=tracking_include or DEFAULT_TRACKING_INCLUDE, tracking_exclude=tracking_exclude or DEFAULT_TRACKING_EXCLUDE, tracking_max_file_bytes=tracking_max_file_bytes, diagram_enabled=diagram_enabled, diagram_renderer=diagram_renderer, diagram_default_type=diagram_default_type, diagram_output_dir=diagram_output_dir, diagram_image_format=diagram_image_format, diagram_kroki_url=diagram_kroki_url, )
[docs] def render_default_config( workspace_root: Path, *, archledger_dir: str, source_format: str = "markdown", project_name: str | None = None, project_uuid: str | None = None, ) -> str: config = build_default_project_config( workspace_root, archledger_dir=archledger_dir, source_format=source_format, project_name=project_name, project_uuid=project_uuid, ) return render_project_config(config)
def _render_table_from_spec( spec: TableSpec, values: dict[str, object], *, comments: dict[str, str] | None = None, ) -> list[str]: """Render a TOML table section from a schema spec and field values. Only handles simple scalar types (bool, str, int). Complex fields like arrays and sub-tables must be rendered separately. """ lines: list[str] = [] lines.append(f"[{spec.name}]") for field in spec.fields: value = values.get(field.name, field.default) if comments and field.name in comments: lines.append(comments[field.name]) if isinstance(value, bool): lines.append(f"{field.name} = {_toml_bool(value)}") elif isinstance(value, str): lines.append(f"{field.name} = {_toml_string(value)}") elif isinstance(value, int): lines.append(f"{field.name} = {value}") # Skip complex types (tuple, dict) - handled separately return lines
[docs] def render_project_config(config: ProjectConfig) -> str: lines = [ "# Project-local archledger configuration.", "# This file lives in the source project root.", f"config_version = {config.config_version}", f"archledger_dir = {_toml_string(config.archledger_dir)}", "", "# Stable project identity. Commit this with your source tree.", f"project_uuid = {_toml_string(config.project_uuid)}", f"project_name = {_toml_string(config.project_name)}", "", "[ids]", f"prefix = {_toml_string(config.id_prefix)}", f"width = {config.id_width}", f"segment_mode = {_toml_string(config.id_segment_mode)}", f"default_segment = {_toml_string(config.id_default_segment)}", "", "[ids.segment_map]", ] lines.extend( f"{segment_key} = {_toml_string(config.id_segment_map[segment_key])}" for segment_key in sorted(config.id_segment_map) ) lines.extend( [ "", "[source]", f"format = {_toml_string(config.source_format)}", f"front_matter = {_toml_string(config.front_matter)}", f"section_extension = {_toml_string(config.section_extension)}", f"record_extension = {_toml_string(config.record_extension)}", f"schema_version = {config.source_schema_version}", "", "[build]", f"default_output = {_toml_string(config.build_default_output)}", f"default_format = {_toml_string(config.build_default_format)}", "# [build].default_output_dir is relative to the directory containing", "# archledger.toml or .archledger.toml.", f"default_output_dir = {_toml_string(config.build_output_dir)}", f"include_draft = {_toml_bool(config.build_include_draft)}", f"include_superseded = {_toml_bool(config.build_include_superseded)}", f"strict = {_toml_bool(config.build_strict)}", f"keep_intermediate = {_toml_bool(config.build_keep_intermediate)}", f"converter = {_toml_string(config.build_converter)}", f"pdf_engine = {_toml_string(config.build_pdf_engine)}", f"reference_docx = {_toml_string(config.build_reference_docx)}", "", ] ) lines.extend(_render_build_output_tables(config.build_outputs)) lines.extend( [ "[diagrams]", f"enabled = {_toml_bool(config.diagram_enabled)}", f"renderer = {_toml_string(config.diagram_renderer)}", f"default_type = {_toml_string(config.diagram_default_type)}", f"output_dir = {_toml_string(config.diagram_output_dir)}", f"image_format = {_toml_string(config.diagram_image_format)}", f"kroki_url = {_toml_string(config.diagram_kroki_url)}", "", "[arc42]", f"template_version = {_toml_string(config.arc42_template_version)}", f"language = {_toml_string(config.arc42_language)}", f"title = {_toml_string(config.arc42_title)}", f"include_help = {_toml_bool(config.arc42_include_help)}", "", "[skill]", f"installed = {_toml_bool(config.skill_installed)}", f"path = {_toml_string(config.skill_path)}", "", "[tracking]", f"enabled = {_toml_bool(config.tracking_enabled)}", "# source-state.json stores SHA-256 content hashes only for files.", "# It does not persist mtimes or file sizes. Directory hashes are", "# derived from file hashes after scanning.", f"state_file = {_toml_string(config.tracking_state_file)}", f"scanner = {_toml_string(config.tracking_scanner)}", "include = [", ] ) lines.extend(f" {_toml_string(item)}," for item in config.tracking_include) lines.extend(["]", "exclude = ["]) lines.extend(f" {_toml_string(item)}," for item in config.tracking_exclude) lines.extend( [ "]", f"max_file_bytes = {config.tracking_max_file_bytes}", f"hash_algorithm = {_toml_string(config.tracking_hash_algorithm)}", "", ] ) return "\n".join(lines)
def _render_build_output_tables( build_outputs: dict[str, dict[str, object]], ) -> list[str]: lines: list[str] = [] for output_name in sorted(build_outputs): output_config = build_outputs[output_name] lines.append(f"[build.outputs.{output_name}]") if "enabled" in output_config: lines.append(f"enabled = {_toml_bool(bool(output_config['enabled']))}") if "tool" in output_config: lines.append(f"tool = {_toml_string(str(output_config['tool']))}") if "pdf_engine" in output_config: lines.append( f"pdf_engine = {_toml_string(str(output_config['pdf_engine']))}" ) if "reference_docx" in output_config: lines.append( "reference_docx = " + _toml_string(str(output_config["reference_docx"])) ) lines.append("") return lines def _toml_bool(value: bool) -> str: return "true" if value else "false" def _toml_string(value: str) -> str: return json.dumps(value) def _validate_enum(value: str, allowed: frozenset[str], field_name: str) -> None: normalized = value.strip().lower() if normalized not in allowed: raise ConfigError( f"{field_name} must be one of: " + ", ".join(sorted(allowed)) + "." )