Files
notarius/docs/roadmap/configkit.md

616 lines
31 KiB
Markdown

# ConfigKit Adoption
## Purpose
Replace Notarius's application-owned YAML parsing, presence bookkeeping,
default application, and configuration-inspection machinery with ConfigKit,
while preserving Notarius ownership of pipeline semantics, module catalogs,
runtime resolution, and invocation behavior.
This roadmap is based on ConfigKit v0.2.0, the current stable release in the
local `../ConfigKit` checkout. ConfigKit's maintained consumer guide includes a
Notarius-shaped example, and its complete test suite passes locally. The
dependency starts at v0.2.0; completing source-aware normalization requires a
reviewed release with the extension below. Do not follow an unreviewed branch
tip. Ordered implementation work belongs in
[the implementation plan](implementation.md).
### Source Metadata Dependency
Review for implementation identified an API gap in v0.2.0: `Resolution`
exposes input presence and change recording, but not declaration origins or
source paths. Those are available only on the completed `Result`, whose
effective and explanation snapshots are already frozen. The target of
recording declaration-relative path normalization in those snapshots therefore
requires an upstream extension. The selected approach is to request source
origins and canonical source records during resolution, then adopt a reviewed
release providing that API. The implementation plan records the dependency
gate. Resolving paths only after loading with a separate inspection report was
considered but not selected; the agreed effective/explanation contract remains
the target. ConfigKit v0.2.0 alone is insufficient for that part of the target.
## Motivation
The current configuration boundary under `internal/core/config` works, but it
reimplements a substantial general-purpose configuration system. In
particular, Notarius currently owns:
- strict YAML setup, duplicate-key and trailing-document checks;
- custom YAML decoders for module bindings, references, pipelines, steps, and
lanes;
- pointer and private-flag bookkeeping to distinguish omitted values from
explicit zero, false, empty, or null values;
- a large file-to-runtime application function that combines normalization,
validation, conversion, and default derivation;
- operational environment overlays and their separate validation path; and
- bespoke cloning and redaction for effective configuration summaries.
ConfigKit already provides strict single-document YAML parsing, typed decode,
input presence, typed defaults, recorded normalization and derived defaults,
semantic diagnostics with source locations, detached results, redacted
effective rendering, and value explanation. Adopting those facilities should
reduce application-specific code and make configuration failures easier for an
operator to understand without moving domain policy into the dependency.
This is a configuration-contract migration, not a mechanical parser swap.
ConfigKit deliberately rejects several conveniences supported by version 4,
and its dynamic integer representation differs from `gopkg.in/yaml.v3`.
## Target End State
After this work:
- every command uses one ConfigKit-backed loading path;
- Notarius configuration files declare schema version 5 using the existing
integer YAML convention;
- the file schema uses canonical object forms rather than YAML type-dependent
shorthand;
- operators can import whole pipeline definitions and select a configuration
profile for development, testing, or deployment;
- defaults, surviving-input presence, normalization, derived values, and
configuration-only validation are expressed through one ConfigKit
`Definition`;
- ConfigKit diagnostics are rendered safely with semantic paths and available
source locations at the CLI boundary;
- the successful load result retains ConfigKit metadata for safe effective
configuration rendering and explanation;
- dynamic module-option integers have one documented `int64` configuration
representation and are range-checked when converted to runtime `int` values;
- pipeline and module resolution remains in Notarius and still fails before
module preparation when a configured composition is invalid;
- configuration discovery and invocation-only CLI state remain application
responsibilities;
- `NOTARIUS_CONFIG` remains a configuration-discovery input, while the seven
field-level operational environment overlays are retired;
- read-only CLI commands expose ConfigKit's redacted effective configuration
and per-path provenance explanations; and
- the direct `gopkg.in/yaml.v3` dependency and the superseded custom parsing,
presence, environment-overlay, and configuration-redaction code are removed.
The migration must preserve the fixed pipeline architecture, deterministic
resolved-pipeline identity, detached ownership, fail-before-execution checks,
secret exclusion, source-relative reference behavior, and offline validation
requirements defined by the architecture and current integration contracts.
## Ownership Boundaries
### ConfigKit owns
- reading and bounding the selected YAML source, its declared imports, and the
selected configuration profile after Notarius chooses them;
- composing imported definitions and applying the selected profile overlay;
- enforcing a single strict YAML document, supported scalar forms, unique
mapping keys, and known typed fields;
- decoding into the application-defined configuration schema;
- distinguishing omitted input from explicit zero, false, empty, and null;
- applying typed application defaults;
- recording application normalization and derived defaults;
- attaching application validation diagnostics to semantic paths and source
locations;
- detaching the successful configuration and its metadata;
- rendering the redacted effective configuration; and
- explaining the provenance of an effective or historical configuration path.
### Notarius continues to own
- configuration-file discovery through `--config`, `NOTARIUS_CONFIG`, and the
installed default path;
- the version policy and the application configuration structs;
- the meaning and validation of every Notarius field;
- pipeline, module, validator, artifact, and reference catalogs;
- conversion from configuration-facing values to runtime pipeline types;
- source-relative interpretation of configured external reference paths;
- run-time selectors and actions such as lane selection, input, resume,
recomputation, session identity, and CLI reference changes;
- PromptKit profile discovery and validation; and
- redaction of resolved runtime structures that are outside ConfigKit's typed
configuration snapshot, including exclusion of materialized reference
content.
ConfigKit configuration profiles must not be confused with PromptKit LLM
profiles or Notarius named pipeline definitions. ConfigKit does not become a
module registry, pipeline resolver, command framework, or secret manager for
PromptKit credentials.
## Configuration Contract Version 5
### Version spelling
Use the following root declaration:
```yaml
version: 5
```
ConfigKit v0.2.0 accepts string or strict decimal-integer input at the exact
path selected by its `VersionRule`. It passes the original spelling to the
application checker and can decode an accepted integer into a string-backed Go
field. This narrow exception lets Notarius retain its existing numeric YAML
convention without weakening strict decoding elsewhere. Quoted `version: "5"`
is also accepted by ConfigKit, but the maintained Notarius spelling is the
unquoted integer shown above.
The version checker must reject absent, null, and unsupported identifiers with
safe, value-free diagnostics at `/version`. Version 5 is a clean break: the
loader must not decode or rewrite a version 4 document and must not retain the
legacy loader as a fallback.
### Canonical module bindings
Every module and validator binding must use an object with a `module` field.
For example:
```yaml
input:
module: seriatim
extract:
module: dnd/spells
merge:
module: appendorder
normalize:
module: dnd/spells
```
Version 5 does not accept scalar binding shorthand. Preserving shorthand would
require a dynamic subtree or a compatibility pre-parser and would retain much
of the complexity this adoption is intended to remove.
### Canonical reference sources
Reference sources must also use explicit objects. An external file reference
uses a `path` field; a generated reference retains its typed `artifact`
selector. For example:
```yaml
references:
party:
path: ./dnd-party.txt
npc_registry:
artifact:
step: describe-session
lane: npc-registry
```
Pipeline-level references remain external-file-only. Step- and binding-local
references may select earlier artifacts according to the existing ordered
handoff rules. Relative external paths resolve from the file that declared the
surviving value, including imported fragments and configuration profiles. CLI
reference paths continue to resolve from the process working directory.
Remove the lane-level `references` compatibility alias in version 5. New files
must place a lane's extraction references under `extract.references`. This
leaves one canonical location and removes merge-precedence behavior from the
file converter.
### Strictness and dynamic options
Keep ConfigKit's strict YAML rules. Version 5 therefore rejects aliases, merge
keys, unsupported tags and scalar forms, duplicate mapping keys, unknown typed
fields, trailing documents, and incompatible YAML types. Do not recreate an
application pre-parser to weaken these rules.
Module `options` remain `map[string]any` where the selected module determines
the valid keys. ConfigKit represents YAML integers in dynamic values as
`int64`. Module option validators must accept that canonical representation,
check range before conversion, and hand runtime modules a safe `int` only where
their APIs require it. Do not marshal and decode dynamic subtrees through YAML
to recover native `int` values.
## ConfigKit-Backed Loading Model
Define a plain, typed configuration document for ConfigKit. It should mirror
the public file schema, use ordinary YAML tags, and contain no custom YAML
unmarshal hooks. Keep it separate from framework runtime types where doing so
avoids YAML concerns, presence flags, or configuration-only unions leaking into
`internal/framework/pipeline`.
The reusable ConfigKit definition should provide:
- fresh built-in defaults for each load;
- the `/version` rule;
- recorded whitespace normalization for identifiers, paths, profile IDs,
endpoints, and other fields whose current contract normalizes input;
- recorded derivation of `concurrency.stage_workers.extract` from the effective
`concurrency.total_llm` only when the worker value was omitted;
- collision checks before normalizing map keys or cross-step identifiers;
- configuration-only semantic validation with precise paths and related paths
where useful; and
- a dynamic secret predicate that covers credential-shaped keys below module
option maps, preserving the current defensive redaction boundary even though
ordinary Notarius configuration contains no credential values.
Use ConfigKit's resolver only for deterministic configuration normalization and
derived defaults. Use its read-only semantic validator for domain constraints.
Do not put module lookup, PromptKit profile lookup, filesystem resource checks,
or runtime pipeline construction in either callback.
After a successful load, perform one explicit, independently tested conversion
from the canonical configuration document to the Notarius runtime `Config` and
pipeline profile types. The converter should be a narrow ownership boundary,
not a second parser or validation framework. It may translate enums and checked
numeric values, but it must not silently change already validated meaning.
Retain the ConfigKit result alongside the runtime configuration in an
application-owned loaded-configuration value. Commands that need only the
runtime `Config` should not gain access to parser internals; commands and debug
writers that expose safe configuration provenance may use the retained result.
## Imports And Configuration Profiles
Enable ConfigKit's explicit imports using its existing `configkit.imports`
syntax, including path, glob, and optional declarations. Operators can keep
whole pipeline definitions in separate files and compose them through the
selected root file. The root file alone declares `version`.
Use ConfigKit's strict composition semantics. Imports combine disjoint
definitions; they do not provide ordered override layers or concatenate
sequences. In particular, one pipeline's ordered `steps` list remains together
in a single definition. Do not add Notarius-specific step merging or a second
import mechanism.
Expose `--config-profile <name>` on every command that loads configuration:
`run`, `config validate`, `pipelines list`, `config effective`, and
`config explain`. Pass the selection to ConfigKit. A missing selection uses
the composed base configuration. ConfigKit's reserved selection `default`
also means no overlay; it does not load a default profile file.
Use ConfigKit's profile location `<configuration-root>/profiles/<name>.yml`
and existing selection rules. A selected missing profile fails loading.
Profiles recursively overlay mappings and replace scalars, sequences, and
nulls as specified upstream. Development profiles can change pipeline-level
`llm_profile`, concurrency, and output settings without repeating the pipeline.
Changing an individual binding inside `steps` requires replacing the complete
steps sequence. Document this limitation prominently in examples.
Configuration profiles and PromptKit profiles are independent. The former
select a configuration overlay with `--config-profile`; the latter select LLM
settings through `llm_profile` and the existing `--llm-profile` override. Neither
selection implicitly chooses the other.
Use the selected file's canonical directory as ConfigKit's configuration root.
Document the upstream requirement that imports and configuration profiles stay
within that root, along with composition conflicts, cycle/repeated-import
errors, and profile sequence replacement. This containment rule does not apply
to Notarius input, reference, output, cache, debug, or PromptKit profile paths.
### Paths And Source Ownership
Resolve relative external reference paths and `promptkit.profile_file` and
`promptkit.profile_dir` relative to the file that supplied the surviving value.
This deliberately changes PromptKit profile-source paths from their current
working-directory convention in version 5. Absolute paths remain absolute.
Keep output, cache, and debug relative-directory semantics unchanged; they are
runtime destinations rather than files bundled with configuration fragments.
Use ConfigKit's source metadata to identify each surviving declaration's
origin, including profile replacements and values inherited from the base.
Record path normalization so effective output and explanation reflect the
paths used at runtime. Preserve attribution when identifier normalization
renames an enclosing map key. Do not resolve overwritten or removed path
values or infer source ownership from diagnostic display labels. Explicit CLI
paths retain their working-directory interpretation.
## Discovery, Precedence, And Overrides
Keep the existing file-discovery order: explicit `--config`, then non-empty
`NOTARIUS_CONFIG`, then the installed default path. `NOTARIUS_CONFIG` selects a
source and is not a field-level configuration override, so it remains outside
ConfigKit.
Retire these seven field-level environment overlays in version 5:
- `NOTARIUS_TOTAL_LLM_CONCURRENCY`;
- `NOTARIUS_STAGE_WORKERS_EXTRACT`;
- `NOTARIUS_OUTPUT_DIR`;
- `NOTARIUS_CACHE_CHUNK_PLANS_MODE`;
- `NOTARIUS_CACHE_CHUNK_PLANS_DIR`;
- `NOTARIUS_CACHE_CHECKPOINTS_DIR`; and
- `NOTARIUS_DEBUG_DIR`.
ConfigKit deliberately does not model general environment overlays. Applying
them after a successful load would make the retained effective configuration,
origins, and explanations disagree with the values used at runtime. Operators
must put durable values in YAML and use the supported explicit CLI flags for
invocation-local changes.
Classify the current run flags by responsibility. A flag that directly
overrides a field in the typed configuration document should become a
ConfigKit `Override` record only when the operator explicitly supplied it;
parser defaults must not be submitted as overrides. Input selection, lane
selection, session identity, resume/recompute actions, command-line references,
and similar invocation state remain outside ConfigKit even when they affect
runtime resolution. The documented precedence becomes built-in defaults,
composed YAML sources, the selected configuration profile, explicit
configuration-valued CLI overrides, and then
invocation-only resolution actions at their existing application boundary.
## Validation And Diagnostics
Move configuration-only checks currently split among parsing, file
application, and `Config.Validate` into the ConfigKit
definition when they can be expressed against the typed file contract. Report
all independently discoverable semantic errors in one validation pass rather
than failing at the first field, while avoiding cascaded diagnostics whose only
cause is an earlier invalid parent.
Keep catalog-dependent checks in `Config.Resolve` and
`pipeline.ResolvePipeline`. These include registered module and validator
existence, stage capabilities, artifact compatibility, reference-slot
compatibility, PromptKit profile availability, and the concrete resolved
pipeline digest. Their existing error classification and fail-before-prepare
behavior must remain intact.
At the CLI boundary, recognize `*configkit.LoadError` and use ConfigKit's
canonical diagnostic renderer. Operators should receive the diagnostic code,
semantic path, safe message, and source coordinates when available. Do not
append raw YAML parser errors, source fragments, configuration values, or
secret values to the outward error chain. File discovery/read context should
remain clear without wrapping the same failure repeatedly.
Nonfatal ConfigKit diagnostics, if Notarius defines any, must not be silently
dropped. They should enter the existing process-warning model only when they
represent an actionable configuration degradation. Ordinary normalization,
defaulting, and provenance records are explanation data, not run warnings.
## Effective Configuration And Redaction
Use `Result.WriteEffective` as the canonical renderer for the loaded effective
configuration. The requested debug bundle should store this deterministic,
redacted YAML rather than a separately maintained JSON clone of the raw
configuration. Update the artifact name and documentation together if the
format changes.
Use ConfigKit's secret predicate to redact credential-shaped dynamic option
paths. PromptKit credentials remain environment references in PromptKit-owned
profile files and must not be copied into Notarius configuration or debug
state.
Do not remove Notarius's redaction of the resolved pipeline merely because the
loaded configuration is ConfigKit-backed. The resolved value contains
application-owned structures, selection results, and reference provenance that
ConfigKit does not own; materialized reference bytes must continue to be
excluded from ordinary summaries. Remove only manual configuration cloning and
redaction made redundant by the frozen ConfigKit result.
## Configuration Inspection Commands
Add these read-only commands as first-class ConfigKit-backed inspection
surfaces:
```text
notarius config effective [--config path/to/config.yml] [--config-profile name]
notarius config explain [--config path/to/config.yml] [--config-profile name] <json-pointer>
```
`config effective` loads the selected configuration through the production
path and writes only ConfigKit's deterministic redacted YAML to standard
output. `config explain` accepts one canonical JSON Pointer, loads through the
same path, and writes ConfigKit's deterministic redacted explanation for that
effective or historical path. Neither command resolves a pipeline, prepares a
module, requires provider credentials, or contacts a provider.
Both commands must use ordinary configuration discovery, ConfigKit diagnostic
rendering, the same import and profile selection as execution, and the existing
CLI exit-status conventions. An unknown or
noncanonical explanation path is a command failure, not an empty successful
result. Standard output contains only the requested inspection output;
diagnostics go to standard error. Effective output is YAML and explanation is
ConfigKit's human-readable text; neither is promised to be reloadable
configuration or a new durable JSON API. There is no unredacted mode.
## Required Code And Contract Changes
The implementation must address the following coherent work areas:
- add and pin a reviewed `gitea.maximumdirect.net/eric/ConfigKit` release with
v0.2.0's semantics and the required source-origin API;
- introduce the version 5 typed document and its ConfigKit definition;
- replace `LoadFileConfig`, custom `UnmarshalYAML` implementations, manual
strict-parser checks, presence flags, `ApplyFileConfig`, and redundant
configuration cloning with the ConfigKit load boundary;
- consolidate defaults, normalization, and configuration-only validation into
the definition while retaining runtime catalog validation;
- update CLI configuration loading and error rendering without changing file
discovery;
- enable ConfigKit imports and configuration profiles, wire `--config-profile`
through all configuration-loading commands, and retain declaration origins
for relative file paths;
- retire field-level environment overlays while retaining `NOTARIUS_CONFIG`;
- classify current run flags so only true configuration overrides use
ConfigKit override records and invocation-only state remains outside the
configuration loader;
- update dynamic module-option decoding and range checks for `int64`;
- integrate ConfigKit effective rendering and metadata into requested debug
bundles and the new `config effective` and `config explain` commands;
- remove superseded code, tests, and the direct `gopkg.in/yaml.v3` dependency
after no production or test-only use remains; and
- update maintained examples and canonical documentation in the same change as
each externally visible contract adjustment.
## Documentation And Migration
Update `docs/config.md` as the canonical owner of version 5 syntax, defaults,
precedence, canonical bindings and references, strict YAML behavior, and the
retirement of field-level environment overrides. Replace the version 3
migration section with a concise version 4-to-5 migration guide covering:
- changing the numeric version value from 4 to 5;
- expanding scalar module bindings into `{module: ...}` objects;
- expanding scalar external references into `{path: ...}` objects;
- moving lane-level reference aliases to `extract.references`;
- resolving configured PromptKit profile-source paths from their declaring
file instead of the working directory;
- using imports for whole pipeline definitions and configuration profiles for
overlays, including complete replacement of step sequences;
- replacing retired operational environment overrides with YAML or supported
explicit CLI flags; and
- any effective/debug artifact or inspection-command changes.
Update `docs/internal/configuration.md` to describe the ConfigKit boundary,
typed document-to-runtime conversion, application-owned validation, retained
metadata, and catalog-resolution handoff. Update `docs/cli.md` only for changed
commands or diagnostic behavior, `docs/operations.md` only for operator-facing
configuration and debug workflows, and the relevant architecture text if the
adopted precedence model changes.
Convert both maintained D&D configurations and every configuration snippet to
version 5 canonical syntax. Examples must remain secret-free and load through
the same production path exercised by the CLI. Do not keep a second set of
version 4 examples.
Keep the minimal D&D example self-contained. Extend the complete D&D example
with an imported whole pipeline definition and a selectable development
configuration profile. Keep configuration overlays physically distinct from
the maintained PromptKit profile files so readers can see which system owns
each file. Test the complete example both with and without the development
overlay, and update subprocess guidance to show optional `--config-profile`
selection. All configuration inspection commands must document what the
selected profile changes and that runtime pipeline resolution is separate.
Because this change establishes a durable dependency and reallocates ownership
at a core boundary, add an ADR recording the decision to use ConfigKit, the
responsibilities retained by Notarius, the deliberate version 5 break, and the
retirement of the application environment-overlay layer. The rejected
alternatives should include continuing the custom loader, retaining version 4
temporarily, or wrapping ConfigKit in a compatibility pre-parser. The ADR
should record rationale, not duplicate the current configuration reference or
implementation details.
## Testing And Verification
Follow the repository's behavior-focused testing policy. Reuse ConfigKit's own
tests for generic YAML and metadata mechanics; Notarius tests should protect
only its contract and integration responsibilities.
Required Notarius coverage includes:
- successful loading of omitted, explicit zero/false/empty, and derived values
that materially affect Notarius behavior;
- numeric and quoted version 5 acceptance and safe rejection of absent, null,
version 4, unsupported identifiers, and non-integer numeric forms;
- canonical binding and reference decoding plus rejection of former shorthand;
- identifier normalization and normalized-collision rejection;
- aggregation and source location of representative application validation
errors;
- unchanged configuration discovery and source-relative reference resolution;
- imported whole pipeline definitions, representative composition conflicts,
and profile precedence through the real ConfigKit loader;
- profile sequence replacement and explicit clearing, so Notarius does not
accidentally append or merge ordered steps;
- relative reference and PromptKit profile-source paths from root files,
imported fragments, and overlays, including an unchanged inherited value,
a replaced value, and an enclosing normalized identifier;
- matching configuration-profile selection across execution, validation,
pipeline listing, effective output, and explanation, without provider calls;
- checked dynamic integer conversion at each production module option boundary;
- detached ownership of loaded configuration and resolved pipelines;
- safe CLI rendering of representative ConfigKit parse, decode, version,
resolution, and validation failures;
- successful redacted `config effective` output and effective, historical,
unknown, and noncanonical `config explain` path behavior;
- effective-configuration redaction, including nested credential-shaped dynamic
option keys, without exposing materialized references;
- maintained minimal and complete example validation through the production
loader and production module catalog; and
- representative end-to-end CLI validation and pipeline resolution without
network access or credentials.
Delete or rewrite legacy parser tests whose only purpose is to retest behavior
owned by ConfigKit. Retain a small integration sample for each strictness rule
that is itself part of the Notarius public contract, and avoid assertions on
incidental diagnostic prose.
The completed implementation must pass:
```sh
go test ./...
go vet ./...
go build ./cmd/notarius
```
It must also validate both maintained example files with the production CLI.
The complete example may use a placeholder credential for offline PromptKit
profile validation, as documented today; no live provider call is required.
## Non-Goals
- Moving pipeline resolution, module catalogs, or PromptKit profiles into
ConfigKit.
- Adding arbitrary environment-to-field binding.
- Retaining the retired field-level operational environment variables as a
parallel precedence layer.
- Loading credentials into the Notarius configuration document.
- Supporting remote configuration sources or formats other than YAML.
- Adding a generic dynamic-subtree conversion layer.
- Composing ordered steps across fragments or adding application-specific
sequence merge, profile inheritance, or profile stacking semantics.
- Preserving scalar shorthand with an application pre-parser.
- Changing the fixed pipeline shape or durable extraction artifacts.
- Changing run input, output publication, cache, checkpoint, retry, or warning
semantics except where configuration loading or debug rendering directly
exposes them.
## Acceptance Criteria
- All configuration-loading commands use a pinned ConfigKit release providing
the required source-origin API, building on v0.2.0, and no production code
directly imports a YAML parser.
- Version 5 is the only accepted file contract, documents `version: 5` as its
canonical spelling, and has one canonical object representation for module
bindings and references.
- The ConfigKit definition is the sole owner of typed defaults, input presence,
recorded normalization, derived configuration defaults, and
configuration-only semantic diagnostics.
- Notarius remains the sole owner of catalog-dependent resolution and runtime
conversion.
- Dynamic option integers are handled without unchecked narrowing or YAML
round-trips.
- CLI diagnostics and effective configuration output are deterministic,
redacted, value-safe, and retain useful path/source context.
- `config effective` and `config explain` expose only ConfigKit's redacted
inspection views and perform no pipeline or provider work.
- Imports compose whole pipeline definitions and selected configuration
profiles apply ConfigKit's documented overlay semantics consistently across
all configuration-loading commands.
- Relative external references and PromptKit profile-source paths use the
surviving declaration's source directory, and safe inspection agrees with
the paths used at runtime.
- The resolved pipeline remains detached, reference content remains excluded
from ordinary summaries, and its identity changes under the same semantic
conditions as before.
- Maintained examples, current documentation, focused tests, and repository-wide
validation agree with the implemented version 5 contract.
- `NOTARIUS_CONFIG` still participates in discovery, while the seven
field-level operational environment variables no longer affect loaded
configuration.
- Superseded custom parser, presence, environment, clone, redaction, and test
code is removed rather than left as an alternate loading path.
## Deferred Composition Work
Step-level composition may be revisited after operators use imported whole
pipeline definitions. It is not part of this adoption: ConfigKit does not
concatenate sequences or merge their elements by application-defined IDs.
Any future extension must define ordering, duplicate step IDs, dependency
validation, profile overrides, and provenance before changing that contract.