Best Practices for Building a SpecBlock
How to write a SpecBlock that gives SpQE Lab clear product obligations without turning the prototype into a brittle implementation script.
Purpose
Section titled “Purpose”A SpecBlock is the source contract given to the SpQE Lab generation pipeline. It should describe the prototype as an observable product: what it accepts, what it produces, how it behaves in normal and error cases, and which invariants must remain true across generation, rendering, file output, and readback.
The main failure mode is not lack of detail. It is misplaced detail: fragile help prose, invented schema fields, exact headings, overly strict JSON shapes, private helper names, or implementation layouts that do not belong in a product contract.
The guiding principle is: specify stable observable invariants through canonical SpecBlock fields. Do not encode private implementation plans unless the architecture itself is part of the experiment.
How to Use This Page
Section titled “How to Use This Page”This page is not a form to complete.
For a first prototype request, start with the SpQE Prototype Request Form and its LLM-aided completion prompt.
This page is a doctrine and review guide for advanced users, reviewers, or teams building their own SpecBlock assistant, custom GPT, RAG workflow, or internal intake process.
It can be used to:
- review whether a SpecBlock is concrete, testable, and non-ambiguous;
- guide a specialized assistant that helps draft or refine SpecBlocks;
- provide retrieval context for a RAG workflow;
- train internal reviewers on what belongs in canonical SpecBlock fields;
- avoid common failure modes such as brittle help prose, invented schema fields, private implementation plans, or over-constrained JSON shapes.
Do not copy this entire page into every prototype request unless you are intentionally building an advanced prompt or a specialized assistant.
For most users, the practical workflow is:
- Use the Prototype Request Form to generate a first SpecBlock.
- Review the result with the checklist and principles on this page.
- Refine only the parts that affect observable product behavior, qualification, or reproducibility.
No weak modal wording in binding requirements
Section titled “No weak modal wording in binding requirements”Binding SpecBlock fields must not use weak modal wording such as “optional,” “possible,” “if possible,” “when feasible,” “try to,” “preferably,” or “may” when describing behavior that will be generated, tested, repaired, or validated later.
In an agentic generation-and-validation pipeline, a later agent or validator may reinterpret a weak suggestion as a binding requirement, or ignore it as non-binding context. This creates unstable contracts.
If a behavior is required, state it as an observable requirement. If it is not required, leave it out of the binding fields or mark it explicitly as non-binding context. A SpecBlock must distinguish clearly between what the prototype must do, what it may support later, and what is only background information.
Canonical first, flexible inside
Section titled “Canonical first, flexible inside”A strong SpecBlock is binding, but not brittle. It should make the public behavior hard to misunderstand while leaving the generated code enough freedom to converge.
- Binding constraints are observable requirements: inputs, outputs, CLI behavior, help behavior, error categories, deterministic rendering, file-output policy, workspace-root semantics, and domain identifiers that must survive across modules.
- Non-binding details are cosmetic or private details: exact wording, section titles, paragraph order, helper names, internal filenames, and module layouts that are not essential to the public product.
- Canonical fields should carry the contract:
functional_objectives,non_functional_constraints,behavior_contract,help_contract,input_sources,output_targets, andarchitectural_objectiveswhen a real architectural intention is required. - Determinism should be defined as stable observable content: no timestamps, no random ordering, no raw OS exception strings, no accidental absolute paths, and no platform-dependent help assumptions.
- Scope should stay small at first: fewer flags, fewer modes, fewer output dialects, and fewer micro-cases reduce avoidable mismatch and improve convergence.
Semantic closure: use canonical top-level fields, not ad hoc schema
Section titled “Semantic closure: use canonical top-level fields, not ad hoc schema”A SpecBlock should close semantic seams without inventing non-canonical fields. The goal is not to freeze every implementation detail, but to define the smallest shared public dialect that generated modules must preserve.
A frequent failure mode is a prototype that runs successfully but loses meaning across module boundaries. One module emits a record with one set of field names, another module reads different aliases, a renderer replaces the explicit payload with a stale internal copy, or a report preserves numbers while dropping entity names. These are contract-closure failures, not syntax failures.
Do not introduce fields such assemantic_contractfor this purpose. Put semantic closure into the existing machine-facing top-level fields.
Where semantic closure belongs
Section titled “Where semantic closure belongs”- Use
functional_objectivesfor observable product obligations, lifecycle guarantees, and public workflows. - Use
non_functional_constraintsfor determinism, import safety, no-network rules, stable error behavior, and cross-platform constraints. - Use
behavior_contractfor default behavior, stdout and stderr rules, readback expectations, and public result categories. - Use
help_contractfor CLI help, no-args behavior, bad-args behavior, and subcommand help fragments. - Use
input_sourcesandoutput_targetsto declare the visible inputs and outputs that must remain coherent. - Use
architectural_objectivesonly for architecture-level intentions that should genuinely guide PASS1.
Practical semantic closure rules
Section titled “Practical semantic closure rules”- Define the canonical public identifiers that must survive across parsing, calculation, rendering, export, and readback.
- Require structured outputs to preserve non-empty domain identifiers when the product describes real entities.
- Require renderers to render the explicit payload they receive, not a stale internal copy of an older contract.
- Require reports and exports to remain internally consistent with the direct command outputs used to build them.
- Define whether important public sections are lists or mappings when that affects observable behavior.
- For numeric outputs, define public rounding and null semantics without requiring fragile exact prose.
- For edge cases, define the public result category rather than every internal branch.
The right balance is binding on public payload meaning and flexible on internal implementation. Do not prescribe private helper names, internal filenames, or a full module design unless the architecture itself is the tested product.
Generated-module directory: alwaysapp/, never top-levelcore/
Section titled “Generated-module directory: alwaysapp/, never top-levelcore/”For generated prototypes, enforce a simple non-negotiable rule: all application modules must live under theapp/package. Subpackages are allowed, for exampleapp/core/orapp/parsers/, but there must never be a top-levelcore/package in the generated program repository.
This matters because SpQE’s own orchestration code also imports an internal package namedcore. If a generated project creates a root-levelcore/package, Python may resolve imports to the generated project instead of SpQE’s internal package. That shadowing causes inconsistent behavior, missing symbols, false diagnostics, and repair loops that target the wrong module.
Express this constraint through modern SpecBlock fields. Put the architecture intention inarchitectural_objectivesand the import-safety rule innon_functional_constraints.
architectural_objectives:- Generate the prototype as an import-safe Python package under app/.
non_functional_constraints:
* Do not create a top-level core package.* Application modules may use app/core/ as a subpackage, but root-level core/ is forbidden.* Imports must not shadow SpQE Lab internal packages.Advanced module layout with technical_structure
Section titled “Advanced module layout with technical_structure”technical_structure is a legacy field that is still supported.
Use it only when the module layout itself is part of the intended experiment, or when an advanced user needs to impose an explicit multi-module decomposition instead of letting the LLM freely invent the prototype tree.
In the current pipeline, technical_structure.modules is extracted and transmitted to PASS1 through the internal modules payload. This means it can directly influence the generated architectural tree. Even though it remains a legacy surface, it can be particularly useful when a SpecBlock must define precise module ownership, CLI layering, parser boundaries, normalization policy, service responsibilities, rendering boundaries, or error-mapping responsibilities.
This field should not be used for ordinary prototypes. A modern SpecBlock should first describe observable product behavior through functional_objectives, input_sources, output_targets, behavior_contract, help_contract, domain_contracts, non_functional_constraints, and architectural_objectives.
However, ARCHCode multi-module stress-test experiments showed that explicit module structure can be useful when the experiment itself is about generating, repairing, and validating a dense modular architecture. In one JSON-to-CSV CLI stress-test, the source SpecBlock announced a 40-module tree layout, while the generated prototype reached 45 observed modules and still converged to a stable passing result after iterative check-points. This kind of run shows that technical_structure can be used as an advanced architectural control surface, not merely as documentation.
technical_structure can conflict with an architecture automatically selected through a recognized prototype family. If a SpecBlock must remain outside a family-driven architecture, lock that intent explicitly in architectural_objectives.
Example:
{ "architectural_objectives": [ "Do not generate a family-driven prototype architecture for this request.", "Use the explicit module decomposition declared in technical_structure.modules as the primary architectural guide.", "Interpret technical_structure.modules names as repository-relative file paths.", "Do not prepend an additional app/ prefix to module names that already start with app/.", "Keep the prototype under the app package and do not create a top-level core package." ]}Module names inside technical_structure.modules must be interpreted as repository-relative file paths, not as fragments to be prefixed by another package path. For example, app/cli/entrypoint.py means repo_root/app/cli/entrypoint.py, not repo_root/app/app/cli/entrypoint.py.
A compact technical_structure example can look like this:
{ "technical_structure": { "modules": [ { "name": "app/cli/__main__.py", "context_scope": { "inputs": [ { "name": "argv", "type": "list[str] | None" } ], "outputs": [ { "name": "exit_code", "type": "int" }, { "name": "stdout", "type": "str" }, { "name": "stderr", "type": "str" } ], "constraints": [ "Public CLI module entrypoint.", "Import-safe: MUST NOT parse argv or call sys.exit at import time.", "Delegates execution to app.cli.entrypoint.run_cli." ] }, "cli_args": [ { "name": "--input", "type": "str", "required": true, "help": "Path to input JSON file.", "default": null }, { "name": "--output", "type": "str", "required": true, "help": "Path to output CSV file.", "default": null }, { "name": "--report-format", "type": "str", "required": false, "help": "Report format: text or json.", "default": "text" } ], "signature_hint": "def main(argv: list[str] | None = None) -> int:" }, { "name": "app/cli/entrypoint.py", "context_scope": { "inputs": [ { "name": "argv", "type": "list[str] | None" } ], "outputs": [ { "name": "exit_code", "type": "int" } ], "constraints": [ "Owns top-level CLI flow, stdout/stderr emission policy, and exception mapping.", "MUST NOT implement business conversion logic directly.", "Uses app.services.convert_service for conversion/report payload assembly.", "MUST NOT import argparse, instantiate ArgumentParser, or parse argv directly.", "MUST call app.cli.parser.parse_args(...) exactly once per invocation, except explicit no-args/help short-circuit if implemented.", "MUST obtain runtime options through app.cli.normalize.normalize_cli_options(...).", "MUST NOT duplicate parser option definitions, parser validation rules, or help text fragments owned by parser modules." ] }, "cli_args": [], "signature_hint": "def run_cli(argv: list[str] | None = None) -> int:" }, { "name": "app/cli/parser.py", "context_scope": { "inputs": [ { "name": "argv", "type": "list[str] | None" } ], "outputs": [ { "name": "namespace", "type": "object" } ], "constraints": [ "Parses CLI args using argparse.", "Converts argparse usage failures to deterministic user-error behavior.", "No business validation beyond CLI parsing concerns.", "Single owner of argv parsing together with app/cli/parser_factory.py." ] }, "cli_args": [], "signature_hint": "def parse_args(argv: list[str] | None) -> object:" }, { "name": "app/cli/parser_factory.py", "context_scope": { "inputs": [], "outputs": [ { "name": "parser", "type": "object" } ], "constraints": [ "Builds and configures the argparse parser only.", "No parsing execution.", "No sys.exit." ] }, "cli_args": [], "signature_hint": "def build_parser() -> object:" }, { "name": "app/cli/normalize.py", "context_scope": { "inputs": [ { "name": "namespace", "type": "object" } ], "outputs": [ { "name": "options", "type": "dict[str, str | None]" } ], "constraints": [ "Normalizes parser namespace to a fixed-shape options dict.", "Normalizes report_format deterministically.", "No filesystem access." ] }, "cli_args": [], "signature_hint": "def normalize_cli_options(namespace: object) -> dict[str, str | None]:" } ] }}Use technical_structure only when explicit module ownership is necessary for the generation experiment. Otherwise, let the generator derive the architecture from the product contract.
Help contract and CLI determinism: small, explicit, conventional
Section titled “Help contract and CLI determinism: small, explicit, conventional”The help contract is one of the most important convergence levers for CLI prototypes. It must be explicit enough to prevent contradictory behavior, but small enough to avoid brittle checks against argparse wording, operating-system differences, or cosmetic formatting.
The key distinction is simple: voluntary help paths return0and user argument errors return2. A no-args invocation may be a product behavior or a help behavior, but the SpecBlock must say which one. For most generated CLIs, the safest default is no-args help with exit0.
Recommended behavior
Section titled “Recommended behavior”--helpreturns0and writes help to stdout.- No arguments return
0and write help to stdout whenno_args_behaviorisprint_help_and_exit_0. - Bad arguments return
2and write at least one short corrective line to stderr. - Unknown subcommands return
2and write at least one short corrective line to stderr. - Help paths must be evaluated before required-argument validation.
- Help checks should be fragment-based and case-insensitive, not full-text comparisons.
- The parser program name should be stable enough that the
usage:line does not depend on platform-specific invocation details.
Recommended fragment strategy
Section titled “Recommended fragment strategy”- Keep
stable_fragments_allshort: usuallyusage:,--help, the main subcommand, and one core flag. - Use
stable_fragments_anyfor argparse wording variants such asoptions:,optional arguments:, orpositional arguments:. - Use
subcommand_fragmentsfor required flags that must appear in subcommand help. - Avoid mandatory headings such as
examples:,exit codes:, or long prose fragments. - Avoid truncation markers such as
...or…in required fragments.
{ "help_contract": { "rc_semantics": { "help": 0, "no_args": 0, "bad_args": 2, "unknown_subcommand": 2 }, "no_args_behavior": "print_help_and_exit_0", "stable_fragments_all": [ "usage:", "--help", "analyze", "--csv" ], "stable_fragments_any": [ "NoiseDoseLab", "noise", "8h", "options:", "optional arguments:", "positional arguments:" ], "subcommand_fragments": { "analyze": [ "--csv", "--reference-db", "--format" ] }, "notes": "Help fragments are minimal, robust, and case-insensitive. Help and no-args help write to stdout with rc=0. User argument errors write a short corrective stderr line with rc=2." }}This contract prevents the common contradiction where no-args is expected to show help but required arguments are validated first. It also prevents the opposite failure where genuine bad arguments are silently treated like help.
Keep CLI surface small
Section titled “Keep CLI surface small”Every CLI flag adds parsing paths, help surface, tests, and failure modes. The best practice remains to start with a small public CLI and expand later. However, this should be expressed as product scope and observable behavior, not as a mandatorytechnical_structuretemplate.
Usefunctional_objectivesto state the few workflows that must exist. Useinput_sourcesandoutput_targetsto state the visible data flow. Usehelp_contractto bind only the core commands and flags that users must discover.
- Start with the smallest CLI that covers the scientific or business objective.
- Avoid cosmetic flags during first-generation prototypes.
- Do not require multiple output dialects unless they are central to the prototype.
- When a flag is core, include it in the help contract.
- When a flag is optional or exploratory, do not make it a hard help fragment.
Pre-freeze checklist
Section titled “Pre-freeze checklist”- The SpecBlock uses canonical top-level fields for binding behavior.
- Help behavior and bad-argument behavior are not contradictory.
- No-args behavior is explicitly defined.
- Required help fragments are few, stable, and cross-platform.
- Structured outputs preserve domain identifiers across modules and renderers.
- Determinism rules forbid timestamps, random ordering, raw OS exception strings, and absolute paths in user-facing messages unless explicitly required.
- Generated modules live under
app/and never under a root-levelcore/package.
Implementation stability: specify observable constraints, not private helpers
Section titled “Implementation stability: specify observable constraints, not private helpers”Some convergence failures come from implementation patterns that confuse static checkers or repair routing. For example, deeply nested helper functions may be harder to inspect than small top-level private helpers. This can matter when a generated prototype is repaired automatically.
However, a SpecBlock should not normally prescribe private helper names, local function placement, or internal implementation tricks. Those details belong to generation and repair, not to the public product contract.
Use this rule only when implementation stability is genuinely part of the experiment. In that case, express it as a general import-safety or maintainability constraint innon_functional_constraints.
non_functional_constraints:- Keep modules import-safe.- Prefer simple, inspectable module-level private helpers when this improves repairability.- Do not rely on runtime side effects during import.File outputs: parent directories must have an explicit policy
Section titled “File outputs: parent directories must have an explicit policy”CLI file outputs are a frequent source of false failures. A command may compute the right result but fail only because the parent directory of an output path does not exist. If the SpecBlock does not define this policy, tests become ambiguous: the failure can be read either as a program bug or as an undeclared test precondition.
The best default for generated CLI prototypes is simple: when a command writes to a user-supplied output path, it should create missing parent directories automatically. If a prototype should not do that, the SpecBlock must say so explicitly.
Recommended placement in canonical fields
Section titled “Recommended placement in canonical fields”- Use
output_targetsto identify every file output path that the CLI may write. - Use
behavior_contractto define whether missing parent directories are auto-created or treated as a user error. - Use
non_functional_constraintsto require deterministic filesystem errors and avoid raw OS exception strings in user-facing messages.
Recommended default policy
Section titled “Recommended default policy”- For every user-supplied output file path, create missing parent directories before writing.
- If the target file already exists, either overwrite deterministically or define a clear refusal policy.
- If writing fails, return a conventional user-error code and print a short corrective message to stderr.
- Do not expose raw Python tracebacks, absolute temporary paths, or OS-specific exception strings in normal user errors.
output_targets:- JSON report file written when the user provides an output path.- Text report written to stdout by default.
behavior_contract: file_output_policy: parent_directories: create_missing_parents existing_files: deterministic_overwrite_allowed write_failure: user_error_to_stderr
non_functional_constraints:- File-output errors must be deterministic and user-readable.- User-facing error messages must not expose raw tracebacks or raw OS exception strings.This policy is intentionally behavioral. It does not require a specific writer module, helper name, or internal path layout. It only binds what the user and the validation harness can observe.
Workspace root: make state authoritative, observable, and isolated
Section titled “Workspace root: make state authoritative, observable, and isolated”Stateful CLI prototypes often fail when--workspace-rootis treated as a cosmetic option rather than as the authority for state. The flag appears in help, but commands may still read from the current working directory, write to another hidden location, or delete a record without making the deletion observable on later reads.
A good SpecBlock must make workspace behavior externally coherent. For the same explicit workspace root and the same entity identifiers, later read commands must observe the effects of earlier successful mutating commands. For a different workspace root, that state must not accidentally appear.
Recommended placement in canonical fields
Section titled “Recommended placement in canonical fields”- Use
functional_objectivesto define the public lifecycle: create, read, update, delete, list, import, export, or any smaller lifecycle actually required. - Use
input_sourcesto declare--workspace-rootas an explicit CLI input when the prototype is workspace-scoped. - Use
output_targetsto describe the observable readback outputs and exported files. - Use
behavior_contractto bind read/write coherence, deletion semantics, and root isolation. - Use
non_functional_constraintsto forbid silent fallback to the process working directory once a root is supplied. - Use
help_contractto ensure that the root flag is discoverable when it is part of the public CLI.
Binding workspace invariants
Section titled “Binding workspace invariants”--workspace-rootis authoritative for every workspace-scoped command.- All reads and writes for a command must resolve against the same effective root.
- A successful mutation under root
Rmust be observable through later reads under rootR. - State created under root
R1must not appear under rootR2unless explicitly recreated there. - After a successful delete or remove, later reads under the same root must no longer resolve the deleted entity.
- Once a root is supplied, the implementation must not silently fall back to the current working directory or to a second hidden storage location.
input_sources:- CLI option --workspace-root for workspace-scoped commands.
functional_objectives:- Support a public lifecycle whose mutations are observable through later read commands under the same workspace root.
behavior_contract: workspace_root_policy: authority: explicit_workspace_root same_root_readback: required cross_root_isolation: required deletion_visibility: deleted_entities_absent_from_later_reads
non_functional_constraints:- Do not silently fall back to the current working directory when --workspace-root is provided.- Do not use a second hidden storage root for the same logical entity.- Keep workspace behavior deterministic across platforms.
help_contract: stable_fragments_all: - usage: - --help - --workspace-rootKeep the wording at the level of externally visible behavior. Do not require a specific internal filename such asworkspace.json. Do not force one persistence helper or one module name. What matters is that the same root, the same entity identifiers, and the same lifecycle produce consistent readback over time.
Final SpecBlock sanity check
Section titled “Final SpecBlock sanity check”- Every binding requirement is carried by a canonical field.
- No non-canonical schema is invented for a rule that can fit in
functional_objectives,non_functional_constraints,behavior_contract,help_contract,input_sources,output_targets, orarchitectural_objectives. - CLI help behavior and user-error behavior are separated: voluntary help returns
0on stdout, while bad arguments return2with a short stderr message. - File-output parent-directory behavior is explicit.
- Workspace-root behavior is authoritative, observable, and isolated when the prototype is stateful.
- Structured outputs preserve domain identifiers across parsing, calculation, rendering, export, and readback.
- Architecture constraints are stated only when they are genuinely needed.
- The generated prototype remains import-safe and avoids root-level package names that can shadow SpQE Lab internals.