feat(foundation): give the workspace root and a study's directory distinct types - #49
Merged
Merged
Conversation
…tinct types
Phase 1 of bd 6ns.1 (name the roots so the wrong one cannot be picked
silently). WorkspaceRoot and ProjectDir are NewTypes over Path in
gridalyn/foundation/platform/roots.py: identity at run time, distinct to mypy.
Why: bd 7rt passed a study's directory where the workspace root was required.
A flagship stage died after writing 21 MB under a doubled
projects/<study>/projects/<study>/. The fix that followed passed
script.base_dir instead, and base_dir equals the workspace root only for the
2 of 9 studies with pathBase: repo.
- Producers: find_workspace_root and find_repo_root return WorkspaceRoot.
StudyProject.root and ProjectScript.root are ProjectDir. The new
ProjectScript.workspace_root is discovered from root, so it does not depend
on pathBase. base_dir stays a Path, documented as the stage cwd.
- Consumers: ArtifactLayout/GridalynWorkspace and the workspace_from_* and
layout_from_environment helpers take a WorkspaceRoot, and so do
operations/artifacts.py (materialize_flexibility_operation_artifacts and its
helpers), build_project_catalog, build_dashboard_catalog,
build_digital_twin_reports, run_digital_twin_build and
generate_locational_clearing. prepare_project_workspace takes a ProjectDir.
- The flagship's analyze_locational_contracts stage passes
script.workspace_root, off the pathBase coincidence.
- gridalyn.foundation and gridalyn.foundation.platform re-export the workspace
names and both types under TYPE_CHECKING. Their untyped lazy __getattr__ made
those names Any to mypy, so 27 of 34 imports were never checked. Run time
stays lazy, and the lazy maps gain ProjectDir and WorkspaceRoot so the
runtime import matches.
- Call sites the typed re-export surfaced (23, all "Path where WorkspaceRoot is
expected"): 14 module constants that are the repo root by construction
(parents[4]), 4 Path(".") defaults, 3 parameters whose docstrings already
named the workspace root, and the twin CLI's _require_workspace_root, which
now returns a WorkspaceRoot. In the twin, only twin/adapters/network.py
changes: its one ArtifactLayout(root) wraps WorkspaceRoot(root), with a
comment, and the exporters' Path signatures wait for the twin-owned phase
(agreed with the twin's owner).
- tests/test_workspace_roots.py pins the values per pathBase, and runs mypy on
two snippets: root=script.root where a WorkspaceRoot is required is rejected,
and root=script.workspace_root is accepted.
- Docs: docstrings on StudyProject, ProjectScript.root/workspace_root/base_dir
and ArtifactLayout. Prose in docs/components/foundation.md and projects.md,
with no fenced block added; ledger lines re-derived through the tool.
Not in this phase: the twin's own base_dir (canonical base directory) gets a
separate NewType in a twin-owned phase; the remaining root-taking functions
come layer by layer.
Verified: simulated first in throwaway worktrees with mypy A/B per (file,
message); a positive control (the flagship's root=script.base_dir surfacing)
confirmed mypy read the patched tree. On this tree: 0 new mypy errors in
gridalyn, projects and gridalyn/twin, with 4 pre-existing errors gone; full
suite 2223 passed / 0 failed; every touched module imports at run time;
pinned black/isort/flake8 clean; doc instruction ledger OK.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… address review
The typed foundation re-export removed 4 pre-existing `Unsupported left operand
type for / ("str")` errors in workspace.py.
- .mypy-baseline 120 -> 116: CI's authoritative count on 4ceee0e, and the
count the local environment measures too.
- .mypy-baseline-projects 850 -> 846: the local count the pre-push hook sees.
CI reports 758 there. The gap between the two environments predates this
change, and a baseline below the local count would block every push.
- docs/contributing/verification.md now cites 116 and 846. It said 120, and
851, which was already stale.
Review:
- ProjectScript.workspace_root is a cached_property. Discovery can spawn
`git rev-parse`, so a loop over the property paid a subprocess on every read.
- The mypy test passes --follow-imports=silent, so each snippet keeps the
imported types and reports only its own errors.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nilsonfh
force-pushed
the
fix/name-the-roots
branch
from
September 15, 2026 19:01
8927c52 to
6ce1b5a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of bd 6ns.1: the workspace root and a study's directory get distinct types, so mypy rejects passing one where the other is required.
WorkspaceRootandProjectDirareNewTypes overPathingridalyn/foundation/platform/roots.py. They are identity at run time, so no behaviour changes; the only effect is what mypy accepts.Why. bd 7rt passed a study's directory where
materialize_flexibility_operation_artifactsneeded the workspace root. A flagship stage died after writing 21 MB underprojects/<study>/projects/<study>/. The fix that followed passedscript.base_dir, which equals the workspace root only for the 2 of 9 studies withpathBase: repo(measured across all 9). This PR makes that call a type error and moves the flagship toscript.workspace_root.What changes
Producers:
find_workspace_root/find_repo_rootreturnWorkspaceRoot.StudyProject.rootandProjectScript.rootareProjectDir.ProjectScript.workspace_root, discovered fromroot, so it does not depend onpathBase.base_dirstaysPath, documented as the stage cwd.Consumers that take the workspace root:
ArtifactLayout,GridalynWorkspace,workspace_from_*,layout_from_environment;operations/artifacts.py;build_project_catalog,build_dashboard_catalog;build_digital_twin_reports,run_digital_twin_build,generate_locational_clearing.prepare_project_workspacetakes aProjectDir.Typed re-export.
gridalyn.foundationresolved these names through an untyped lazy__getattr__, so to mypy they wereAny, and 27 of the 34 imports were never checked. Both foundation__init__s now re-export them underTYPE_CHECKING. Run time stays lazy, and the lazy maps also gain the two types, sofrom gridalyn.foundation import WorkspaceRootworks at run time too.The 23 call sites that surfaced:
Path(".")defaults;--root;twin/adapters/network.py'sArtifactLayout(root)becomesArtifactLayout(WorkspaceRoot(root)), with a comment. The publicexport(root)signatures stayPathuntil the twin-owned phase.Tests:
tests/test_workspace_roots.pypins the values under bothpathBaseconventions. It also runs mypy on two snippets:root=script.rootmust be rejected, androot=script.workspace_rootmust be accepted. That protects the contract if someone later makes the type a plain alias.Docs: docstrings, plus prose in
docs/components/{foundation,projects}.md. No fenced block was added; ledger lines were re-derived through the tool. The fourdocs/components/projects.mdentries were already 7 lines stale on main (ledger 52/100/106/112, actual 59/107/113/119); with this PR's one added line they are re-derived to 60/108/114/120.foundation.md#2moves 130 → 140 from the new paragraph.Not in this phase
base_dir(its canonical base-artifact directory) is a different concept fromStudyProject.base_dir. It gets its ownNewTypein a twin-owned phase.gridalyn/) follow layer by layer.Verification
After rebasing onto
2fbb8315(#48)#48 merged while this was in review, so this branch was rebased. It is now
70c98fb1(phase 1) +6ce1b5af(baselines and review).gridalyn/projects/scripting.py, and git merged it cleanly. feat(projects): let a declared extension contribute a semantic capability #48 addsresolve_extensionsat the end ofProjectScript, away fromroot,workspace_rootandbase_dir.git merge-treereported no conflicts.ArtifactLayout,layout_from_environment,workspace_from_*,find_workspace_root,prepare_project_workspace, the catalog and operations helpers,.base_dir,script.root), and changes no ledger entry.6ce1b5af): success on attempt 1. test, lint, docs and Governed project contracts all pass. Its mypy counts are gridalyn 116/116, twin 12/12 and projects 758 (baseline 846; the gap between the local and CI environments predates this PR).Before the rebase (on
872dcd47)root=script.base_dirshowed up as a new error.Unsupported left operand type for / ("str")inworkspace.py).🤖 Generated with Claude Code