exlab_wizard.readme.generator#

README generator. Backend Spec §10 / §11.4.

Renders README.md (YAML front matter + Markdown prose body) and the readme_fields.json cache file from a fully-resolved ReadmeContext.

The generator is the single producer for both files. The four field layers (core / template / config / custom) are merged here and validated before any bytes hit the disk: an out-of-bound or missing required field raises before a partial README can be written.

Validation gates (User Interaction Spec §2 + Backend Spec §10.3):

  • label non-empty after trim, <= LABEL_MAX_LENGTH.

  • operator non-empty after trim.

  • objective non-empty after trim, <= OBJECTIVE_MAX_LENGTH.

  • every required: true template field has a value.

  • every required: true config field has a value.

  • field ids are unique across the template + config layers.

  • custom field labels do not collide with the four-layer set’s ids.

  • core field ids (label, operator, objective) are not redeclared by the template or config layer (raises TemplateCoreFieldRedeclaredError).

  • every typed field value matches its declared type (string / text / choice / date / boolean).

Output format follows §10.7: a YAML front matter block delimited by --- lines at the top of the file followed by a Markdown prose body. The front matter is emitted via yaml.safe_dump(..., sort_keys=False) so the document order matches the spec example exactly.

The companion readme_fields.json is written at <dst>/.exlab-wizard/readme_fields.json using the typed ReadmeFieldsJson Struct via msgspec.json.encode (§11.4 contract: every cache file goes through msgspec).

Classes

CoreFields(label, operator, objective)

Mandatory core fields (User Interaction Spec §2).

CustomField(label, value)

An ad-hoc user-added field.

ReadmeContext(level, core, template_fields, ...)

Inputs to ReadmeGenerator.

ReadmeGenerator()

Renders README.md + readme_fields.json.

SystemFields(created, created_by, equipment, ...)

Auto-populated, non-editable system fields.

TemplateFieldDecl(id, label, type[, ...])

Field declaration from a template's _exlab_readme.fields list or from config.yaml readme.defaults.

class exlab_wizard.readme.generator.CoreFields(label, operator, objective)[source]#

Bases: object

Mandatory core fields (User Interaction Spec §2).

All three are non-empty (after trim) when the controller hands the context to ReadmeGenerator; the generator re-validates so a misuse never lets a malformed README onto disk.

Parameters:
label: str#
objective: str#
operator: str#
class exlab_wizard.readme.generator.CustomField(label, value)[source]#

Bases: object

An ad-hoc user-added field. Backend Spec §10.4.

Custom fields are plain string key-value pairs (no type selection) and their order in the output mirrors the order the user added them.

Parameters:
label: str#
value: str#
class exlab_wizard.readme.generator.ReadmeContext(level, core, template_fields, config_fields, custom_fields, system, template_field_decls=<factory>, config_field_decls=<factory>)[source]#

Bases: object

Inputs to ReadmeGenerator. Composed by the controller.

The controller pre-merges the four layers into the dicts below so the generator does not need to know about the merge order; the generator’s job is to validate, render, and persist.

Parameters:
config_field_decls: list[TemplateFieldDecl]#
config_fields: dict[str, Any]#
core: CoreFields#
custom_fields: list[CustomField]#
level: CreationLevel#
system: SystemFields#
template_field_decls: list[TemplateFieldDecl]#
template_fields: dict[str, Any]#
class exlab_wizard.readme.generator.ReadmeGenerator[source]#

Bases: object

Renders README.md + readme_fields.json. Backend Spec §10.

async generate(dst, ctx)[source]#

Validate ctx, write both files, return (readme, cache).

The destination directory dst must already exist (the controller creates it during the directory-render phase). The .exlab-wizard/ cache directory is created on demand.

Both files are written via asyncio.to_thread so the asyncio event loop is never blocked on disk syscalls. The two writes share a single timestamp (ctx.system.created) so the generated_at and created fields agree.

Parameters:
Return type:

tuple[Path, Path]

class exlab_wizard.readme.generator.SystemFields(created, created_by, equipment, template, project, run, run_kind)[source]#

Bases: object

Auto-populated, non-editable system fields. Backend Spec §10.6.

Parameters:
created: datetime#
created_by: str#
equipment: dict[str, str]#
project: str#
run: str | None#
run_kind: str#
template: dict[str, str]#
class exlab_wizard.readme.generator.TemplateFieldDecl(id, label, type, required=False, default='', options=None, hint=None)[source]#

Bases: object

Field declaration from a template’s _exlab_readme.fields list or from config.yaml readme.defaults. Backend Spec §10.3.

The same shape covers both layers: the controller knows which list it came from and packs the matching ReadmeContext slot.

Parameters:
default: Any = ''#
hint: str | None = None#
id: str#
label: str#
options: list[str] | None = None#
required: bool = False#
type: FieldType#