T-0G2O-a-guard-for-project-shape-conformance
State: closed/done · Impact: high · Complexity: medium
Nothing today asserts that a moon project actually declares the baseline task
vocabulary its category owes, or that a project registered by a workspace glob
carries any metadata at all. .moon/workspace.yml already records this exact
failure mode for Rust: a crate lands, nobody adds its metadata, and it
silently has no fmt-check, no clippy, no test target while moon run :check :test reports green. This task adds a mechanical guard for that — modeled on
scripts/guard-ts-lib-checks.ts — and brings the current non-conforming
corpus into compliance in the same PR, per
P-0011-avoid-deferral-for-simple-tasks.
| Location | Role today |
|---|---|
scripts/guard-ts-lib-checks.ts | House pattern this guard follows: _git.ts-backed discovery, a count-carrying success line, ✗ <path> per violation, exit 0/1, guarded by if (import.meta.main). |
scripts/_git.ts | Shared repoRoot / workingTreeFiles helpers every repo-level guard discovers its corpus through. |
scripts/install-desktop-apps.ts | Precedent for shelling to moon through the command seam (runChecked(spawnRunner("moon"), ["query", "projects"], { cwd }), around line 281). |
packages/ts/detect-runners | Publish-channel package normalizing moon/just/make/mise/cargo/node-script tasks into one Task model; a reuse candidate for this guard instead of hand-scanning moon.yml. Its moon provider is kind: "exec" and shells to moon query tasks only when the caller opts in. |
justfile (ci-guards recipe) | Runs every repo-level guard script; the new guard is not among them. |
.github/workflows/ci.yml | Runs each guard as an inline step in its guard block; the new guard has no step. |
moon.yml (root) | Declares each repo-level guard as a root:guard-* task with toolchains: 'system'; the new guard has no task. |
packages/ts/prettier-config/, solutions/determined/packages/platform/, solutions/determined/packages/shell/, solutions/augmented/packages/ts/test-vue-component/, solutions/determined/crates/determined-sqlite/, solutions/determined/crates/vault-transform/ | Six projects registered by a .moon/workspace.yml glob (packages/ts/*, solutions/determined/crates/*, solutions/augmented/packages/ts/*) with no moon.yml. moon query projects reports each with layer: "unknown", stack: "unknown" and zero tasks, so no aspect matches them and no gate reaches them. |
solutions/augmented/apps/augmented_web/moon.yml, solutions/augmented/apps/vimit_prototype/moon.yml | Declare a moon.yml and hand-roll tasks, but set no layer and no stack — they use a bespoke tags: list instead. moon query projects reports layer: "unknown", stack: "unknown", so like the six above they match no aspect. A guard keyed on “has a moon.yml” would miss these; the invariant is the metadata, not the file. |
solutions/augmented/apps/learn_web/package.json, solutions/augmented/apps/vimit_prototype/package.json | Each declares "lint": "eslint . --fix" (vimit_prototype: "nuxt prepare && eslint . --fix") — a lint script that mutates the tree. T-9FH9-the-script-vocabulary-lands-across-solutions moves the --fix form to lint:fix; nothing today prevents a mutating lint/format:check from recurring. |
Proposed
Section titled “Proposed”scripts/guard-project-shape.ts exists, is registered in the three guard
homes, and asserts two things over every project moon query projects
reports:
- Every glob-registered project declares its category’s metadata. A
project whose
language/layer/stackall read"unknown"(nomoon.yml, or one with no metadata) is a violation — the same failure mode.moon/workspace.yml’s own comment describes for Rust, generalized to every glob-discovered tier. - No
lintorformat:checkscript mutates the tree. A project whosepackage.jsonlintorformat:checkscript contains--fixor--writeis a violation, regardless of category.
The eight projects listed in ## Today — six with no moon.yml at all, two
declaring one without layer/stack — each end up declaring real
language/layer/stack metadata, so they read layer: "unknown" no
longer and the appropriate aspect (ts-lib.yml, the Rust aspect, or the
Nuxt-product-adjacent aspect) reaches them. guard-project-shape then reports
zero violations and a non-zero inspected count.
Approach
Section titled “Approach”- Evaluate
packages/ts/detect-runnersas the discovery substrate before hand-rolling YAML parsing. If its moon provider’sTaskmodel exposes enough (declared task names, andlanguage/layer/stackoff the project record) it can back the guard directly; if not — or if itsexec: trueshell-out tomoon query tasksis unacceptable for a pre-commit-hook-adjacent guard — fall back torunChecked(spawnRunner("moon"), ["query", "projects"], { cwd })per theinstall-desktop-apps.ts:281precedent, parsing the JSONprojects[].layer/.stack/.language/.tasksfields directly rather than hand-scanningmoon.ymlYAML. - Write
scripts/guard-project-shape.tsfollowingscripts/guard-ts-lib-checks.ts’s shape exactly: discover the corpus viascripts/_git.ts’srepoRoot/workingTreeFiles(neverprocess.cwd()), inspect every project moon reports, flag (a) any project whoselanguage/layer/stackare all"unknown", and (b) any project whosepackage.jsonlintorformat:checkscript contains--fixor--write. Success line carries the inspected count; each violation prints✗ <path>; exit 0 clean, 1 on violations; guarded byif (import.meta.main). - Write
scripts/guard-project-shape.test.tscolocated, mirroringguard-ts-lib-checks.test.ts’s fixture shape. The load-bearing assertion is the negative one: run outside a git repository, the guard throws — it never returns an empty, vacuously clean corpus. - Bring all eight non-conforming projects into compliance. The six with
no
moon.ymlget one;augmented_webandvimit_prototypegain the missinglayer/stackon the file they already have. Each declares reallanguage/layer/stackand letting the matching aspect (ts-lib.ymlfor the TypeScript library projects, the Rust aspect fordetermined-sqliteandvault-transform) supply its tasks, per the target vocabulary in T-VAF5-a-standard-for-the-baseline-task-vocabulary. - Register the guard in the three homes: add
bun scripts/guard-project-shape.tsto thejustfile’sci-guardsrecipe; add a matching inline step to.github/workflows/ci.yml’s guard block; add aguard-project-shapetask to the rootmoon.ymlwithtoolchains: 'system'(without it moon routes the task through its own bun toolchain and hard-errors onmarkdown-contract’sfile:dependency, exactly as the neighboringguard-solution-boundarytask’s comment records). - Run
just ci-guardsend to end to confirm the new guard passes alongside the existing ones.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
scripts/guard-project-shape.ts | new | The guard: glob-registered-metadata check + mutating-gate check. |
scripts/guard-project-shape.test.ts | new | Colocated test; outside-a-repository-throws is the load-bearing case. |
packages/ts/prettier-config/moon.yml | new | Declares this project’s metadata so an aspect reaches it. |
solutions/determined/packages/platform/moon.yml | new | Declares this project’s metadata so an aspect reaches it. |
solutions/determined/packages/shell/moon.yml | new | Declares this project’s metadata so an aspect reaches it. |
solutions/augmented/packages/ts/test-vue-component/moon.yml | new | Declares this project’s metadata so an aspect reaches it. |
solutions/determined/crates/determined-sqlite/moon.yml | new | Declares this crate’s Rust metadata so the Rust aspect reaches it. |
solutions/determined/crates/vault-transform/moon.yml | new | Declares this crate’s Rust metadata so the Rust aspect reaches it. |
justfile | modify | Add bun scripts/guard-project-shape.ts to the ci-guards recipe. |
.github/workflows/ci.yml | modify | Add a “Guard — project shape conformance” step alongside the other guard steps. |
moon.yml | modify | Add a guard-project-shape task with toolchains: 'system', matching the neighboring guard-* tasks. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun scripts/guard-project-shape.tsexits 0 and its success line reports a non-zero inspected count. - AC-2:
bun test scripts/guard-project-shape.test.tspasses, including a case that asserts the guard throws when run outside a git repository. - AC-3:
moon query projectsreports zero projects withlayer: "unknown"andstack: "unknown"together. - AC-4:
grep -rE '"(lint|format:check)":.*(--fix|--write)"' --include=package.json .(excludingnode_modules) returns no matches. - AC-5:
just ci-guardsexits 0. - AC-6:
grep -n 'guard-project-shape' justfile .github/workflows/ci.yml moon.ymlreturns a match in each of the three files.
Out of scope
Section titled “Out of scope”- Collapsing the 46 Rust per-crate
moon.ymloverrides via$project— that is T-7VPK-the-rust-aspect-collapses-its-per-crate-overrides. - Landing the full baseline vocabulary (
build/test/typecheck/format/lint) on every already-registered project — that is T-076A-the-script-vocabulary-lands-in-packages-apps-and-sites and T-9FH9-the-script-vocabulary-lands-across-solutions. This task only makes the eight glob-registered-but-unshaped projects visible to their aspect; it does not audit the vocabulary of every already-shaped project. - Moving
learn_web’s andvimit_prototype’slintscript off--fix— that edit belongs to T-9FH9-the-script-vocabulary-lands-across-solutions. AC-4 only asserts that after that task lands, no mutating gate has recurred.
Dependencies
Section titled “Dependencies”- T-0DSA-the-typescript-aspect-delegates-and-spans-both-stacks, T-7VPK-the-rust-aspect-collapses-its-per-crate-overrides and T-PJ8G-the-tool-and-product-tiers-get-their-gates land the aspect shapes this guard’s target state assumes each category delegates to.
- T-VAF5-a-standard-for-the-baseline-task-vocabulary defines the which-verb-which-project rule this guard mechanizes.
Discovery context
Section titled “Discovery context”- Task 11 of the task-vocabulary milestone plan under M-3MGB. The corpus count (glob-registered
projects with no metadata) was verified directly against
moon query projectswhile writing this spec, not carried over unchecked from the originating survey.
Depends on
Section titled “Depends on”T-VAF5-a-standard-for-the-baseline-task-vocabulary, T-076A-the-script-vocabulary-lands-in-packages-apps-and-sites, T-9FH9-the-script-vocabulary-lands-across-solutions