Skip to content

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.

LocationRole today
scripts/guard-ts-lib-checks.tsHouse 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.tsShared repoRoot / workingTreeFiles helpers every repo-level guard discovers its corpus through.
scripts/install-desktop-apps.tsPrecedent for shelling to moon through the command seam (runChecked(spawnRunner("moon"), ["query", "projects"], { cwd }), around line 281).
packages/ts/detect-runnersPublish-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.ymlRuns 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.ymlDeclare 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.jsonEach 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.

scripts/guard-project-shape.ts exists, is registered in the three guard homes, and asserts two things over every project moon query projects reports:

  1. Every glob-registered project declares its category’s metadata. A project whose language/layer/stack all read "unknown" (no moon.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.
  2. No lint or format:check script mutates the tree. A project whose package.json lint or format:check script contains --fix or --write is 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.

  1. Evaluate packages/ts/detect-runners as the discovery substrate before hand-rolling YAML parsing. If its moon provider’s Task model exposes enough (declared task names, and language/layer/stack off the project record) it can back the guard directly; if not — or if its exec: true shell-out to moon query tasks is unacceptable for a pre-commit-hook-adjacent guard — fall back to runChecked(spawnRunner("moon"), ["query", "projects"], { cwd }) per the install-desktop-apps.ts:281 precedent, parsing the JSON projects[].layer/.stack/.language/.tasks fields directly rather than hand-scanning moon.yml YAML.
  2. Write scripts/guard-project-shape.ts following scripts/guard-ts-lib-checks.ts’s shape exactly: discover the corpus via scripts/_git.ts’s repoRoot/workingTreeFiles (never process.cwd()), inspect every project moon reports, flag (a) any project whose language/layer/stack are all "unknown", and (b) any project whose package.json lint or format:check script contains --fix or --write. Success line carries the inspected count; each violation prints ✗ <path>; exit 0 clean, 1 on violations; guarded by if (import.meta.main).
  3. Write scripts/guard-project-shape.test.ts colocated, mirroring guard-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.
  4. Bring all eight non-conforming projects into compliance. The six with no moon.yml get one; augmented_web and vimit_prototype gain the missing layer/stack on the file they already have. Each declares real language/layer/stack and letting the matching aspect (ts-lib.yml for the TypeScript library projects, the Rust aspect for determined-sqlite and vault-transform) supply its tasks, per the target vocabulary in T-VAF5-a-standard-for-the-baseline-task-vocabulary.
  5. Register the guard in the three homes: add bun scripts/guard-project-shape.ts to the justfile’s ci-guards recipe; add a matching inline step to .github/workflows/ci.yml’s guard block; add a guard-project-shape task to the root moon.yml with toolchains: 'system' (without it moon routes the task through its own bun toolchain and hard-errors on markdown-contract’s file: dependency, exactly as the neighboring guard-solution-boundary task’s comment records).
  6. Run just ci-guards end to end to confirm the new guard passes alongside the existing ones.
LocationKindChange
scripts/guard-project-shape.tsnewThe guard: glob-registered-metadata check + mutating-gate check.
scripts/guard-project-shape.test.tsnewColocated test; outside-a-repository-throws is the load-bearing case.
packages/ts/prettier-config/moon.ymlnewDeclares this project’s metadata so an aspect reaches it.
solutions/determined/packages/platform/moon.ymlnewDeclares this project’s metadata so an aspect reaches it.
solutions/determined/packages/shell/moon.ymlnewDeclares this project’s metadata so an aspect reaches it.
solutions/augmented/packages/ts/test-vue-component/moon.ymlnewDeclares this project’s metadata so an aspect reaches it.
solutions/determined/crates/determined-sqlite/moon.ymlnewDeclares this crate’s Rust metadata so the Rust aspect reaches it.
solutions/determined/crates/vault-transform/moon.ymlnewDeclares this crate’s Rust metadata so the Rust aspect reaches it.
justfilemodifyAdd bun scripts/guard-project-shape.ts to the ci-guards recipe.
.github/workflows/ci.ymlmodifyAdd a “Guard — project shape conformance” step alongside the other guard steps.
moon.ymlmodifyAdd a guard-project-shape task with toolchains: 'system', matching the neighboring guard-* tasks.
  • AC-1: bun scripts/guard-project-shape.ts exits 0 and its success line reports a non-zero inspected count.
  • AC-2: bun test scripts/guard-project-shape.test.ts passes, including a case that asserts the guard throws when run outside a git repository.
  • AC-3: moon query projects reports zero projects with layer: "unknown" and stack: "unknown" together.
  • AC-4: grep -rE '"(lint|format:check)":.*(--fix|--write)"' --include=package.json . (excluding node_modules) returns no matches.
  • AC-5: just ci-guards exits 0.
  • AC-6: grep -n 'guard-project-shape' justfile .github/workflows/ci.yml moon.yml returns a match in each of the three files.
  • 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 projects while writing this spec, not carried over unchecked from the originating survey.

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


← Back to Tasks