83 lines
7.8 KiB
Text
83 lines
7.8 KiB
Text
let d = import "adr-defaults.ncl" in
|
|
|
|
d.make_adr {
|
|
id = "adr-026",
|
|
title = "nulib M6 Restructure: 8-Layer ADR-025-Compliant Module Tree",
|
|
status = 'Accepted,
|
|
date = "2026-04-18",
|
|
|
|
context = "ADR-025 mandated empty root mod.nu, selective imports, and bash-direct dispatch but deferred the file layout question: the existing `lib_provisioning/` and `main_provisioning/` directories were flat accretions with no enforced layer contracts. `lib_provisioning/` contained 242 files spanning primitives (string utils, logging), platform concerns (SOPS, KMS, SSH), domain logic (workspace state, orchestrator queries), and CLI command handlers — all in one directory. `main_provisioning/` held per-command Nu scripts with mixed dependency depth. Cross-layer violations were undetectable: a primitive utility could `use` a domain module without any structural signal. The ADR-025 pre-commit hook checked for star-imports but could not enforce dependency direction. The result was a codebase where adding a new utility required navigating 242 flat files and guessing import depth.",
|
|
|
|
decision = "Reorganize provisioning/core/nulib/ into a strict 6-layer tree with one-directional dependency flow: primitives/ → tools/ → platform/ → domain/ → orchestration/ → cli/. Each layer may only import from layers below it; violations are detectable by grep on import paths. The migration uses the strangler-fig pattern: (1) move real implementation to the new tree; (2) leave a transition shim in the original location (`# Transition shim (ADR-026 M6)` as first line); (3) update external callers to the new path. All 242 lib_provisioning/ files are either moved to the new tree or archived to .wrks/core_nulib/shimmed/ when they have zero callers. The shim layer is a pure re-export façade: `export use new/path.nu *`. config/accessor is placed in platform/ (not domain/) because it already depended on platform/target.nu — layer placement follows actual dependency topology, not naming intuition.",
|
|
|
|
rationale = [
|
|
{
|
|
claim = "Flat directories are structurally unenforceable — layer violations are invisible at review time",
|
|
detail = "In a 242-file flat directory, `use lib_provisioning/utils/settings.nu [fn]` and `use lib_provisioning/domain/workspace/state.nu [fn]` look identical to a reviewer. The first is a primitives import; the second crosses a domain boundary. Without directory structure that encodes layer, pre-commit hooks can only check for star-imports, not dependency direction. The 8-layer tree makes violations visible: any `use platform/X` inside `primitives/Y` is a directory-level signal that something is wrong.",
|
|
},
|
|
{
|
|
claim = "Strangler-fig migration preserves working code during the transition",
|
|
detail = "A big-bang migration of 242 files would require updating all callers atomically. With shims, each file moves independently: the shim at the old path keeps callers working until they are individually updated. This decouples the migration from caller updates and allows incremental validation. The shim marker (`# Transition shim (ADR-026 M6)`) enables bulk identification via grep for post-migration cleanup.",
|
|
},
|
|
{
|
|
claim = "config/accessor belongs in platform/, not domain/ — discovered empirically",
|
|
detail = "The original plan placed config/accessor in domain/ because it felt like business-layer logic. During migration, it was found that config/accessor/core.nu already imported platform/target.nu. Placing it in domain/ would have required domain/ → platform/ imports, a layer violation in reverse. Moving it to platform/ eliminated all cross-layer violations from primitives/ and platform/ simultaneously and was the correct structural choice — layer assignment must follow actual dependency topology.",
|
|
},
|
|
{
|
|
claim = "Archiving zero-caller shims to .wrks/ preserves history without polluting the live tree",
|
|
detail = "Of the 242 lib_provisioning/ files, a significant fraction had zero external callers — they were either dead code or superseded by newer implementations. Deleting them would lose their history; keeping them in the live tree would require maintaining shims forever. .wrks/core_nulib/shimmed/ is the designated archive for these files: not in git history, not in the live module tree, but recoverable if a caller is discovered later.",
|
|
},
|
|
],
|
|
|
|
consequences = {
|
|
positive = [
|
|
"Layer violations are detectable by grep on import path prefixes — no AST tooling required",
|
|
"New files have an obvious home: a string utility goes in primitives/, a SOPS wrapper in platform/, a workspace query in domain/",
|
|
"Shim layer enables incremental caller migration without breaking the working tree at any point",
|
|
"242 files reduced to a structured 6-layer tree with clear ownership boundaries",
|
|
],
|
|
negative = [
|
|
"Shims must be explicitly removed once all callers migrate — they are migration debt, not permanent architecture",
|
|
"The .wrks/core_nulib/shimmed/ archive is outside git tracking; files there are recoverable only from the local filesystem",
|
|
"Contributors must learn the 6-layer contract; the ADR-025 pre-commit hook alone does not enforce layer direction",
|
|
],
|
|
},
|
|
|
|
alternatives_considered = [
|
|
{
|
|
option = "Keep lib_provisioning/ flat and enforce layers via naming convention (lib_primitives_, lib_platform_, etc.)",
|
|
why_rejected = "Naming conventions degrade under refactoring pressure. A file renamed from lib_platform_foo to just foo loses the signal. Directory structure is enforced by the filesystem and grep; naming is enforced only by discipline.",
|
|
},
|
|
{
|
|
option = "Big-bang migration: move all 242 files and update all callers atomically",
|
|
why_rejected = "The caller surface spans provisioning/core/nulib/, provisioning/extensions/, provisioning/platform/crates/ (Nushell test scripts), and workspaces/. Updating all callers atomically requires a multi-day coordinated change that cannot be validated incrementally. A single broken caller would fail the entire migration. Strangler-fig allows per-file validation.",
|
|
},
|
|
],
|
|
|
|
constraints = [
|
|
{
|
|
id = "layer-import-direction",
|
|
claim = "Files in primitives/ must not import from tools/, platform/, domain/, orchestration/, or cli/. Files in tools/ must not import from platform/ or above. The rule extends transitively up each layer.",
|
|
scope = "provisioning/core/nulib/",
|
|
severity = 'Hard,
|
|
check = { tag = 'Grep, pattern = "^use (platform|domain|orchestration|cli)/", paths = ["provisioning/core/nulib/primitives/"], must_be_empty = true },
|
|
rationale = "One-directional dependency flow is the architectural guarantee of the 8-layer tree. Without it, the tree is a cosmetic rename of the flat directory.",
|
|
},
|
|
{
|
|
id = "shim-marker-required",
|
|
claim = "Every transition shim in lib_provisioning/ or main_provisioning/ must have `# Transition shim (ADR-026 M6)` as its first line",
|
|
scope = "provisioning/core/nulib/lib_provisioning/, provisioning/core/nulib/main_provisioning/",
|
|
severity = 'Soft,
|
|
check = { tag = 'Manual, description = "grep -rL 'Transition shim' provisioning/core/nulib/lib_provisioning/ — must list only empty mod.nu files" },
|
|
rationale = "The marker enables bulk identification of shims for post-migration cleanup. Without it, shims are indistinguishable from real implementations by file content alone.",
|
|
},
|
|
],
|
|
|
|
ontology_check = {
|
|
decision_string = "Reorganize provisioning/core/nulib/ into 8-layer tree (primitives/tools/platform/domain/orchestration/cli/) with strangler-fig migration and shim layer at lib_provisioning/",
|
|
invariants_at_risk = ["config-driven-always"],
|
|
verdict = 'Safe,
|
|
},
|
|
|
|
related_adrs = ["adr-025-unified-lazy-loading"],
|
|
}
|