Key changes:
- Added internal/project/effective.go with normalized effective config, defaults, provenance, and
JSON output.
- Added config subcommands:
- config validate
- config effective [--json|--yaml]
- config inspect [<key>]
- config explain <key>
- config sources
- Added YAML schema fields for configurable outputs, cache roots, script cache, inventory extensions,
topdata output/wiki paths, and autogen cache root.
- Routed existing hardcoded paths through effective config wrappers for module archives, HAK
manifests/archives, script cache, music cache/credits, topdata outputs, and autogen caches.
- Added validation for unsafe generated output/cache paths.
- Updated command descriptions to avoid fixed build/, .cache, sow_top, etc.
- Added regression tests for defaults, provenance, deterministic effective config, YAML-controlled
derivation, config commands, and invalid paths.
26 KiB
Create a long-term multi-repository hardening plan focused on making YAML configuration the authoritative source of truth.
Goal
Reduce hardcoded behavior across all repositories and toolkit code.
All behavior that touches repository-specific configuration must be driven by YAML. If a behavior currently exists only as an explicit rule in application code, move it into configuration or create a new configuration option for it.
Core principle
Configuration is fact.
Code should interpret and enforce configuration, not silently override it.
Requirements
- YAML is the only human-authored repository configuration format.
- JSON remains acceptable only for generated artifacts, datasets, manifests, caches, and machine-readable outputs.
- Any code path that touches configurable behavior must lock to YAML-defined values.
- If YAML defines a value, code must not substitute another value unless explicitly configured as a fallback.
- If behavior varies by repository, dataset, build target, asset type, or command, it belongs in YAML.
- If no configuration option exists for a behavior, create one.
- Remove implicit repository assumptions from code.
- Remove hidden defaults unless they are documented toolkit-level defaults.
- Make all defaults visible, documented, and overrideable.
Audit scope
Audit all repositories and toolkit code for hardcoded or implicit behavior related to:
- paths
- module metadata
- resrefs
- HAK names
- HAK order
- HAK grouping
- HAK priority
- HAK splitting
- HAK size limits
- optional HAKs
- include/exclude globs
- generated manifest names
- cache names
- release tags
- dataset names
- autogen producers
- autogen consumers
- derive rules
- music prefixes
- credits behavior
- extract behavior
- topdata behavior
- package names
- TLK names
- build directories
- source directories
- validation rules
- cleanup rules
- normalization rules
- naming schemes
- file extension rules
- ignore rules
- command-specific behavior
- logging verbosity
- deterministic ordering rules
Hardening rules
-
YAML wins. If YAML declares a value, the application must use it exactly.
-
No silent fallbacks. Fallbacks must be explicit, documented, and visible in logs.
-
No hidden repository assumptions. Repository-specific behavior must not live in toolkit code.
-
No magic names. Names like
sow_top,sow_core,content,build,.cache, or specific manifest names must come from YAML unless they are generated from a configured rule. -
No implicit behavior drift. Existing behavior should remain stable unless the YAML explicitly changes it.
-
Deterministic by default. Ordering, output paths, generated names, cache keys, manifests, and logs must be stable.
-
Validation before execution. Invalid or incomplete configuration should fail early with clear errors.
-
Configuration gaps become schema work. If code needs a behavior that YAML cannot express, extend the schema.
Implementation direction
- Build a central configuration model.
- Route all repository behavior through that model.
- Add typed config objects or schema validation.
- Normalize config once at startup.
- Pass normalized config into commands instead of reading globals or hardcoded constants.
- Make command behavior depend on normalized config only.
- Add explicit toolkit defaults in one place.
- Generate an effective configuration view for debugging.
- Add a command such as:
toolkit config inspect
toolkit config validate
toolkit config explain <key>
toolkit config effective
Effective config
Add support for printing the final resolved configuration after defaults, overrides, and dataset-specific settings have been applied.
This should help verify that:
- YAML was loaded.
- defaults were applied intentionally.
- no hidden behavior was used.
- paths resolved as expected.
- command behavior is explainable.
Repository hardening process
For each repository:
- Inventory existing YAML config.
- Inventory generated JSON artifacts separately.
- Search code for hardcoded repository behavior.
- Map each hardcoded behavior to an existing YAML entry.
- If no YAML entry exists, add one.
- Update schema and validation.
- Update code to consume normalized config.
- Add tests proving YAML controls the behavior.
- Add regression tests for existing outputs.
- Document the config option.
Testing requirements
Add tests for:
- YAML config loading
- config validation
- effective config generation
- deterministic ordering
- missing required config
- duplicate config entries
- invalid paths
- invalid HAK definitions
- invalid autogen definitions
- invalid dataset definitions
- command behavior matching YAML
- no hardcoded repository names
- JSON artifacts remaining generated data only
Add regression tests where possible:
- same YAML input produces same build plan
- same YAML input produces same HAK order
- same YAML input produces same manifest names
- same YAML input produces same cache paths
- same YAML input produces same autogen behavior
Code audit targets
Look specifically for:
- string literals that look like paths, names, prefixes, manifests, or cache files
- constants that duplicate config values
- default values inside command implementations
- repository-specific branch logic
- command-specific assumptions
- filename conventions not declared in config
- implicit glob patterns
- implicit output names
- implicit ordering rules
- duplicated config parsing
- separate code paths that bypass central config
Deliverables
- Long-term hardening plan
- Config authority rules
- Audit checklist
- Proposed YAML schema expansions
- Effective config design
- Migration strategy per repository
- Testing strategy
- Documentation strategy
- List of hardcoded assumptions found
- Refactor tickets grouped by risk and priority
Acceptance criteria
- YAML is the only human-authored repository config.
- Code does not override YAML silently.
- Repository-specific behavior is not hardcoded.
- Any behavior touching config is lock-set to the normalized YAML value.
- Missing config options are added instead of encoded in application logic.
- Generated JSON artifacts remain unchanged unless intentionally modified.
- Builds remain deterministic.
- Existing repositories continue to build.
- Developers can inspect the effective config and understand why each value was used.
Current project-needs analysis
Current state
- Root repository configuration is already discovered as:
nwn-tool.yamlnwn-tool.yml- legacy
nwn-tool.json
- YAML is decoded with
KnownFields(true). - Legacy JSON is decoded with
DisallowUnknownFields(). internal/app.loadProjectprints the loaded config filename and warns when legacy JSON is used.- The root config model currently lives in
internal/project.Config. internal/project.defaultConfigcurrently only defaultspaths.buildtobuild.- Generated and machine-readable JSON remains widespread and should stay JSON:
- HAK manifest:
build/haks.json - canonical GFF JSON documents
- topdata authored/generated datasets
- topdata wiki state
- topdata deploy manifest
- autogen manifests and release caches
- credits inventory
- HAK manifest:
- Some behavior is already YAML-driven:
- module name/resref/description
- module HAK order
- source/assets/build paths
- HAK names, priorities, split behavior, max bytes, optional flag, include globs
- music prefix mapping
- topdata source/build/reference/assets/package names
- extract ignore extensions and post-extract module deletion
- autogen producer/consumer ids, roots, includes, modes, derive rules, release tags, asset names, cache names
- Some behavior is still implicit in code and should be made explicit or documented as toolkit defaults.
Important naming clarification
The current repository root config file is nwn-tool.yaml, not config.yaml.
Hardening assumption:
- Keep
nwn-tool.yamlas the canonical root config filename. - Keep
nwn-tool.ymlas an accepted alternate YAML filename. - Treat
nwn-tool.jsonas temporary legacy compatibility only. - Do not rename generated or nested JSON files.
- Do not treat topdata dataset JSON as root repository configuration.
If a future decision changes the canonical name to config.yaml, update project.ConfigFile, docs, consumer wrappers, tests, and migration scripts together.
Current hardcoded assumptions found
Project config and path defaults
paths.builddefaults tobuildininternal/project.defaultConfig.HAKManifestPathalways returns<build>/haks.json.ModuleArchivePathalways uses<build>/<module.resref>.mod.HAKArchivePathalways uses<build>/<hak.name>.hak.SourceDir,AssetsDir, andBuildDirdirectly join configured paths with the repo root and do not yet expose a richer path policy.TopDataBuildDirfalls back to<paths.build>/topdatawhentopdata.buildis empty.TopDataUsesRepoCachespecial-casestopdata.build == .cache.TopDataCompiled2DADiralways appends2da.TopDataCompiledTLKPathusessow_tlk.tlkeither underbuildor topdata build output.TopDataWikiRootDiruses.cache/wikifor repo-cache mode and<topdata.build>/wikiotherwise.TopDataWikiStatePathalways usesstate.json.TopDataPackageHAKNamedefaults tosow_top.hak.TopDataPackageTLKNamedefaults tosow_tlk.tlk.
Hardening direction:
- Split these into documented toolkit defaults and repository-overridable fields.
- Keep existing values as defaults only for compatibility.
- Show all derived values in effective config output.
Inventory and extension rules
- Source extensions are hardcoded in
project.SourceExtensions. - Asset extensions are hardcoded in
project.AssetExtensions. Project.Scantreats.jsonand.nssas source inputs.- Source JSON extension detection assumes resource filenames like
<resref>.<ext>.json. - Validator required fields are hardcoded, currently including
ifo -> Mod_Name. - Built-in script prefixes are hardcoded in
internal/validator. - Validator reference checks hardcode specific field labels and expected resource extensions:
Conversation -> .dlgMod_Entry_Area -> .areModel -> .mdlSound -> .wav
Hardening direction:
- Add a
resourcesorinventoryconfig section for extension policy and source/asset classification. - Keep NWN defaults built in, but make them inspectable and overrideable.
- Consider allowing validators to be named profiles, for example
nwn_module, instead of requiring every repository to spell out base NWN rules.
Build and HAK behavior
build-haksreads/writes the hardcoded manifest name fromProject.HAKManifestPath.- HAK files are always emitted under
paths.build. - HAK archive filenames are always derived from
haks[].name. extractscans all*.hakin the build directory rather than only configured/generated HAKs.build-hakshas command flags that mutate behavior independently of config:--hak--archive--source-manifest--plan-only--quiet--verbose--debug
SOW_BUILD_HAKS_KEEP_EXISTINGcontrols HAK cleanup behavior outside YAML.
Hardening direction:
- Add configurable manifest names and cleanup policy.
- Define command-line flags as explicit per-run overrides in the effective config model.
- Log all active environment overrides.
- Consider config-controlled archive discovery for
extract.
Script compiler behavior
- Compiled scripts are cached under
.cache/ncs. - Compiler discovery is hardcoded to:
SOW_NWN_SCRIPT_COMPILERtools/script-compiler/nwn_script_comptools/script-compiler/nwn_script_comp.exetools/nwn_script_comptools/nwn_script_comp.exePATH
- Script source directory is always
<paths.source>/scripts. - Compiler environment probing uses
SOW_NWN_USER_DIRECTORY,NWN_HOME,NWN_USER_DIRECTORY,SOW_NWN_ROOT, andNWN_ROOT.
Hardening direction:
- Add
tools.script_compilerandscript.cacheconfig. - Keep environment variables as explicit override sources.
- Include resolved compiler path and cache path in effective config and debug logs.
Music behavior
- Music conversion/scanning is hardcoded around
envi/music. - Music staging is hardcoded to
.cache/music. - Credits output is hardcoded to
.cache/credits. - Credits overlay filename is hardcoded to
CREDITS.md. - FFmpeg/ffprobe discovery is environment/PATH based.
- BMU output naming rules and 16-character NWN stem limits are hardcoded.
Hardening direction:
- Apply
MUSIC_REFACTOR_CONTRACT.md. - Convert music behavior to dataset configuration.
- Keep old
music.prefixesonly as compatibility shorthand until repositories migrate.
Topdata behavior
- Topdata source layout is partly convention-based:
datatlkbase_dialog.jsonlock.json.tlk_state.jsontemplatesassets
- Topdata compiled output subdirectories are hardcoded:
2dawikiwiki/pageswiki/state.json
- Wiki generation has hardcoded managed namespaces and status labels.
- Wiki deploy defaults are hardcoded:
.wiki_deploy_manifest.jsonAuto-generated from native builder- managed namespaces including
classes,feat,itemtypes,races,skills,spells,meta
- Topdata package fallback names are
sow_top.hakandsow_tlk.tlk. - Autogen released manifest cache path is
.cache/<cache_name>. - Autogen released manifest max age is one hour.
SOW_AUTOGEN_MANIFEST_REFRESHforces refresh outside YAML.- Released autogen manifest discovery derives a Shadows Over Westgate assets repo spec from git remote and environment overrides.
- Supported autogen modes are hardcoded:
parts_rowshead_visualeffects
- Supported derive kinds are hardcoded:
trailing_numeric_suffixmodel_stem
- Supported derive group source is hardcoded:
first_path_segment
Hardening direction:
- Separate topdata toolkit defaults from repository-specific defaults.
- Add explicit config for output subpaths, wiki generation/deploy defaults, autogen cache policy, and release source.
- Keep dataset JSON shape unchanged.
- Treat topdata dataset internals as authored data, not root YAML config, unless the behavior genuinely varies by repository.
Extract behavior
- Extract output layout is hardcoded:
.nss-><paths.source>/scripts/<resref>.nss- GFF resources ->
<paths.source>/<sourceSubdir(ext)>/<resref>.<ext>.json - other resources ->
<paths.assets>/<extension>/<resref>.<extension>
- Stale cleanup uses the current project inventory.
- Ignore behavior is configurable only by extension.
Hardening direction:
- Add extract output layout settings or named extraction profiles.
- Keep current layout as
nwn_canonical_json. - Make cleanup scope explicit and visible.
App and logging behavior
- Command descriptions mention hardcoded paths like
src/,assets/,build/,.cache/2da,.cache/wiki,sow_top.hak, andsow_tlk.tlk. - Verbosity flags are command-specific, especially for
build-haks. - Spinner and phase labels are hardcoded in
internal/app/app.go. - Config load diagnostics are not yet a full effective-config explanation.
Hardening direction:
- Update descriptions to avoid implying fixed repository layouts.
- Introduce shared global verbosity flags or a documented command-level policy.
- Expose effective config and active overrides through
configcommands.
Proposed schema expansions
These are target concepts, not a requirement to implement every field in one pass.
toolkit:
defaults_profile: nwn
generated_artifacts:
format: json
logging:
default_verbosity: normal
paths: relative
paths:
source: src
assets: assets
build: build
cache: .cache
tools: tools
outputs:
module_archive: "{module.resref}.mod"
hak_manifest: haks.json
hak_archive: "{hak.name}.hak"
inventory:
source_extensions:
- .are
- .dlg
- .fac
- .git
- .ifo
- .jrl
- .utc
- .utd
- .ute
- .uti
- .utm
- .utp
- .uts
- .utt
- .utw
asset_extensions:
- .2da
- .bmu
- .dds
- .mdl
- .wav
source_json_pattern: "{resref}.{extension}.json"
validation:
profile: nwn_module
builtin_script_prefixes:
- nw_
- x0_
- x1_
- x2_
- x3_
- ga_
- gc_
- gen_
- gui_
- nwg_
- ta_
scripts:
source_dir: scripts
cache: "{paths.cache}/ncs"
compiler:
path: ""
env:
path: SOW_NWN_SCRIPT_COMPILER
nwn_root: SOW_NWN_ROOT
nwn_user_directory: SOW_NWN_USER_DIRECTORY
search:
- "{paths.tools}/script-compiler/nwn_script_comp"
- "{paths.tools}/script-compiler/nwn_script_comp.exe"
- "{paths.tools}/nwn_script_comp"
- "{paths.tools}/nwn_script_comp.exe"
- PATH:nwn_script_comp
extract:
layout: nwn_canonical_json
ignore_extensions: []
cleanup_stale: true
delete_module_archive_after_success: false
topdata:
source: topdata
build: "{paths.build}/topdata"
compiled_2da_dir: 2da
compiled_tlk: sow_tlk.tlk
package_hak: sow_top.hak
package_tlk: sow_tlk.tlk
wiki:
enabled_by_default: false
output_root: wiki
pages_dir: pages
state_file: state.json
managed_namespaces:
- classes
- feat
- itemtypes
- races
- skills
- spells
deploy_manifest: .wiki_deploy_manifest.json
deploy_edit_summary: Auto-generated from native builder
autogen:
cache:
root: "{paths.cache}"
max_age: 1h
refresh_env: SOW_AUTOGEN_MANIFEST_REFRESH
release_source:
provider: gitea
server_url_env: SOW_ASSETS_SERVER_URL
repo_env: SOW_ASSETS_REPO
Implementation does not need to preserve exactly this naming, but each hardcoded behavior above should either become:
- a field in schema,
- a documented toolkit default visible in effective config, or
- a deliberate non-configurable engine invariant with a clear rationale.
Effective config design
Commands
Add config inspection commands:
sow-toolkit config validate
sow-toolkit config effective [--json|--yaml]
sow-toolkit config inspect [<key>] [--json]
sow-toolkit config explain <key>
sow-toolkit config sources
config validate should load, normalize, and validate without scanning/building.
config effective should print the complete normalized config after defaults and compatibility migrations.
config explain <key> should show:
- final value
- source of the value:
- toolkit default
- root YAML
- compatibility migration
- environment override
- command-line override
- whether the value is deprecated
- validation rules that apply
config sources should print:
- loaded config path and format
- legacy status
- accepted root config candidates
- active environment overrides
- command-line overrides used for the current invocation
Internal model
Introduce a normalized/effective config layer separate from raw YAML structs.
Suggested distinction:
RawConfig: direct YAML representation with pointer fields where inheritance or "unset" matters.EffectiveConfig: fully resolved values consumed by commands.ConfigProvenance: source metadata for inspect/explain commands.ConfigDiagnostics: warnings/errors/deprecations produced during normalization and validation.
Commands should consume EffectiveConfig or methods derived from it, not raw YAML fields.
Override rules
Order of authority:
- toolkit built-in defaults
- YAML root config
- compatibility migrations from deprecated YAML fields
- environment overrides
- command-line overrides
For each override:
- document whether it is allowed
- record provenance
- show it in debug/effective output
- avoid silent behavior changes
Environment variables should not be removed immediately, but they should be treated as visible overrides, not hidden behavior.
Hardening phases
Phase 1: Baseline audit and effective config shell
- Add a config audit document generated from code search.
- Implement
config validate. - Implement
config effective --jsonusing current config and current defaults. - Add provenance for:
- config filename
- legacy JSON use
paths.builddefault- topdata default package names
- HAK manifest default name
- Do not change build behavior in this phase.
Phase 2: Centralize derived paths and names
- Move path/name derivation out of scattered methods and into effective config.
- Cover:
- module archive path
- HAK archive path template
- HAK manifest path
- cache root
- script cache
- topdata compiled dirs
- topdata package names
- wiki output paths
- autogen cache root
- Keep old methods as wrappers around effective config during migration.
- Add tests proving old and new derivations match.
Phase 3: Schema expansion for current hardcoded behavior
- Add schema fields for outputs, cache roots, inventory extensions, script compiler settings, extract layout, topdata output subpaths, wiki deploy defaults, and autogen cache policy.
- Preserve current defaults.
- Add strict validation for new fields.
- Update README examples.
Phase 4: Command integration
- Update build, extract, validate, compare, topdata, autogen, music, and wiki commands to consume effective config.
- Convert environment variables into explicit override records.
- Update logs to report configured names/paths instead of hardcoded labels.
- Add
--debugoutput for effective values used by each command.
Phase 5: Repository migrations
- Update consumer repository YAML to explicitly declare any behavior that is repository-specific.
- Remove compatibility shims only after all active repositories have migrated.
- Keep generated JSON artifacts stable unless a separate contract says otherwise.
- Add regression checks comparing pre/post build plans and generated manifests.
Phase 6: Enforcement
- Add tests or static checks that reject new repository-specific literals in core command paths.
- Remove legacy JSON loading after the agreed migration window.
- Turn deprecated YAML shorthand warnings into validation errors where appropriate.
Repository-by-repository checklist
For each consumer repository:
- Identify root config file and confirm it is
nwn-tool.yamlornwn-tool.yml. - Confirm no root
nwn-tool.jsonremains except as deliberate migration fixture. - List all wrapper scripts and environment variables they set.
- Record local tool paths and whether they need YAML fields.
- List generated artifact paths and confirm they are not treated as config.
- Run current build and save:
- HAK manifest
- module HAK order
- topdata package paths
- autogen manifest paths
- credits inventory paths
- Convert implicit behavior to explicit YAML.
- Run
config effectiveand verify values. - Run build/validate workflows and compare generated outputs.
- Document intentional diffs.
Test strategy expansion
Add tests for:
config validatesucceeds without scanning missing optional trees.config effective --jsonis deterministic.config explain paths.buildidentifies toolkit default when omitted.config explain topdata.package_hakidentifies YAML source when configured.- legacy
nwn-tool.jsonemits warning and provenance. - YAML wins over legacy JSON when both exist.
- unknown YAML fields fail before any scan/build side effects.
- environment overrides appear in effective config.
- command-line overrides appear in effective config for the command invocation.
- HAK manifest name can be configured without changing generated JSON schema.
- topdata package names can be configured and all command output follows them.
- script compiler cache path can be configured.
- extract layout profile preserves current output paths.
- autogen cache root and max age are configurable.
- wiki deploy manifest name is configurable.
- source/asset extension defaults match current scanning behavior.
- invalid path traversal in output/cache config fails early.
- absolute paths are rejected unless the field explicitly allows them.
- duplicate or case-colliding generated output paths fail validation.
- config-generated effective values are sorted deterministically.
Regression tests:
- current minimal YAML still builds to the same default paths.
- current Shadows Over Westgate-style topdata package output remains
sow_top.hak/sow_tlk.tlkunless YAML changes it. - current HAK manifest remains
haks.jsonunless YAML changes it. - current
.cacheoutputs remain stable unless YAML changes them. - old
music.prefixescompatibility still maps to current behavior until removed.
Priority refactor tickets
P0: Authority and safety
- Add effective config command output.
- Record config provenance and legacy warnings.
- Centralize output/cache path derivation.
- Validate path traversal and unsafe output roots.
- Make environment overrides visible.
P1: Repository-specific names and paths
- Configure HAK manifest filename.
- Configure cache root.
- Configure script compiler path/search/cache.
- Configure topdata compiled output subpaths.
- Configure wiki deploy manifest/edit summary/namespaces.
- Configure autogen cache root/max age/release source.
P2: Inventory and validation profiles
- Configure source and asset extension lists.
- Configure or profile validator reference rules.
- Configure built-in script prefixes.
- Configure extract output layout.
P3: Migration cleanup
- Deprecate and remove legacy JSON root config loading.
- Deprecate music prefix shorthand after dataset migration.
- Replace command descriptions that mention hardcoded defaults.
- Add static/lint checks for new repository-specific literals.
Documentation strategy
- Document
nwn-tool.yamlas the canonical root config file. - Maintain a generated effective-config reference in docs.
- Document every toolkit default separately from repository examples.
- For every config field, document:
- type
- default
- whether relative paths are repo-relative or output-root-relative
- whether absolute paths are allowed
- validation rules
- generated artifacts affected
- migration notes
- Keep a clear line between:
- human-authored root YAML config
- human-authored topdata dataset JSON
- generated JSON artifacts
- compatibility/legacy files
Expanded acceptance criteria
sow-toolkit config effective --jsonshows all values that commands use.- Every effective value can be traced to YAML, toolkit default, environment override, compatibility migration, or command override.
- Existing hardcoded defaults are either configurable or documented as toolkit defaults.
- Command implementations do not invent repository-specific paths after config normalization.
- Topdata, HAK, music, extract, autogen, wiki, and script compiler paths are inspectable.
- Legacy JSON root config use is visible and eventually removable.
- Generated JSON schemas remain stable unless intentionally changed.
- Existing consumer repositories build before and after migration.