diff --git a/.STATUS b/.STATUS index 3c7d80d02..327fdbf30 100644 --- a/.STATUS +++ b/.STATUS @@ -1,9 +1,57 @@ status: Active -version: 4.5.0 +version: 4.6.0 progress: 100 -release_date: 2026-07-27 -milestone: **v4.4.2 SHIPPED 2026-07-27** — PR #318 (dev→main merge, tag v4.4.2, PATCH): the one fix merged to dev since v4.4.1 (PR #317, closes #316). `preflight-check` skill made reachable by its declared identity — it lived at the pre-rename `skills/check/` location while declaring `name: preflight-check`, so directory-based skill discovery couldn't find it; moved to `skills/preflight-check/`, `/craft:check`'s public argument contract unchanged. New recursive fixture-backed identity tests inventory every command/skill directory-vs-frontmatter mismatch and command/skill name collision plugin-wide, rejecting new drift; 8 pre-existing mismatches + 3 intentional shim collisions ledgered as owned debt through v5.0.0. Test automation now targets stable Python 3.14 (`.python-version` pin, 5 GitHub Actions workflows aligned, drift-guard policy test) — the local-interpreter assertion in that test was found too brittle on review (hard-failed for any contributor not on 3.14) and fixed to skip instead before merge. `docs/NEWS.md`/`docs/index.md`'s own v4.4.2 release-note prose initially contained the literal `skills/check/` string, which the new #317 regression guard (`test_no_live_legacy_check_skill_references`) correctly caught as a repo-wide scan, not scoped to code — rephrased without the literal path, second commit. Full suite 2627 passed/0 failed/52 skipped/1 xfailed/1 xpassed; `pre-release-check.sh 4.4.2` clean (10/10); docs-staleness GREEN; live site + Homebrew formula (fresh sha256) + aggregator sync + Craft MCP Release all confirmed v4.4.2; post-release sweep ALL CLEAN. Release: https://github.com/Data-Wise/craft/releases/tag/v4.4.2 · PR: https://github.com/Data-Wise/craft/pull/318, https://github.com/Data-Wise/craft/pull/317. Prior: **v4.4.1 SHIPPED 2026-07-26** — PR #315 (dev→main merge, tag v4.4.1, PATCH): post-4.4.0 fixes only, no features. `scripts/branch-guard.sh`'s `2>&1`/`1>&2` false-positive (a command like `git commit -m "...(7 -> 9)" 2>&1` misdetected as file creation) was fixed live on the installed hook earlier this session but never ported to the repo's canonical copy, leaving `test_repo_copy_matches_installed` red and the bug reintroducible on reinstall — ported with 2 regression tests (implements `GRILL-rtk-hook-safety-close-out-2026-07-26.md` decision #3). `docs/help/refine-flag.md` and `docs/reference/COMMAND-PARAMETERS.md` fixed for PR #313's 9-command declarer set (was still 7, missing `/craft:plan`/`/craft:smart-help`) — new dogfood test guards against recurrence. `docs/index.md`'s "Latest" callout and `docs/NEWS.md` synced (both still described v4.3.0's content under a v4.4.0 label; NEWS.md had no v4.4.0 entry at all). Also triaged 7 remaining open issues (#228, #213, #199, #170, #169, #133, #108): closed #108/#213/#133, re-scope comments on #228/#170/#169, confirmed #199 valid. Full suite 2654 passed/0 failed/16 skipped/1 xfailed/1 xpassed; `pre-release-check.sh 4.4.1` clean; docs-staleness GREEN; live site + Homebrew formula confirmed v4.4.1; post-release sweep ALL CLEAN. Release: https://github.com/Data-Wise/craft/releases/tag/v4.4.1 · PR: https://github.com/Data-Wise/craft/pull/315. Prior: **v4.4.0 SHIPPED 2026-07-26** — PR #314 (dev→main merge, tag v4.4.0, MINOR): the one `feat:` commit since v4.3.1. `smart-help` declares `--refine` (D7, PR #313) — opt-in, default-OFF, closing the last dangling item on #268's prompt-refiner umbrella. `prompt-refiner`'s Default Policy table made exhaustive and re-keyed on command file path — fixed 3 stale display names, added 2 live callers it was silently missing (`commands/plan.md`, `commands/arch/plan.md`); a new dogfood test (`test_refine_default_policy_table_exhaustive`) guards against future drift. #268's other two proposed items (named destinations, `--scope global`) and #266 (multi-round grill mode) were grilled then killed on adversarial review before implementation — see `docs/specs/GRILL-prompt-refiner-remaining-2026-07-26.md`'s amendment; #266 closed pointing at savant's `edit-loop` (shipped 2026-07-13), which already solves the observed pattern with tooling craft structurally lacks. Full suite 2618 passed/0 failed/49 skipped/1 xfailed/1 xpassed; `pre-release-check.sh 4.4.0` clean (10/10); docs-staleness GREEN; live site + Homebrew formula + aggregator sync all confirmed v4.4.0. Release: https://github.com/Data-Wise/craft/releases/tag/v4.4.0 · PR: https://github.com/Data-Wise/craft/pull/314, https://github.com/Data-Wise/craft/pull/313. Prior: **v4.3.1 SHIPPED 2026-07-25** — PR #312 (dev→main merge, tag v4.3.1, PATCH): two bug fixes merged to dev since v4.3.0, both traced to the same session. `CRAFT_GUARD_ALLOW_DEV_EDIT` escape hatch (#281) surfaced in the guard-bypass-marker confirms themselves (#309, PR #310) — the documented "Claude writes `.claude/allow-once`" one-shot flow was circular for the one case where the approved action IS creating/editing that marker; added the env-var hint to the three marker-specific confirms, reconciled both branch-guard reference docs. Bare-`vX.Y.Z` version-grep bug class swept repo-wide (PR #311) — the same false positive that broke the Homebrew release gate twice (v4.2.0, v4.3.0; fixed for `docs/index.md` in #308) also affected `docs.yml`'s live-site poll, `verify-surfaces.sh`'s `resolve_docs_site`, `version-sync-precommit.sh`'s blocking `.STATUS` pre-commit check, and manual release-verification docs — all re-anchored to the actual version signal instead of the first bare match, each with a regression test verified via planted-defect positive control. Full suite 2650 passed/0 failed/16 skipped/1 xfailed (tracked)/1 xpassed (known perf flake); `pre-release-check.sh 4.3.1` clean (10/10); docs-staleness GREEN. Release: https://github.com/Data-Wise/craft/releases/tag/v4.3.1 · PR: https://github.com/Data-Wise/craft/pull/312, https://github.com/Data-Wise/craft/pull/310, https://github.com/Data-Wise/craft/pull/311. Prior: **v4.3.0 SHIPPED 2026-07-25** — PR #307 (dev→main merge, tag v4.3.0): `CRAFT_GUARD_ALLOW_DEV_EDIT` escape hatch (#281, PR #305) closing the guard-bypass-marker creation deadlock, reusing the #168 env-var pre-authorization pattern; new advisory-only `reference-scope-guard.sh` hook (#286, PR #306) warning on non-conforming filenames under `~/.claude/reference/`, sharing `install-guards.sh`/`guards.json` with `branch-guard`/`no-switch-guard`. Full suite 2648 passed/0 failed. **Homebrew Release recurrence:** the exact same `pre-release-check.sh` false positive that broke v4.2.0's automated tap push (worked around manually then) broke v4.3.0's too — `docs/index.md`'s version check grepped the first bare `vX.Y.Z` string in the file, landing on a historical "since v4.0.0" migration note instead of the actual version badge. Fixed at the source this time (PR #308, fast-tracked dev→main): matched the badge pattern specifically (`version-X.Y.Z`), same as the adjacent README.md check. Re-dispatched `homebrew-release.yml` via `workflow_dispatch --ref main -f version=4.3.0` (the original `release`-event run had checked out the tag commit before the fix existed, so a plain rerun would have failed identically) — all 3 jobs green, tap formula confirmed live (`v4.3.0.tar.gz`, fresh sha256). Craft MCP Release + Aggregator Sync both green from the original release event. Release: https://github.com/Data-Wise/craft/releases/tag/v4.3.0 · PR: https://github.com/Data-Wise/craft/pull/307, https://github.com/Data-Wise/craft/pull/308. Prior: **v4.2.0 SHIPPED 2026-07-19** — PR #296 (dev→main merge, tag v4.2.0): **BREAKING:** `/craft:done` renamed to `/craft:finish` (ADR-006) — matches the existing zsh `finish` alias, no back-compat shim, command count unchanged (48, a rename not an addition); `/craft:restore` spec/report status corrected DRAFT→SHIPPED (had already shipped in v4.1.0 when written). Full suite 2649 passed/1 failed (a genuine broken-link regression from the rename sweep, `docs/commands/brief.md` still linked pre-rename `done.md` — caught by the test run itself, fixed same-commit)/10 skipped. Also merged to dev this cycle (not yet in a tagged release): PR #297 (`utils/docs_detector.py`'s `_extract_command_name()` truncated multi-hyphen command names past a 500-char read window, bumped to 2000), PR #298 (extends `/craft:finish` Step 1.10.5 with a `~/.claude/projects//memory/`+`.remember/` staleness check, 14-day threshold, report-only), and a `docs/reports/` folder established as the `/savant:report` output convention (moved `REPORT-craft-restore-2026-07-17.md` there, added to `scripts/config/exclusions.txt` alongside `docs/specs/`). **Homebrew:** v4.2.0's automated tap-formula push failed — `homebrew-tap`'s `main` gained branch protection (PR + 2 required checks) since `homebrew-release.yml` was last touched, so its direct `git push origin main` was rejected (GH006); recovered manually via `homebrew-tap`#164, which surfaced 2 more CI-gate bugs fixed directly in that repo (#165: `check-revision-bump.sh` false-positived on URL-tag-versioned formulas like craft's, no explicit `version` field to read; #170: the ".STATUS conflict markers" required check was path-filtered to only `.STATUS` changes, so it could never fire — and the PR stayed BLOCKED forever — on formula-only PRs). craft's own workflow fixed to match (PR #299: push a bot branch + open/auto-merge a PR instead of pushing directly), landing independently from a background-dispatched session whose worktree had gone stale after only completing 1 of the 2 related craft/homebrew-tap fixes — recovered by inspecting the worktree directly rather than trusting the dispatch's completion state. Release: https://github.com/Data-Wise/craft/releases/tag/v4.2.0 · PR: https://github.com/Data-Wise/craft/pull/296. Prior: **v4.1.0 SHIPPED 2026-07-17** — PR #295 (dev→main merge, tag v4.1.0): `/craft:restore` (NEW root command, combines `dev/git` skill's mode-aware git-activity recap with `adhd-workflow`'s `.STATUS`-based session recap into one read-only entry point; replaces the deleted `git-recap`/`recap` commands; no `--sync`; a cross-plugin `/restore` dispatcher to `savant:restore` was investigated and explicitly descoped — router can't dispatch cross-plugin) + `/craft:refine` confirm-flow fix (a 2026-07-01 design — fenced refined-prompt block + 4-way confirm: Execute now/Copy for elsewhere/Edit first/Skip — was locked but never implemented; now shipped across all 5 `--refine` callers + standalone, closing the "asks for acceptance without printing first" bug) + full post-v4.0.0 doc-staleness cleanup (47→48 count-drift sweep across ~35 files, mermaid health score 82.3→100 on new diagrams, `docs-staleness-check.sh` RED→GREEN). New architecture docs: `docs/architecture/craft-restore-pipeline.md`, `docs/architecture/prompt-refiner-pipeline.md`. Homebrew formula PR data-wise/homebrew-tap#153 open (tested locally: `brew style`/`brew audit --strict`/`brew install` all clean, 48 commands). Full suite 2650 passed/0 failed. Release: https://github.com/Data-Wise/craft/releases/tag/v4.1.0 · PR: https://github.com/Data-Wise/craft/pull/295. Prior: **v4.0.0 SHIPPED 2026-07-16** — PR #294 (dev→main merge-commit `678e4d4a`, tag v4.0.0): craft docs-authoring surface extracted to the standalone `folio` plugin (folio v1.0.0 released FIRST, per the rollback runbook's ordering). Counts 94→47 commands / 45→40 skills / 8→2 agents. Homebrew formula regenerated with real values (sha256, command_count=47) via homebrew-release.yml (3/3 jobs green); `brew audit`/`brew upgrade` verified clean for both craft and folio formulas. Post-release docs sweep (same session): commands/hub.md + its docs/ mirror had drifted badly (stale example counts, 2 stray text artifacts, a main-menu ASCII template whose header counts didn't match listed items and referenced commands moved to folio or folded into skills); rewrote to match the real 47-command roster. API-REFERENCE-COMMANDS.md (1850 lines, last synced v1.24.0) converted to a thin index into docs/commands/*.md + docs/REFCARD.md rather than re-syncing a third documentation surface indefinitely; DEPENDENCY-ARCHITECTURE.md left untouched (its craft-vs-folio ownership question is still open, test_integration_dependency_system.py:38). Full pytest suite 2641 passed/0 failed. KNOWN, NOT FIXED: docs/REFCARD.md has the same staleness pattern across multiple sections (Site/Distribution/CI/CD subtotals, 20+ stale `/craft:git:worktree` examples for a command folded into the dev/git skill) — flagged, not yet swept. Folio's Aggregator Sync workflow fails by design (folio is a new plugin, never added to the shared Data-Wise aggregator 's marketplace.json — aggregator-sync.sh deliberately refuses to silently add unknown plugins; adding folio there is a separate, reviewed decision). CP-4 tasks/ cleanup: craft's tasks/ deleted directly on dev (unprotected); folio's tasks/session-plan-T2.4-CP2.md removed via PRs #15 (dev) and #16 (main) rather than a direct-branch delete (blocked by the permission classifier). Release: https://github.com/Data-Wise/craft/releases/tag/v4.0.0 · PR: https://github.com/Data-Wise/craft/pull/294. Prior: **v4 FOLIO SPLIT MERGED to dev 2026-07-16** — PR #290 (merge-commit `4af10f27d`, phase history preserved for the eventual v4 release tag): craft's docs-authoring surface extracted to the standalone `folio` plugin. **Counts 95/45/8 → 47/40/2.** NOT yet released (v2.61.2 still the current tag); `feature/folio-split` worktree + branch PRESERVED — Phase 4 (folio `/folio:do` + coordinated release, folio first / craft tag LAST) rides it. Phase 3.6 (router consolidations) CLOSED at **1 router of 5 planned**: `code:audit` shipped shrunk 5 cmds→2 (the `audit-router` skill, D12); `orch` (D11), `arch` (D6), and `ci` (D13) all DROPPED after reading the command bodies in full; `plan:feature` stayed excluded (pre-existing D2 lock). Root cause of the 4 drops, recorded in `docs/specs/GRILL-phase-3-6-router-consolidation-2026-07-15.md`: **the grill locked load-bearing decisions from line counts and flag names without reading the bodies** — every family that looked cohesive from surface metrics collapsed on a full read. D13 also withdrew its own planned `repo`-flag normalization as a premise artifact (no validator enforces cross-family flag consistency; the distinguishing test is *find the thing that fails if it's wrong*). Real defects fixed: 4 `category:` metadata mismatches (`ci/{fix,local}.md` declared `code`; `orch/{drive,workflow}.md` declared `orchestrate` against a nonexistent dir) — flagged by `help_file_validator._check_category_mismatch`, live-wired via `docs_update_orchestrator` → `/craft:docs:update`; a repo-wide sweep found the `orch` pair (pre-existing on dev). **Repo-wide category mismatches now 0.** T3.4 command floor 60→40 (`ci.yml:92`): the floor is a mass-deletion guardrail and had gone stale TWICE (86→60) while the real count fell 94→70→47, leaving it ABOVE the surface and hard-failing CI every run; rationale now commented in-file. `tests/test_git_shim_correctness.sh` INVERTED (not deleted) — it asserted `commands/git/*.md` were thin shims, but T3.5.2 completed that migration by deleting all 11, making the assertions unsatisfiable (7 FAIL on the branch, 21/21 on dev); it now guards the migration's END STATE (shims stay deleted · Operations 1–12 survive in `skills/dev/git/SKILL.md` · `issue-check` stays live · no unsanctioned command reappears), verified by 3 planted defects. `/craft:git:issue-check` KEPT as a live command (NOT folded into the git skill, reversing the initial plan): `tests/test_issue_check_unit.py` extracts its `classify_issue()` block from `commands/git/issue-check.md` and `exec()`s it, so the command file is the classifier's single source of truth — the same constraint as `commands/ci/triage.md` (D13). Git's directory-rename detection had silently relocated dev's new command into `skills/dev/git/references/` during the merge, which would have broken that test. Merge resolved 34 conflicts (~30 count-cascade, dev 95/45/8 vs branch 46/40/2) — every tracked count recomputed mechanically via `bump-version.sh --counts-only` rather than hand-resolved; the guard-script conflicts expected in `no-switch-guard.sh`/`install-guards.sh`/`version-check.sh` did NOT materialise. Prior: **PR #291 MERGED to dev 2026-07-16** (squash `88b221da9`): `ci-bash-suites` validator (`.claude-plugin/skills/validation/ci-bash-suites.md`) — closes the **gate-divergence** gap that caused two of #290's three CI blockers. `/craft:check` never ran the shell suites CI invokes directly because pytest doesn't collect them, so the local gate reported green for a surface it never touched; T3.5.2 shipped a regression citing "full pytest 2575 passed" while a CI-required bash suite had been red for days. Derives its suite list FROM `.github/workflows/*.yml` rather than hardcoding (craft has 22 `tests/*.sh`, CI runs **6** — the parse corrected a 4-suite assumption held all session); advisory in default mode, blocking in `release`, per the release-gated validator contract (`CRAFT_MODE` can't distinguish `--for pr` from `--for commit`). Verified by replaying the REAL T3.5.2 bug (pre-fix suite restored from `d41de18ee`) → caught. No count cascade (validators sit outside the counted surface). Also 2026-07-16: two stale claims fixed in `docs/commands.md` (`5034aef7d`) — a dry-run tip saying "46 commands" (the old TOTAL pasted into a dry-run claim; now 27) and "46 commands for documentation generation, validation, and site management" (now 2 — that sentence described a surface craft no longer has). Local `~/.claude/hooks/branch-guard.sh` re-synced via `install-guards.sh` (#290 merged the v4 hook text into dev, so the installed hook predated the repo and was advising a DELETED command, `/craft:git:guard disable branch-guard`) — this cleared the last known local test failure; dogfood 51 passed/0 failed. ⚠️ KNOWN, NOT FIXED: `docs/commands.md:10`'s "17 Categories" list is stale (`discovery-usage` demoted, `orchestrate`→`orch`, no `workflow` dir) but "category" has two defensible definitions that disagree — frontmatter-declared gives 10 (…`smart`), directories give a different 10 (…`utils`), and the current list matches neither; left for a decision rather than guessed at. Prior: Guard-hardening arc SHIPPED to dev 2026-07-15 — PR #287 (cumulative-cwd cd-target resolution, both guard hooks), PR #288 (guards.json write-race mkdir-lock, both writers), PR #289 (plan-orchestrator dispatch hardening); full detail in the 2026-07-15 Session Recap below. Counts unchanged 95/45/8; NOT yet released. Prior: PR #283 SHIPPED to dev 2026-07-14 (squash `057ea6785`): `/craft:git:issue-check ` — before implementing a fix requested by an open GitHub issue, checks whether the issue's premise still holds against current code (structured verdict: valid/moot/unclear, always cited evidence, never cached, advisory-only). Also gates `/craft:orch:drive` with a cheap `#NNN`-citation pre-filter and corrects `/recap`'s pointer copy. Built via `/craft:brainstorm` → `/craft:grill` (9 locked decisions in `docs/specs/GRILL-github-attention-triage-2026-07-14.md`; adversarial-review pass caught and corrected a factually-wrong dogfood target — issue #199 assumed moot, actually still open) → `/craft:plan` (plan-orchestrator, `ORCHESTRATE-github-attention-triage.md`) → `orchestrate-dispatch`. Dispatched agent completed all 5 phases correctly but silently failed to commit Phase 5 or update its own checkboxes before stopping mid-sentence — caught by the hang-detection cross-check (notification text vs. ORCHESTRATE file state), not a code failure; recovered by verifying git log + a fresh full suite run, fixing 5 newly-stale "94 commands" doc refs the count-cascade tooling missed, then committing + correcting checkboxes. PR CI initially failed on a genuine regression (broken relative link in the ORCHESTRATE file's own header, fixed same session) plus the 3 already-known `test_v115_adhd_enhancements` pre-existing failures; merged via `--admin` once confirmed unrelated. Counts 94→95/45/8; NOT yet released (v2.61.2 still current tag). Prior: Docs/website audit COMMITTED to dev 2026-07-09 (`d6aaa9096`) — 264 deleted-command refs stripped across 40 files, 5 whole-docs archived to `docs/archive/deleted-commands-v3/`, `mkdocs build --strict` clean. FOLIO SPLIT PLANNED (new initiative, supersedes dropped Phase 3): grill ledger `docs/specs/GRILL-folio-split-2026-07-09.md` (5 locked: tools+docs-standards scope · caller-based cut · drop DOCS from /do+hub · dup count tooling · history-preserving subtree, craft v4/folio v1) + `docs/plans/ORCHESTRATE-folio-split.md` (5 phases, P0 gates). P0 caller-audit RAN via dynamic Workflow (7 agents, adversarially verified, partition_sound): STAYS = docs:update + docs:changelog + site:deploy-as-shell; MOVES = 26; flagged docs:claude-md:* trio as craft-internal governance (likely stays) — partition NOT yet recorded into the plan docs. EXECUTION STARTED 2026-07-09 ("build auto"): worktree ~/.git-worktrees/craft/feature-folio-split live (T1.1 done, ORCHESTRATE + tasks/ moved to branch root); T1.2 folio repo creation gated on ask. ⚠️ ACCOUNT CI: zero-CI mode active across private repos since 2026-07-09 (craft is public = unaffected/free) — see memory `github-actions-zero-ci-strategy-2026-07` + savant .STATUS for the full record. Prior: Phase 1 native-first prune MERGED to dev 2026-07-09 — PR #279 (squash `4fa44fbb`): pruned 21 dead-namespace commands (115→94, −5533 lines) + excluded dead docs (specs/plans/archive) from the built mkdocs site. ⏹ NATIVE-FIRST THINNING REFUTED: a Stage-2 dynamic workflow (3 pilots × 4-axis gate + cross-plugin routing test) returned NO-GO on Phase 2 — the `do`/`hub` router structurally can't dispatch cross-plugin, AND 3/3 pilot replacements (native `/simplify`, `/code-review`, `agent-skills:ci-cd`) aren't ≥ craft on all 4 axes (weaker/auto-apply, third-party dep, or generate nothing). Phases 2 (cascade thinning) + 3 (`folio` docs/publishing split) DROPPED; folio only revisitable on docs-maintenance merits, grilled fresh. ADR-002 rich-body gate held mid-build: 3 of 24 "safe" deletions (`git:docs:refcard`, `check:gen-validator`, `workflow:insights`) carried unique logic → restored, not deleted (caught by `test_skill_referenced_commands_exist`). Verification gap fixed: pytest passed 2676 but CI's Validate-Plugin-Structure bash suite (`test_git_shim_correctness`) asserted deleted `git/{sync,git-recap}.md` exist → shim array updated (see memory `pytest-doesnt-cover-craft-ci-bash-suites`). Counts now 94/45/8 on dev; NOT yet released — v2.61.2 still the current tag. Full record: `docs/specs/SPEC-craft-native-first-breakup-2026-07-09.md` OUTCOME block. Worktree `feature/craft-native-first-breakup` removed + remote branch deleted 2026-07-09; local branch ref left stale (branch-guard blocks force-delete of squash-merged branch — documented deadlock, harmless). Prior: v2.61.2 SHIPPED 2026-07-09 — PR #275 (dev→main merge, tag v2.61.2) + PR #276 (CI fix): docs/NEWS.md release highlights page, CHANGELOG/VERSION-HISTORY v2.61.1 section, REFCARD.md version box corrected (2.61.0→2.61.2, counts 117→115), exclusions.txt archived-doc path fixed. PATCH bump. Docs Staleness + Markdown Linting GREEN; Validate Plugin Structure + Link Validation pending (known hang). Release: https://github.com/Data-Wise/craft/releases/tag/v2.61.2. Counts unchanged 115/45/8. Prior: v2.61.0 SHIPPED 2026-07-06 — PR #271 (dev→main merge `6c3e2e95`, tag v2.61.0): do.md dead-agent-dispatch removal + 24-doc sweep, `verify-surfaces.sh --report-only`/`--version` + 2 new legs (GitHub release, docs site) + release-rollback runbook, 4 confirmed branch-guard/no-switch-guard gaps closed, quota tooling removed (all from PR #270, squashed to dev pre-release), plus post-merge docs-audit fixes: 2 dead-agent surface-forms PR #270's hyphenated-only grep missed (`docs/index.md`'s feature grid, `skills/orchestrator-resilience/SKILL.md`'s fallback-dispatch example), a docs-staleness false positive (`orch.md`'s mode-table "4 agents"), and backfilled `CHANGELOG.md`/`docs/CHANGELOG.md` `[Unreleased]` entries missed during #270's own PR prep. MINOR bump. Full suite 2707 passed/1 pre-existing failure (`test_roadmap_orchestrator_enhancements`, stale `ROADMAP.md` path predating this release — root-caused and FIXED post-release in `dc6ee28a`, corrected to `docs/archive/ROADMAP.md`; this exact failure had forced `--admin` overrides on PR #270, PR #271, and `main` directly, three releases running). Merged via `--admin` (same pre-existing failure, confirmed by log each time). Release: https://github.com/Data-Wise/craft/releases/tag/v2.61.0 · PR: https://github.com/Data-Wise/craft/pull/271. Downstream: docs site live at v2.61.0 (confirmed via curl), tap formula confirmed v2.61.0 (live raw.githubusercontent fetch, not local clone), Homebrew Release + Aggregator Sync + Craft MCP Release workflows all SUCCESS. `dev` fast-forwarded to `main`'s merge commit, pushed. Counts unchanged 115/45/8. Also this session: grilled and CLOSED the orchestrate token-usage-hooks backlog (captured 2026-07-02) with NO build — two review passes (backend/architecture + interface/CLI-contract lenses) found hook 1 infeasible (no live token-introspection API exists for a prompt-driven orchestrator session), hook 3 structurally uncomputable for an in-progress run; the one surviving feasible piece (`--token-report`) wouldn't have fixed the retroactive-analysis problem that motivated the backlog, and duplicates the accounting-tooling category this same session already removed (quota tooling) — see `docs/specs/GRILL-orchestrate-token-usage-hooks-2026-07-06.md`. Prior: v2.60.0 SHIPPED 2026-07-05 — PR #269 (dev→main merge `763c5e8c`, tag v2.60.0): command-namespace reorganization (full grill-and-implement cycle, `docs/specs/SPEC-command-namespace-reorganization-2026-07-05.md`) — root promotions `/craft:next`/`/craft:done`/`/craft:refine`/`/craft:brief`/`/craft:brainstorm` (heavy real-world use); `orchestrate`→`orch` rename across ~110 files; `ci:` consolidation (`code:ci-fix`/`code:ci-local`→`ci:fix`/`ci:local`); new `docs:generate` router (9 generators, none merged — genuinely distinct logic); new `task:` namespace; `/craft:quota` deleted (folded into `/craft:orch` pre-flight); `code:desktop-watch` deleted (was a pure redirect shim); `adhd-guide` converted to a skill reference. Pre-flight caught and fixed 8 real test failures before release: 6 stale "117 commands" claims (actual 115) across dist/docs files not covered by `bump-version.sh --counts-only`'s file list, one hardcoded pre-rename path in `test_drive_command_in_nav`, and a missing TOC on the newly-moved `adhd-guide.md` (>300 lines, tripped the skill-standards release gate). MINOR bump. Full suite 2707 passed/0 failed/5 skipped; pre-release-check PASSED; main CI GREEN (Craft CI + Deploy Documentation + Documentation Quality); live site confirmed v2.60.0; homebrew-release workflow SUCCESS (tap formula confirmed v2.60.0 via live `gh api`, NOT the stale local `~/projects/dev-tools/homebrew-tap` clone which still read 2.58.0); aggregator PR #19 auto-merged in `Data-Wise/claude-plugins`; post-release sweep ALL CLEAN. Counts 115/45/8. Release: https://github.com/Data-Wise/craft/releases/tag/v2.60.0 · PR: https://github.com/Data-Wise/craft/pull/269. Prior: v2.59.0 SHIPPED 2026-07-04 — PR #260 (dev→main merge, tag v2.59.0): `plugin-audit` skill (44→45, cross-plugin command/skill collision detector, schema + false-positive fixes before merge, #237) + docs-site staleness hardening H1/H2/H3 (`docs.yml` live-site post-deploy version poll, stale-count guard extended to all of `docs/**/*.md`, #259). Counts 116/45/8 (later corrected to 115 in v2.60.0 after the `resume.md` deletion cascade settled). Prior: v2.58.0 SHIPPED 2026-07-02 — PR #251 (dev→main merge, tag v2.58.0): distribution-surface hardening — curl-pipe installer fixed to clone current `main` instead of a frozen v1.16.0 mirror (D1, 11 contract tests); homebrew-tap copy-based install (no symlinks); net-new `mcp/` TS MCP server (craft-mcp Desktop bridge, 3 read-only tools, DXT/.mcpb); esbuild audit-clear. Counts 116/44/8. Prior: v2.57.0 SHIPPED 2026-07-01 — PR #242 (dev→main merge `ec5797e6`, tag v2.57.0): grill attack-angles (#238 — 7 project-agnostic adversarial axes, angle 3 reads target project's own CLAUDE.md/ADRs at runtime; skill count 43→44) + orchestrate-family wave-1 (#239 — deleted fictional 523-line `resume.md` session-teleportation command, command count 117→116; thin-shimmed `orchestrate:plan` to `plan-orchestrator` skill) + doc-impact rubric extension (#241 — 4 new scored types Tutorial/API/Cookbook/Architecture-doc with tiered thresholds + arch-doc double-count subtraction rule + advisory Site Consistency checklist) + orchestrate-dispatch mode (#240 — 3rd `--output` value dispatching a background Agent from the live planning session, with confirm-before-dispatch gate, scoped concurrency cap, failure/hang detection at 2×-effort-estimate window, resumability, scoped `.STATUS` auto-write). MINOR bump (4 feat/refactor PRs). #240/#241 were background-agent dispatches using the very pattern #240 formalizes — monitoring protocol caught 1 self-introduced broken-link regression (a cross-tree link into `~/.claude/...` from this session's own BRAINSTORM doc, fixed on dev) + recovered a killed agent's completed-but-uncommitted Phase 3 work. A CHANGELOG merge-order conflict between #240/#241 (both added `[Unreleased]` entries) was resolved by rebase, keeping both. Pre-release-check initially reported doc-coverage as BLOCKING due to a macOS bash-3.2 `mapfile` incompatibility (false positive — confirmed PASS, 0 gaps, via Homebrew bash 5.x/zsh). Full suite 2072 passed/7 failed/13 errors (documented env-caused baseline, not regressions); pre-release-check PASSED; main CI GREEN (Craft CI + Deploy Documentation + Documentation Quality); live site confirmed v2.57.0; homebrew-release workflow SUCCESS (formula confirmed v2.57.0, desc "116 commands"); post-release sweep ALL CLEAN. Counts 116/44/8. Release: https://github.com/Data-Wise/craft/releases/tag/v2.57.0 · PR: https://github.com/Data-Wise/craft/pull/242. Prior: v2.56.0 SHIPPED 2026-07-01 — PR #234 (dev→main merge `43beb7b3`, tag v2.56.0): token-usage reduction (PR #232 squash `e6c24a27` — orchestrator model pinning, `/refine` 631→42 lines, `/brainstorm` redesign, new `command-skill-token-efficiency` skill) + release-tracking hardening (namespace-refactor token-cost go/no-go measured 68.3% reduction; broken `/usage` scheduled-trigger replaced with `ccusage`+`claude-monitor`; 2 cross-spec factual bugs fixed via a second grill pass). MINOR bump (feat: #232). All CI checks GREEN on first pass — no `--admin` needed (Validate Plugin Structure resolved cleanly this time). Full suite 2089 passed; pre-release-check PASSED; main CI GREEN; live site confirmed v2.56.0; homebrew-release workflow SUCCESS; post-release sweep ALL CLEAN. Counts 117/43/8. Prior: v2.54.0 SHIPPED 2026-06-27 — `/craft:workflow:brief` (117th command): 3-line action block generator (Next step / Watch out for / Connects to) with `--plan`/`--verbose`/`--show-context` flags; `--brief` integrated into `/craft:do` Step 5.5. 29 e2e + 12 dogfood tests. Main CI GREEN; docs deployed; homebrew tap updated (117 commands); post-release sweep CLEAN (getting-started.md fixed); all BLOCK surfaces aligned. Counts: 117 / 39 / 8. Prior: v2.53.0 SHIPPED 2026-06-26 — multi-surface-aware release (surface registry + aggregator CI propagation + Cowork verify leg + `/craft:dist:surfaces`, #218/#184) + docs-staleness fix (docs/plans/ excluded from count-consistency scan). MINOR bump (feat). All tests pass; main CI GREEN; docs deployed; homebrew tap updated (116 commands); post-release sweep ALL CLEAN; aggregator sync wired (all 4 satellite repos). Counts 116/39/8. Prior: v2.52.0 SHIPPED 2026-06-26 — interactive-by-default commands (#215) + default-on test/docs scaffolding (#216) (PR #217 dev→main merge `c5f704f3`, tag v2.52.0, `--admin` on stuck Validate-Plugin-Structure): grill/orchestrate AskUserQuestion model (Recommended-first + per-option consequence) + `--yes`/`--non-interactive`; refine default-on for brainstorm/do/plan:feature/grill (opt-in elsewhere) + `--yes` cascade + orchestrate Step 0.5 structured model; AND brainstorm/plan:feature/grill emit a tier-inferred test-plan + doc-scorer Documentation section BY DEFAULT (`--no-tests`/`--no-docs`; arch:plan/spec-review opt-in via `--tests`/`--docs`), logic in skills (brainstorm-insights/plan-orchestrator, deprecation-trap-safe + guarded), doc-scorer REUSED, lifecycle-split, count-cascade-excluded. Built spec→grill→plan→subagent-driven-TDD (2 plans, 7+8 tasks, per-task spec/quality + opus whole-branch reviews; gates caught a dropped-phrase regression, a refine over-flip, a decorative deprecation-guard sentinel, and a MEMORY.md over-compaction that dropped 37/102 pointers → all fixed). MINOR bump (4 feat). Full suite 1990 passed; pre-release-check PASSED; main CI GREEN; docs deployed (live site 2.52.0); Homebrew Release auto-updated tap; verify-surfaces 6/6 ALIGNED (refreshed local lag: brew formula 2.51→2.52 via `--formula`, marketplace-update-THEN-plugin-update for the Code pin); post-release sweep ALL CLEAN. Counts 115/39/8. Prior: v2.51.0 SHIPPED 2026-06-25 — governance retro (#205) + skill-standards release gate (PRs #211/#212→dev, #214 dev→main merge `180181f8`, tag v2.51.0; CLOSED #205 + #138 via default-branch auto-close): D1 **R09 status-not-drift** governance check (`governance/checks/status_drift.py`, warn) — audits `.STATUS` `version:` vs manifest + SHIPPED/tagged vX.Y.Z vs git tag, fixture `--selftest` + LIVE in `pre-release-check.sh` (this very release was self-gated by it: ".STATUS: version + release claims match" ✓); D3 ADR-003 (release-drift advisory not hard-gate + P7 freeze); skill-standards validator graduated to a **release-tier gate** (`CRAFT_MODE=release` blocks, advisory below) + fixed a latent CRAFT_MODE no-op (LLM-prose command never exported the gating var → gate+version-check silently dead). MINOR bump (2 feat: #211/#212). Full suite 1970 passed; pre-release-check PASSED; main CI GREEN; **verify-surfaces 6/6 ALIGNED** (after refreshing local lag: brew upgrade 2.50→2.51, `claude plugin update` 2.50→2.51, pulled stale `~/projects/dev-tools/homebrew-tap` checkout — the script reads that local clone FIRST, was the only "blocker"); post-release sweep ALL CLEAN. Counts 115/39/8. Process: merged #211/#212 (conflict-free despite branches behind dev — `--stat` looked conflicted but they authored ZERO changes to `.STATUS`/`CLAUDE.md`/`governance.md`; verified via `merge-tree --write-tree`), pushed a local-only gate branch first (data-loss risk), `git:clean` removed 2 worktrees (11+2 squash-merged branch refs left dangling — branch-guard blocks `-D`, harmless). Prior: v2.50.0 SHIPPED 2026-06-25 — v2.49.x issues sprint (PRs #206/#207/#208→dev, #209 dev→main --admin on phantom Validate Plugin Structure, tag v2.50.0): advisory skill-standards validator in `/craft:check` (hot-reload, never fails); homebrew dist-gates `verify_caveats.py`+`post_install_check.py` wired into `/release` Steps 10b/10c (advisory, strict via HOMEBREW_GATE_STRICT) + 10d (BLOCKING aggregator-sync + Cowork verify) (#200/#199); SessionEnd insights facet hook + idempotent installer, per-session-id dedup (#183); ADR-001 Workflow-vs-worktree (#171); +14 e2e/dogfood tests pinning locked decisions. Built via craft:grill (10 decisions D1–D10) → superpowers writing-plans (4 TDD tracks) → 2 dynamic Workflows (impl+verify, ran from dev session under branch-guard bypass, re-protected after). MINOR bump (3 feat: commits). Full suite 1958 passed; pre-release-check PASSED; main CI GREEN; tap+brew+Code+marketplace all 2.50.0 (verify-surfaces 6/6 ALIGNED); post-release sweep CLEAN. KEY INCIDENTS (recovered): (1) #206 squash folded UNPUSHED local-dev base commits → #207/#208 phantom conflicts → rebased --onto origin/dev to untangle (memory squash-folds-unpushed-dev-base-commits; push dev before PRing off it); (2) GitHub comma-list `Closes #200, #199, #183, #171` only auto-closed #200 (+#171 via Track-0 trailer) — each issue needs its OWN keyword; closed #183/#199 manually. Counts 115/39/8. Prior: v2.49.0 SHIPPED 2026-06-23 — skill-standards auditor + /done memory/settings + ADR-002 + release re-split (PR #203 auditor→dev, #204 release dev→main --admin, tag v2.49.0): `/craft:code:skill-standards` batch-audits `skills/**/SKILL.md` vs a vendored copy of Anthropic's standards (score+exit 0/1/2, `--json`/`--markdown`, `--fix` = version-tag-strip + frontmatter-normalize + TOC-stub, never prose; `--refresh-standards`); `/done` Memory Optimize (Step 1.12) + Settings Sync (Step 1.10.5); ADR-002 consolidated `/done` into the `adhd-workflow` skill (`references/done.md` SoT + thin shim); 4 oversized skills split to `references/` (release re-split loss-gated after a lossy first attempt, 0 lines lost) → 39 skills 100/100. Counts 114→115. Release: dev was unsynced with main (v2.48.0 release merge commits) → dev-wins sync merge resolved the PR #204 conflict; full suite 1929 passed; main CI GREEN; docs deployed; tap synced. POST-RELEASE TOOLING (homebrew-tap + savant): root-caused the local Code-registered lag — the formula `post_install` ran `claude plugin update` WITHOUT first `claude plugin marketplace update local-plugins` (no-op on a stale cache); fixed in `homebrew-tap/generator/generate.py` for ALL claude-plugin formulas; reconciled manifest-drift across 6 formulas (rforge-orchestrator's `deprecate!` was at risk of silent un-deprecation; himalaya-mcp 12 skills/22 MCP tools; scholar 28→34; craft `{command_count}` single-source token) gated by regen==committed; added a Formula Drift Guard CI workflow (regen-vs-committed, first run GREEN); savant `CLAUDE.md` 19→25 skills. Memories: post-install-marketplace-refresh-before-update, deprecated-command-rich-body-trap, skill-authoring-tooling-landscape. Prior: v2.48.0 SHIPPED 2026-06-23 (grill + orchestrate Step 0.5 Clarify). Prior: v2.47.0 SHIPPED 2026-06-21 — Governance roadmap COMPLETE (PR #196/#197→dev, #198→main, tag v2.47.0): R04 automated as content-drift (manual→checks/no_drifted_copy.py — installed SKILL.md must be byte-identical to canon; DISTINCT from R07 version-pin, catches a hand-edited body even when the version matches; gates session, vacuous-skip otherwise) + #184 release pre-flight advisory (pre-release-check.sh runs run_rules.py --json, prints RED count, NEVER blocks — gentle-ramp; TestReleaseGuard184 invariant) + live-flow test coverage (e2e soak lifecycle hook→ledger→promote-check + R04-in-audit; dogfood ledger edges). Governance suite 47→59. KEY FINDING (memory r04-content-drift-not-filetype): naïve "real-dir=copy" heuristic was dead (installs are real dirs, no symlinks) → content-drift; always fire a new WARN checker on the live env first (a false positive poisons the soak ledger). #198 merged CLEAN (no --admin). Homebrew tap synced to v2.47.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 / 39 / 8. Governance now: R01–R04+R06 automated · R05 advisory · R07 external · R08; 5 surfaces (visibility·prevention·promotion·cross-repo·release-advisory). Prior: v2.46.0 SHIPPED 2026-06-21 — Governance Phase 2 PR #3 (PR #194→dev, #195→main, tag v2.46.0): soak-then-flip promotion + cross-repo wrapper, completing Phase 2 (visibility · prevention · promotion · cross-repo). soak.py = LOCAL gitignored STATE.json ledger (first_seen/last_seen/last_red per rule) fed by the SessionStart hook each audit; run_rules.py --promote-check (--window 14d, --state) lists warn rules soaked clean long enough to recommend a human warn→error flip (machinery recommends, human promotes; advisory exit 0). run.sh = cross-repo wrapper (consumers invoke the one installed engine, cwd-portable, no drift, R07). 8 dogfood tests (injected today → no flake), 47 governance suite green. Bundled #193: test_performance_cached_run → xfail(strict=False) (wall-clock microbenchmark flaked on shared CI, forced --admin; now a soft signal) — and #195 MERGED CLEAN with NO --admin (the xfail paid off). Homebrew tap synced to v2.46.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 / 39 / 8. R04 automation + #184 release-pre-flight guard remain deferred. Prior: v2.45.0 SHIPPED 2026-06-21 — Governance Phase 2 PR #2 (PR #190→dev, #192→main, tag v2.45.0): SessionStart visibility hook (governance/session_hook.py) — audits the live ~/.claude/skills tree at session open and injects a compact RED-only summary into context via SessionStart additionalContext; silent when clean, mtime-cached, no-op where skills tree absent, never breaks a session on error. Visibility not prevention (SessionStart hooks inject context, can't block). Completes the three-surface model: pre-commit + CI (prevention) · SessionStart (visibility). Implemented direct-to-worktree from the dev session (worktree-path-aware). #192 admin-merged (strict up-to-date BEHIND from the v2.44.0 merge-commit divergence; PR-context full suite GREEN). Homebrew tap synced to v2.45.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 / 39 / 8. Global install of the hook DEFERRED to post-v2.45.0 (point at released plugin path; docs in governance/README.md). PR #3 (soak-then-flip + cross-repo wrapper) + R04 deferred. Also: removed leaked ORCHESTRATE-guard-suite.md (PR #191). Prior: v2.44.0 SHIPPED 2026-06-21 — Governance Phase 2 PR #1 (PR #188→dev, #189→main, tag v2.44.0): R03 automated (manual→script — checks/no_private_in_public_marketplace.py scans a marketplace manifest, fails on a public-marketplace ref to a private/PII repo, denylist {savant}, dual-mode file/dir + good/bad fixtures; closes the last error-rule enforcement gap), pre-commit governance-gate hook (^governance/ → selftest + render --check drift) + CI gate in ci.yml (with explicit "CI does NOT evaluate R01/R07 live-env" note), engine {marketplace} substitution through audit()+selftest() + --marketplace. Built from a draft→adversarial-verify→ultra-finalize workflow chain (verified packet); implemented directly into the worktree from the dev session (branch-guard is worktree-path-aware — corrected a stale assumption). Also corrected stale "not yet wired" governance docs. #189 admin-merged (only failure was the flaky test_performance_cached_run timing test, unrelated to the diff; PR-context full suite GREEN). Homebrew tap synced to v2.44.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 commands / 39 skills / 8 agents. Phase 2 PR #2 (SessionStart hook + soak-then-flip + cross-repo wrapper) NEXT; R04 deferred. Prior: v2.43.0 SHIPPED 2026-06-20 — Skill-Ecosystem Governance (Phase 0) (PR #185→dev, #187→main, tag v2.43.0): policy-as-code under governance/ — RULES.yaml (8 skill-location rules, single source of truth), run_rules.py (audit + --selftest meta-validation, fail-closed), render_rules.py (CLAUDE.md block generation + --check drift gate), portable checkers + good/bad fixtures, guide + Plugin Release Runbook, 24 e2e/dogfood tests + governance pytest marker. PR #185 review hardening (all 5 issues): fail-closed gating (broken checker on error rule exits 1), recursive broken-symlink detection, R01 gates session (not ci) + visible canon-skip, --check exit-2, --selftest surfaces external rules, UTF-8. Homebrew tap synced to v2.43.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 commands / 39 skills / 8 agents. Phase 2 enforced-gates roadmap spec open in PR #186. Prior: v2.42.0 SHIPPED 2026-06-19 — /craft:code:fewer-prompts curated read-only Bash allowlist (PR #181). Prior: v2.41.1 SHIPPED 2026-06-19 — Documentation Coverage (PR #180): 25 tutorial stubs for all Phase 8 commands + broken link fix in TUTORIAL-smart-help.md. Prior: v2.41.0 SHIPPED 2026-06-19 — token-efficiency (quota gate, engine flag, parity runbook). v2.40.0 SHIPPED 2026-06-19 — Guard Suite (PR #174): /craft:git:guard + guards.json registry & ADHD toggle UX, no-switch-guard promoted to scripts/, guard-consistency validator, install-guards.sh. Destructive-restore reconciled to no-switch-guard; branch-guard rm-rf-.git/reset--hard softened to confirm (catastrophic ops stay NON-muteable). 111 commands. Review found 1 CRITICAL (catastrophic-bypass) + 6 issues — ALL fixed pre-merge; +5 registry tests; ci.yml now installs both guards + runs the no-switch suite. Prior: v2.39.0 SHIPPED 2026-06-19 — doc-coverage enforcement (PR #172→dev, #173→main). Prior: v2.38.2 SHIPPED 2026-06-15 — multi-surface release arc (3 patches). v2.38.0: multi-surface engine (verify-surfaces Step 13.6, cache-prune Step 13.7, Data-Wise aggregator marketplace) + branch-guard draft=dev (PRs #158/#159/#160/#161). Adversarial review found 3 HIGH + 6 MEDIUM/LOW bugs → v2.38.1 (PR #163: branch-aware dogfood tests fixed RED main CI; cache-prune data-loss; verify-surfaces git-tag no-op) → v2.38.2 (PR #165: corrupt-surface blocks, symlink GC, arg-parse, subshell). All 3 → main, main CI GREEN, tap + aggregator (Data-Wise/claude-plugins) pinned to 2.38.2, post-release sweep CLEAN. Counts: 112 commands / 39 skills / 8 agents. Prior: v2.37.0 SHIPPED 2026-06-13 — drift-prevention chain + CI triage/watch. Bundled brainstorm items 1-7 (PRs #151/#152/#153/#154/#155): #1 strict-markers guard + #4 workflow recipe discoverability; #7 bump-version categorical-subtotal sweep; #2 CI count-drift tripwire (README live counts); #5 /craft:ci:triage + #6 /craft:ci:watch. PR #156 → main 8c21a54d (merge commit), release tag v2.37.0. Downstream all GREEN: main CI verified, homebrew-release.yml updated tap to v2.37.0 (desc "112 commands", url v2.37.0.tar.gz). Post-release sweep ALL CLEAN (drift tooling paid off). Counts: 112 commands / 39 skills / 8 agents. Prior: v2.36.0 SHIPPED 2026-06-13 (PR #150 → main eb7e143d). -last_session: 2026-07-30 +release_date: 2026-08-07 +milestone: **v4.5.0 SHIPPED 2026-08-07** — PR #329 (dev→main merge, tag v4.5.0, MINOR): new `repo-triage` skill (41st skill) batch-grounds open GitHub issues and stale worktrees/branches against current repo state, offering confirmed (never automatic) deletion/closure — built via brainstorm→grill (`GRILL-repo-triage-2026-08-07.md`, 2 factual corrections)→dispatched ORCHESTRATE worktree, code-reviewed (8 findings, all fixed pre-merge: jq-null pr_state bug, swallowed git-diff failures, not-merged-branch passthrough, tracked ORCHESTRATE artifact, stale skill-count prose, duplicated evidence-normalization + fence-extraction logic consolidated into `classifier_loader.py`, cross-repo file-check bug), merged PR #328 squash to dev first. Also shipped: 8 new ADHD-friendly ecosystem-tool cheat-sheet tutorials (Codex, Remember, i-have-adhd, Token Optimizer, Security Guidance, Claude HUD, Dropbox, Agent Skills) under a new nested `Ecosystem Tool Cheat Sheets` mkdocs subsection (`SPEC-ecosystem-tool-tutorials-2026-08-07.md`), plus a `docs/commands/hub.md` nav-completeness fix (`TUTORIAL-opencode-plugins.md` was missing from mkdocs.yml). Pre-flight found 2 non-blocking local-only failures (not regressions, CI confirmed green on the same commit): `test_branch_guard_dogfood.py` (the installed hook is a symlink to the sibling `cc-config` repo, out of scope per cross-repo permission boundary) and `test_no_live_legacy_check_skill_references` (was walking gitignored `.remember/` via raw filesystem `rglob()` — fixed to scope to `git ls-files`, genuine bug, fixed pre-release). Full suite 2632 passed/0 failed (post-fix); `pre-release-check.sh 4.5.0` clean; docs-staleness GREEN; live site + Homebrew formula + aggregator sync (claude-plugins PR #46, auto-merged) all confirmed v4.5.0; `verify-surfaces.sh` needed a local `homebrew-tap` `git pull` + `brew upgrade` + `claude plugin update` refresh before reporting ALIGNED (known pattern — the script reads the local tap checkout, not live GitHub). KNOWN, NOT FIXED: `aggregator-sync.yml` returns 404 by that exact workflow name in `claude-plugins` (the release skill's own docs name a workflow that doesn't exist under that filename) — propagation still worked via some other mechanism (PR #46 auto-merged), doc/workflow-name discrepancy left unswept, out of scope for this release. Release: https://github.com/Data-Wise/craft/releases/tag/v4.5.0 · PR: https://github.com/Data-Wise/craft/pull/329, https://github.com/Data-Wise/craft/pull/328. **Post-release same-day (2026-08-07):** doc audit found + fixed 3 stale-prose bugs the count-consistency gate can't catch (`REFCARD.md`'s version-box date/highlight + skill count, `skills-agents.md`'s "8 specialized agents", `docs/index.md`'s 5× "47 commands" + missing real install instructions — Quick Start only had the dev-only local-plugins symlink, added Homebrew + Data-Wise marketplace paths); added a `triage-your-repo` cookbook recipe for the repo-triage skill. Brainstormed → spec'd → adversarially reviewed a gate fix for this exact blind spot (`SPEC-doc-staleness-prose-gaps-2026-08-07.md`: 2 new Phase 7 checks, release-date claims + structured-line-scoped count prose; review killed a 3rd check that was falsified by its own target and caught 90+ false positives in the 2nd, both fixed pre-build; not yet implemented). Cross-repo: `homebrew-tap#211` fixed a stale `/craft:git:unprotect` caveat reference (command removed in v4) — first push tripped Formula Drift Guard (content changed, no revision bump), fixed with `revision: 1`; merged, verified live via `brew upgrade` (4.5.0 → 4.5.0_1, new caveat text confirmed). Follow-up noted, not built: `homebrew-tap`'s own `manifest.json` caveats prose has the identical staleness blind spot, out of scope for the craft-only SPEC. Prior: **v4.4.2 SHIPPED 2026-07-27** — PR #318 (dev→main merge, tag v4.4.2, PATCH): the one fix merged to dev since v4.4.1 (PR #317, closes #316). `preflight-check` skill made reachable by its declared identity — it lived at the pre-rename `skills/check/` location while declaring `name: preflight-check`, so directory-based skill discovery couldn't find it; moved to `skills/preflight-check/`, `/craft:check`'s public argument contract unchanged. New recursive fixture-backed identity tests inventory every command/skill directory-vs-frontmatter mismatch and command/skill name collision plugin-wide, rejecting new drift; 8 pre-existing mismatches + 3 intentional shim collisions ledgered as owned debt through v5.0.0. Test automation now targets stable Python 3.14 (`.python-version` pin, 5 GitHub Actions workflows aligned, drift-guard policy test) — the local-interpreter assertion in that test was found too brittle on review (hard-failed for any contributor not on 3.14) and fixed to skip instead before merge. `docs/NEWS.md`/`docs/index.md`'s own v4.4.2 release-note prose initially contained the literal `skills/check/` string, which the new #317 regression guard (`test_no_live_legacy_check_skill_references`) correctly caught as a repo-wide scan, not scoped to code — rephrased without the literal path, second commit. Full suite 2627 passed/0 failed/52 skipped/1 xfailed/1 xpassed; `pre-release-check.sh 4.4.2` clean (10/10); docs-staleness GREEN; live site + Homebrew formula (fresh sha256) + aggregator sync + Craft MCP Release all confirmed v4.4.2; post-release sweep ALL CLEAN. Release: https://github.com/Data-Wise/craft/releases/tag/v4.4.2 · PR: https://github.com/Data-Wise/craft/pull/318, https://github.com/Data-Wise/craft/pull/317. Prior: **v4.4.1 SHIPPED 2026-07-26** — PR #315 (dev→main merge, tag v4.4.1, PATCH): post-4.4.0 fixes only, no features. `scripts/branch-guard.sh`'s `2>&1`/`1>&2` false-positive (a command like `git commit -m "...(7 -> 9)" 2>&1` misdetected as file creation) was fixed live on the installed hook earlier this session but never ported to the repo's canonical copy, leaving `test_repo_copy_matches_installed` red and the bug reintroducible on reinstall — ported with 2 regression tests (implements `GRILL-rtk-hook-safety-close-out-2026-07-26.md` decision #3). `docs/help/refine-flag.md` and `docs/reference/COMMAND-PARAMETERS.md` fixed for PR #313's 9-command declarer set (was still 7, missing `/craft:plan`/`/craft:smart-help`) — new dogfood test guards against recurrence. `docs/index.md`'s "Latest" callout and `docs/NEWS.md` synced (both still described v4.3.0's content under a v4.4.0 label; NEWS.md had no v4.4.0 entry at all). Also triaged 7 remaining open issues (#228, #213, #199, #170, #169, #133, #108): closed #108/#213/#133, re-scope comments on #228/#170/#169, confirmed #199 valid. Full suite 2654 passed/0 failed/16 skipped/1 xfailed/1 xpassed; `pre-release-check.sh 4.4.1` clean; docs-staleness GREEN; live site + Homebrew formula confirmed v4.4.1; post-release sweep ALL CLEAN. Release: https://github.com/Data-Wise/craft/releases/tag/v4.4.1 · PR: https://github.com/Data-Wise/craft/pull/315. Prior: **v4.4.0 SHIPPED 2026-07-26** — PR #314 (dev→main merge, tag v4.4.0, MINOR): the one `feat:` commit since v4.3.1. `smart-help` declares `--refine` (D7, PR #313) — opt-in, default-OFF, closing the last dangling item on #268's prompt-refiner umbrella. `prompt-refiner`'s Default Policy table made exhaustive and re-keyed on command file path — fixed 3 stale display names, added 2 live callers it was silently missing (`commands/plan.md`, `commands/arch/plan.md`); a new dogfood test (`test_refine_default_policy_table_exhaustive`) guards against future drift. #268's other two proposed items (named destinations, `--scope global`) and #266 (multi-round grill mode) were grilled then killed on adversarial review before implementation — see `docs/specs/GRILL-prompt-refiner-remaining-2026-07-26.md`'s amendment; #266 closed pointing at savant's `edit-loop` (shipped 2026-07-13), which already solves the observed pattern with tooling craft structurally lacks. Full suite 2618 passed/0 failed/49 skipped/1 xfailed/1 xpassed; `pre-release-check.sh 4.4.0` clean (10/10); docs-staleness GREEN; live site + Homebrew formula + aggregator sync all confirmed v4.4.0. Release: https://github.com/Data-Wise/craft/releases/tag/v4.4.0 · PR: https://github.com/Data-Wise/craft/pull/314, https://github.com/Data-Wise/craft/pull/313. Prior: **v4.3.1 SHIPPED 2026-07-25** — PR #312 (dev→main merge, tag v4.3.1, PATCH): two bug fixes merged to dev since v4.3.0, both traced to the same session. `CRAFT_GUARD_ALLOW_DEV_EDIT` escape hatch (#281) surfaced in the guard-bypass-marker confirms themselves (#309, PR #310) — the documented "Claude writes `.claude/allow-once`" one-shot flow was circular for the one case where the approved action IS creating/editing that marker; added the env-var hint to the three marker-specific confirms, reconciled both branch-guard reference docs. Bare-`vX.Y.Z` version-grep bug class swept repo-wide (PR #311) — the same false positive that broke the Homebrew release gate twice (v4.2.0, v4.3.0; fixed for `docs/index.md` in #308) also affected `docs.yml`'s live-site poll, `verify-surfaces.sh`'s `resolve_docs_site`, `version-sync-precommit.sh`'s blocking `.STATUS` pre-commit check, and manual release-verification docs — all re-anchored to the actual version signal instead of the first bare match, each with a regression test verified via planted-defect positive control. Full suite 2650 passed/0 failed/16 skipped/1 xfailed (tracked)/1 xpassed (known perf flake); `pre-release-check.sh 4.3.1` clean (10/10); docs-staleness GREEN. Release: https://github.com/Data-Wise/craft/releases/tag/v4.3.1 · PR: https://github.com/Data-Wise/craft/pull/312, https://github.com/Data-Wise/craft/pull/310, https://github.com/Data-Wise/craft/pull/311. Prior: **v4.3.0 SHIPPED 2026-07-25** — PR #307 (dev→main merge, tag v4.3.0): `CRAFT_GUARD_ALLOW_DEV_EDIT` escape hatch (#281, PR #305) closing the guard-bypass-marker creation deadlock, reusing the #168 env-var pre-authorization pattern; new advisory-only `reference-scope-guard.sh` hook (#286, PR #306) warning on non-conforming filenames under `~/.claude/reference/`, sharing `install-guards.sh`/`guards.json` with `branch-guard`/`no-switch-guard`. Full suite 2648 passed/0 failed. **Homebrew Release recurrence:** the exact same `pre-release-check.sh` false positive that broke v4.2.0's automated tap push (worked around manually then) broke v4.3.0's too — `docs/index.md`'s version check grepped the first bare `vX.Y.Z` string in the file, landing on a historical "since v4.0.0" migration note instead of the actual version badge. Fixed at the source this time (PR #308, fast-tracked dev→main): matched the badge pattern specifically (`version-X.Y.Z`), same as the adjacent README.md check. Re-dispatched `homebrew-release.yml` via `workflow_dispatch --ref main -f version=4.3.0` (the original `release`-event run had checked out the tag commit before the fix existed, so a plain rerun would have failed identically) — all 3 jobs green, tap formula confirmed live (`v4.3.0.tar.gz`, fresh sha256). Craft MCP Release + Aggregator Sync both green from the original release event. Release: https://github.com/Data-Wise/craft/releases/tag/v4.3.0 · PR: https://github.com/Data-Wise/craft/pull/307, https://github.com/Data-Wise/craft/pull/308. Prior: **v4.2.0 SHIPPED 2026-07-19** — PR #296 (dev→main merge, tag v4.2.0): **BREAKING:** `/craft:done` renamed to `/craft:finish` (ADR-006) — matches the existing zsh `finish` alias, no back-compat shim, command count unchanged (48, a rename not an addition); `/craft:restore` spec/report status corrected DRAFT→SHIPPED (had already shipped in v4.1.0 when written). Full suite 2649 passed/1 failed (a genuine broken-link regression from the rename sweep, `docs/commands/brief.md` still linked pre-rename `done.md` — caught by the test run itself, fixed same-commit)/10 skipped. Also merged to dev this cycle (not yet in a tagged release): PR #297 (`utils/docs_detector.py`'s `_extract_command_name()` truncated multi-hyphen command names past a 500-char read window, bumped to 2000), PR #298 (extends `/craft:finish` Step 1.10.5 with a `~/.claude/projects//memory/`+`.remember/` staleness check, 14-day threshold, report-only), and a `docs/reports/` folder established as the `/savant:report` output convention (moved `REPORT-craft-restore-2026-07-17.md` there, added to `scripts/config/exclusions.txt` alongside `docs/specs/`). **Homebrew:** v4.2.0's automated tap-formula push failed — `homebrew-tap`'s `main` gained branch protection (PR + 2 required checks) since `homebrew-release.yml` was last touched, so its direct `git push origin main` was rejected (GH006); recovered manually via `homebrew-tap`#164, which surfaced 2 more CI-gate bugs fixed directly in that repo (#165: `check-revision-bump.sh` false-positived on URL-tag-versioned formulas like craft's, no explicit `version` field to read; #170: the ".STATUS conflict markers" required check was path-filtered to only `.STATUS` changes, so it could never fire — and the PR stayed BLOCKED forever — on formula-only PRs). craft's own workflow fixed to match (PR #299: push a bot branch + open/auto-merge a PR instead of pushing directly), landing independently from a background-dispatched session whose worktree had gone stale after only completing 1 of the 2 related craft/homebrew-tap fixes — recovered by inspecting the worktree directly rather than trusting the dispatch's completion state. Release: https://github.com/Data-Wise/craft/releases/tag/v4.2.0 · PR: https://github.com/Data-Wise/craft/pull/296. Prior: **v4.1.0 SHIPPED 2026-07-17** — PR #295 (dev→main merge, tag v4.1.0): `/craft:restore` (NEW root command, combines `dev/git` skill's mode-aware git-activity recap with `adhd-workflow`'s `.STATUS`-based session recap into one read-only entry point; replaces the deleted `git-recap`/`recap` commands; no `--sync`; a cross-plugin `/restore` dispatcher to `savant:restore` was investigated and explicitly descoped — router can't dispatch cross-plugin) + `/craft:refine` confirm-flow fix (a 2026-07-01 design — fenced refined-prompt block + 4-way confirm: Execute now/Copy for elsewhere/Edit first/Skip — was locked but never implemented; now shipped across all 5 `--refine` callers + standalone, closing the "asks for acceptance without printing first" bug) + full post-v4.0.0 doc-staleness cleanup (47→48 count-drift sweep across ~35 files, mermaid health score 82.3→100 on new diagrams, `docs-staleness-check.sh` RED→GREEN). New architecture docs: `docs/architecture/craft-restore-pipeline.md`, `docs/architecture/prompt-refiner-pipeline.md`. Homebrew formula PR data-wise/homebrew-tap#153 open (tested locally: `brew style`/`brew audit --strict`/`brew install` all clean, 48 commands). Full suite 2650 passed/0 failed. Release: https://github.com/Data-Wise/craft/releases/tag/v4.1.0 · PR: https://github.com/Data-Wise/craft/pull/295. Prior: **v4.0.0 SHIPPED 2026-07-16** — PR #294 (dev→main merge-commit `678e4d4a`, tag v4.0.0): craft docs-authoring surface extracted to the standalone `folio` plugin (folio v1.0.0 released FIRST, per the rollback runbook's ordering). Counts 94→47 commands / 45→40 skills / 8→2 agents. Homebrew formula regenerated with real values (sha256, command_count=47) via homebrew-release.yml (3/3 jobs green); `brew audit`/`brew upgrade` verified clean for both craft and folio formulas. Post-release docs sweep (same session): commands/hub.md + its docs/ mirror had drifted badly (stale example counts, 2 stray text artifacts, a main-menu ASCII template whose header counts didn't match listed items and referenced commands moved to folio or folded into skills); rewrote to match the real 47-command roster. API-REFERENCE-COMMANDS.md (1850 lines, last synced v1.24.0) converted to a thin index into docs/commands/*.md + docs/REFCARD.md rather than re-syncing a third documentation surface indefinitely; DEPENDENCY-ARCHITECTURE.md left untouched (its craft-vs-folio ownership question is still open, test_integration_dependency_system.py:38). Full pytest suite 2641 passed/0 failed. KNOWN, NOT FIXED: docs/REFCARD.md has the same staleness pattern across multiple sections (Site/Distribution/CI/CD subtotals, 20+ stale `/craft:git:worktree` examples for a command folded into the dev/git skill) — flagged, not yet swept. Folio's Aggregator Sync workflow fails by design (folio is a new plugin, never added to the shared Data-Wise aggregator 's marketplace.json — aggregator-sync.sh deliberately refuses to silently add unknown plugins; adding folio there is a separate, reviewed decision). CP-4 tasks/ cleanup: craft's tasks/ deleted directly on dev (unprotected); folio's tasks/session-plan-T2.4-CP2.md removed via PRs #15 (dev) and #16 (main) rather than a direct-branch delete (blocked by the permission classifier). Release: https://github.com/Data-Wise/craft/releases/tag/v4.0.0 · PR: https://github.com/Data-Wise/craft/pull/294. Prior: **v4 FOLIO SPLIT MERGED to dev 2026-07-16** — PR #290 (merge-commit `4af10f27d`, phase history preserved for the eventual v4 release tag): craft's docs-authoring surface extracted to the standalone `folio` plugin. **Counts 95/45/8 → 47/40/2.** NOT yet released (v2.61.2 still the current tag); `feature/folio-split` worktree + branch PRESERVED — Phase 4 (folio `/folio:do` + coordinated release, folio first / craft tag LAST) rides it. Phase 3.6 (router consolidations) CLOSED at **1 router of 5 planned**: `code:audit` shipped shrunk 5 cmds→2 (the `audit-router` skill, D12); `orch` (D11), `arch` (D6), and `ci` (D13) all DROPPED after reading the command bodies in full; `plan:feature` stayed excluded (pre-existing D2 lock). Root cause of the 4 drops, recorded in `docs/specs/GRILL-phase-3-6-router-consolidation-2026-07-15.md`: **the grill locked load-bearing decisions from line counts and flag names without reading the bodies** — every family that looked cohesive from surface metrics collapsed on a full read. D13 also withdrew its own planned `repo`-flag normalization as a premise artifact (no validator enforces cross-family flag consistency; the distinguishing test is *find the thing that fails if it's wrong*). Real defects fixed: 4 `category:` metadata mismatches (`ci/{fix,local}.md` declared `code`; `orch/{drive,workflow}.md` declared `orchestrate` against a nonexistent dir) — flagged by `help_file_validator._check_category_mismatch`, live-wired via `docs_update_orchestrator` → `/craft:docs:update`; a repo-wide sweep found the `orch` pair (pre-existing on dev). **Repo-wide category mismatches now 0.** T3.4 command floor 60→40 (`ci.yml:92`): the floor is a mass-deletion guardrail and had gone stale TWICE (86→60) while the real count fell 94→70→47, leaving it ABOVE the surface and hard-failing CI every run; rationale now commented in-file. `tests/test_git_shim_correctness.sh` INVERTED (not deleted) — it asserted `commands/git/*.md` were thin shims, but T3.5.2 completed that migration by deleting all 11, making the assertions unsatisfiable (7 FAIL on the branch, 21/21 on dev); it now guards the migration's END STATE (shims stay deleted · Operations 1–12 survive in `skills/dev/git/SKILL.md` · `issue-check` stays live · no unsanctioned command reappears), verified by 3 planted defects. `/craft:git:issue-check` KEPT as a live command (NOT folded into the git skill, reversing the initial plan): `tests/test_issue_check_unit.py` extracts its `classify_issue()` block from `commands/git/issue-check.md` and `exec()`s it, so the command file is the classifier's single source of truth — the same constraint as `commands/ci/triage.md` (D13). Git's directory-rename detection had silently relocated dev's new command into `skills/dev/git/references/` during the merge, which would have broken that test. Merge resolved 34 conflicts (~30 count-cascade, dev 95/45/8 vs branch 46/40/2) — every tracked count recomputed mechanically via `bump-version.sh --counts-only` rather than hand-resolved; the guard-script conflicts expected in `no-switch-guard.sh`/`install-guards.sh`/`version-check.sh` did NOT materialise. Prior: **PR #291 MERGED to dev 2026-07-16** (squash `88b221da9`): `ci-bash-suites` validator (`.claude-plugin/skills/validation/ci-bash-suites.md`) — closes the **gate-divergence** gap that caused two of #290's three CI blockers. `/craft:check` never ran the shell suites CI invokes directly because pytest doesn't collect them, so the local gate reported green for a surface it never touched; T3.5.2 shipped a regression citing "full pytest 2575 passed" while a CI-required bash suite had been red for days. Derives its suite list FROM `.github/workflows/*.yml` rather than hardcoding (craft has 22 `tests/*.sh`, CI runs **6** — the parse corrected a 4-suite assumption held all session); advisory in default mode, blocking in `release`, per the release-gated validator contract (`CRAFT_MODE` can't distinguish `--for pr` from `--for commit`). Verified by replaying the REAL T3.5.2 bug (pre-fix suite restored from `d41de18ee`) → caught. No count cascade (validators sit outside the counted surface). Also 2026-07-16: two stale claims fixed in `docs/commands.md` (`5034aef7d`) — a dry-run tip saying "46 commands" (the old TOTAL pasted into a dry-run claim; now 27) and "46 commands for documentation generation, validation, and site management" (now 2 — that sentence described a surface craft no longer has). Local `~/.claude/hooks/branch-guard.sh` re-synced via `install-guards.sh` (#290 merged the v4 hook text into dev, so the installed hook predated the repo and was advising a DELETED command, `/craft:git:guard disable branch-guard`) — this cleared the last known local test failure; dogfood 51 passed/0 failed. ⚠️ KNOWN, NOT FIXED: `docs/commands.md:10`'s "17 Categories" list is stale (`discovery-usage` demoted, `orchestrate`→`orch`, no `workflow` dir) but "category" has two defensible definitions that disagree — frontmatter-declared gives 10 (…`smart`), directories give a different 10 (…`utils`), and the current list matches neither; left for a decision rather than guessed at. Prior: Guard-hardening arc SHIPPED to dev 2026-07-15 — PR #287 (cumulative-cwd cd-target resolution, both guard hooks), PR #288 (guards.json write-race mkdir-lock, both writers), PR #289 (plan-orchestrator dispatch hardening); full detail in the 2026-07-15 Session Recap below. Counts unchanged 95/45/8; NOT yet released. Prior: PR #283 SHIPPED to dev 2026-07-14 (squash `057ea6785`): `/craft:git:issue-check ` — before implementing a fix requested by an open GitHub issue, checks whether the issue's premise still holds against current code (structured verdict: valid/moot/unclear, always cited evidence, never cached, advisory-only). Also gates `/craft:orch:drive` with a cheap `#NNN`-citation pre-filter and corrects `/recap`'s pointer copy. Built via `/craft:brainstorm` → `/craft:grill` (9 locked decisions in `docs/specs/GRILL-github-attention-triage-2026-07-14.md`; adversarial-review pass caught and corrected a factually-wrong dogfood target — issue #199 assumed moot, actually still open) → `/craft:plan` (plan-orchestrator, `ORCHESTRATE-github-attention-triage.md`) → `orchestrate-dispatch`. Dispatched agent completed all 5 phases correctly but silently failed to commit Phase 5 or update its own checkboxes before stopping mid-sentence — caught by the hang-detection cross-check (notification text vs. ORCHESTRATE file state), not a code failure; recovered by verifying git log + a fresh full suite run, fixing 5 newly-stale "94 commands" doc refs the count-cascade tooling missed, then committing + correcting checkboxes. PR CI initially failed on a genuine regression (broken relative link in the ORCHESTRATE file's own header, fixed same session) plus the 3 already-known `test_v115_adhd_enhancements` pre-existing failures; merged via `--admin` once confirmed unrelated. Counts 94→95/45/8; NOT yet released (v2.61.2 still current tag). Prior: Docs/website audit COMMITTED to dev 2026-07-09 (`d6aaa9096`) — 264 deleted-command refs stripped across 40 files, 5 whole-docs archived to `docs/archive/deleted-commands-v3/`, `mkdocs build --strict` clean. FOLIO SPLIT PLANNED (new initiative, supersedes dropped Phase 3): grill ledger `docs/specs/GRILL-folio-split-2026-07-09.md` (5 locked: tools+docs-standards scope · caller-based cut · drop DOCS from /do+hub · dup count tooling · history-preserving subtree, craft v4/folio v1) + `docs/plans/ORCHESTRATE-folio-split.md` (5 phases, P0 gates). P0 caller-audit RAN via dynamic Workflow (7 agents, adversarially verified, partition_sound): STAYS = docs:update + docs:changelog + site:deploy-as-shell; MOVES = 26; flagged docs:claude-md:* trio as craft-internal governance (likely stays) — partition NOT yet recorded into the plan docs. EXECUTION STARTED 2026-07-09 ("build auto"): worktree ~/.git-worktrees/craft/feature-folio-split live (T1.1 done, ORCHESTRATE + tasks/ moved to branch root); T1.2 folio repo creation gated on ask. ⚠️ ACCOUNT CI: zero-CI mode active across private repos since 2026-07-09 (craft is public = unaffected/free) — see memory `github-actions-zero-ci-strategy-2026-07` + savant .STATUS for the full record. Prior: Phase 1 native-first prune MERGED to dev 2026-07-09 — PR #279 (squash `4fa44fbb`): pruned 21 dead-namespace commands (115→94, −5533 lines) + excluded dead docs (specs/plans/archive) from the built mkdocs site. ⏹ NATIVE-FIRST THINNING REFUTED: a Stage-2 dynamic workflow (3 pilots × 4-axis gate + cross-plugin routing test) returned NO-GO on Phase 2 — the `do`/`hub` router structurally can't dispatch cross-plugin, AND 3/3 pilot replacements (native `/simplify`, `/code-review`, `agent-skills:ci-cd`) aren't ≥ craft on all 4 axes (weaker/auto-apply, third-party dep, or generate nothing). Phases 2 (cascade thinning) + 3 (`folio` docs/publishing split) DROPPED; folio only revisitable on docs-maintenance merits, grilled fresh. ADR-002 rich-body gate held mid-build: 3 of 24 "safe" deletions (`git:docs:refcard`, `check:gen-validator`, `workflow:insights`) carried unique logic → restored, not deleted (caught by `test_skill_referenced_commands_exist`). Verification gap fixed: pytest passed 2676 but CI's Validate-Plugin-Structure bash suite (`test_git_shim_correctness`) asserted deleted `git/{sync,git-recap}.md` exist → shim array updated (see memory `pytest-doesnt-cover-craft-ci-bash-suites`). Counts now 94/45/8 on dev; NOT yet released — v2.61.2 still the current tag. Full record: `docs/specs/SPEC-craft-native-first-breakup-2026-07-09.md` OUTCOME block. Worktree `feature/craft-native-first-breakup` removed + remote branch deleted 2026-07-09; local branch ref left stale (branch-guard blocks force-delete of squash-merged branch — documented deadlock, harmless). Prior: v2.61.2 SHIPPED 2026-07-09 — PR #275 (dev→main merge, tag v2.61.2) + PR #276 (CI fix): docs/NEWS.md release highlights page, CHANGELOG/VERSION-HISTORY v2.61.1 section, REFCARD.md version box corrected (2.61.0→2.61.2, counts 117→115), exclusions.txt archived-doc path fixed. PATCH bump. Docs Staleness + Markdown Linting GREEN; Validate Plugin Structure + Link Validation pending (known hang). Release: https://github.com/Data-Wise/craft/releases/tag/v2.61.2. Counts unchanged 115/45/8. Prior: v2.61.0 SHIPPED 2026-07-06 — PR #271 (dev→main merge `6c3e2e95`, tag v2.61.0): do.md dead-agent-dispatch removal + 24-doc sweep, `verify-surfaces.sh --report-only`/`--version` + 2 new legs (GitHub release, docs site) + release-rollback runbook, 4 confirmed branch-guard/no-switch-guard gaps closed, quota tooling removed (all from PR #270, squashed to dev pre-release), plus post-merge docs-audit fixes: 2 dead-agent surface-forms PR #270's hyphenated-only grep missed (`docs/index.md`'s feature grid, `skills/orchestrator-resilience/SKILL.md`'s fallback-dispatch example), a docs-staleness false positive (`orch.md`'s mode-table "4 agents"), and backfilled `CHANGELOG.md`/`docs/CHANGELOG.md` `[Unreleased]` entries missed during #270's own PR prep. MINOR bump. Full suite 2707 passed/1 pre-existing failure (`test_roadmap_orchestrator_enhancements`, stale `ROADMAP.md` path predating this release — root-caused and FIXED post-release in `dc6ee28a`, corrected to `docs/archive/ROADMAP.md`; this exact failure had forced `--admin` overrides on PR #270, PR #271, and `main` directly, three releases running). Merged via `--admin` (same pre-existing failure, confirmed by log each time). Release: https://github.com/Data-Wise/craft/releases/tag/v2.61.0 · PR: https://github.com/Data-Wise/craft/pull/271. Downstream: docs site live at v2.61.0 (confirmed via curl), tap formula confirmed v2.61.0 (live raw.githubusercontent fetch, not local clone), Homebrew Release + Aggregator Sync + Craft MCP Release workflows all SUCCESS. `dev` fast-forwarded to `main`'s merge commit, pushed. Counts unchanged 115/45/8. Also this session: grilled and CLOSED the orchestrate token-usage-hooks backlog (captured 2026-07-02) with NO build — two review passes (backend/architecture + interface/CLI-contract lenses) found hook 1 infeasible (no live token-introspection API exists for a prompt-driven orchestrator session), hook 3 structurally uncomputable for an in-progress run; the one surviving feasible piece (`--token-report`) wouldn't have fixed the retroactive-analysis problem that motivated the backlog, and duplicates the accounting-tooling category this same session already removed (quota tooling) — see `docs/specs/GRILL-orchestrate-token-usage-hooks-2026-07-06.md`. Prior: v2.60.0 SHIPPED 2026-07-05 — PR #269 (dev→main merge `763c5e8c`, tag v2.60.0): command-namespace reorganization (full grill-and-implement cycle, `docs/specs/SPEC-command-namespace-reorganization-2026-07-05.md`) — root promotions `/craft:next`/`/craft:done`/`/craft:refine`/`/craft:brief`/`/craft:brainstorm` (heavy real-world use); `orchestrate`→`orch` rename across ~110 files; `ci:` consolidation (`code:ci-fix`/`code:ci-local`→`ci:fix`/`ci:local`); new `docs:generate` router (9 generators, none merged — genuinely distinct logic); new `task:` namespace; `/craft:quota` deleted (folded into `/craft:orch` pre-flight); `code:desktop-watch` deleted (was a pure redirect shim); `adhd-guide` converted to a skill reference. Pre-flight caught and fixed 8 real test failures before release: 6 stale "117 commands" claims (actual 115) across dist/docs files not covered by `bump-version.sh --counts-only`'s file list, one hardcoded pre-rename path in `test_drive_command_in_nav`, and a missing TOC on the newly-moved `adhd-guide.md` (>300 lines, tripped the skill-standards release gate). MINOR bump. Full suite 2707 passed/0 failed/5 skipped; pre-release-check PASSED; main CI GREEN (Craft CI + Deploy Documentation + Documentation Quality); live site confirmed v2.60.0; homebrew-release workflow SUCCESS (tap formula confirmed v2.60.0 via live `gh api`, NOT the stale local `~/projects/dev-tools/homebrew-tap` clone which still read 2.58.0); aggregator PR #19 auto-merged in `Data-Wise/claude-plugins`; post-release sweep ALL CLEAN. Counts 115/45/8. Release: https://github.com/Data-Wise/craft/releases/tag/v2.60.0 · PR: https://github.com/Data-Wise/craft/pull/269. Prior: v2.59.0 SHIPPED 2026-07-04 — PR #260 (dev→main merge, tag v2.59.0): `plugin-audit` skill (44→45, cross-plugin command/skill collision detector, schema + false-positive fixes before merge, #237) + docs-site staleness hardening H1/H2/H3 (`docs.yml` live-site post-deploy version poll, stale-count guard extended to all of `docs/**/*.md`, #259). Counts 116/45/8 (later corrected to 115 in v2.60.0 after the `resume.md` deletion cascade settled). Prior: v2.58.0 SHIPPED 2026-07-02 — PR #251 (dev→main merge, tag v2.58.0): distribution-surface hardening — curl-pipe installer fixed to clone current `main` instead of a frozen v1.16.0 mirror (D1, 11 contract tests); homebrew-tap copy-based install (no symlinks); net-new `mcp/` TS MCP server (craft-mcp Desktop bridge, 3 read-only tools, DXT/.mcpb); esbuild audit-clear. Counts 116/44/8. Prior: v2.57.0 SHIPPED 2026-07-01 — PR #242 (dev→main merge `ec5797e6`, tag v2.57.0): grill attack-angles (#238 — 7 project-agnostic adversarial axes, angle 3 reads target project's own CLAUDE.md/ADRs at runtime; skill count 43→44) + orchestrate-family wave-1 (#239 — deleted fictional 523-line `resume.md` session-teleportation command, command count 117→116; thin-shimmed `orchestrate:plan` to `plan-orchestrator` skill) + doc-impact rubric extension (#241 — 4 new scored types Tutorial/API/Cookbook/Architecture-doc with tiered thresholds + arch-doc double-count subtraction rule + advisory Site Consistency checklist) + orchestrate-dispatch mode (#240 — 3rd `--output` value dispatching a background Agent from the live planning session, with confirm-before-dispatch gate, scoped concurrency cap, failure/hang detection at 2×-effort-estimate window, resumability, scoped `.STATUS` auto-write). MINOR bump (4 feat/refactor PRs). #240/#241 were background-agent dispatches using the very pattern #240 formalizes — monitoring protocol caught 1 self-introduced broken-link regression (a cross-tree link into `~/.claude/...` from this session's own BRAINSTORM doc, fixed on dev) + recovered a killed agent's completed-but-uncommitted Phase 3 work. A CHANGELOG merge-order conflict between #240/#241 (both added `[Unreleased]` entries) was resolved by rebase, keeping both. Pre-release-check initially reported doc-coverage as BLOCKING due to a macOS bash-3.2 `mapfile` incompatibility (false positive — confirmed PASS, 0 gaps, via Homebrew bash 5.x/zsh). Full suite 2072 passed/7 failed/13 errors (documented env-caused baseline, not regressions); pre-release-check PASSED; main CI GREEN (Craft CI + Deploy Documentation + Documentation Quality); live site confirmed v2.57.0; homebrew-release workflow SUCCESS (formula confirmed v2.57.0, desc "116 commands"); post-release sweep ALL CLEAN. Counts 116/44/8. Release: https://github.com/Data-Wise/craft/releases/tag/v2.57.0 · PR: https://github.com/Data-Wise/craft/pull/242. Prior: v2.56.0 SHIPPED 2026-07-01 — PR #234 (dev→main merge `43beb7b3`, tag v2.56.0): token-usage reduction (PR #232 squash `e6c24a27` — orchestrator model pinning, `/refine` 631→42 lines, `/brainstorm` redesign, new `command-skill-token-efficiency` skill) + release-tracking hardening (namespace-refactor token-cost go/no-go measured 68.3% reduction; broken `/usage` scheduled-trigger replaced with `ccusage`+`claude-monitor`; 2 cross-spec factual bugs fixed via a second grill pass). MINOR bump (feat: #232). All CI checks GREEN on first pass — no `--admin` needed (Validate Plugin Structure resolved cleanly this time). Full suite 2089 passed; pre-release-check PASSED; main CI GREEN; live site confirmed v2.56.0; homebrew-release workflow SUCCESS; post-release sweep ALL CLEAN. Counts 117/43/8. Prior: v2.54.0 SHIPPED 2026-06-27 — `/craft:workflow:brief` (117th command): 3-line action block generator (Next step / Watch out for / Connects to) with `--plan`/`--verbose`/`--show-context` flags; `--brief` integrated into `/craft:do` Step 5.5. 29 e2e + 12 dogfood tests. Main CI GREEN; docs deployed; homebrew tap updated (117 commands); post-release sweep CLEAN (getting-started.md fixed); all BLOCK surfaces aligned. Counts: 117 / 39 / 8. Prior: v2.53.0 SHIPPED 2026-06-26 — multi-surface-aware release (surface registry + aggregator CI propagation + Cowork verify leg + `/craft:dist:surfaces`, #218/#184) + docs-staleness fix (docs/plans/ excluded from count-consistency scan). MINOR bump (feat). All tests pass; main CI GREEN; docs deployed; homebrew tap updated (116 commands); post-release sweep ALL CLEAN; aggregator sync wired (all 4 satellite repos). Counts 116/39/8. Prior: v2.52.0 SHIPPED 2026-06-26 — interactive-by-default commands (#215) + default-on test/docs scaffolding (#216) (PR #217 dev→main merge `c5f704f3`, tag v2.52.0, `--admin` on stuck Validate-Plugin-Structure): grill/orchestrate AskUserQuestion model (Recommended-first + per-option consequence) + `--yes`/`--non-interactive`; refine default-on for brainstorm/do/plan:feature/grill (opt-in elsewhere) + `--yes` cascade + orchestrate Step 0.5 structured model; AND brainstorm/plan:feature/grill emit a tier-inferred test-plan + doc-scorer Documentation section BY DEFAULT (`--no-tests`/`--no-docs`; arch:plan/spec-review opt-in via `--tests`/`--docs`), logic in skills (brainstorm-insights/plan-orchestrator, deprecation-trap-safe + guarded), doc-scorer REUSED, lifecycle-split, count-cascade-excluded. Built spec→grill→plan→subagent-driven-TDD (2 plans, 7+8 tasks, per-task spec/quality + opus whole-branch reviews; gates caught a dropped-phrase regression, a refine over-flip, a decorative deprecation-guard sentinel, and a MEMORY.md over-compaction that dropped 37/102 pointers → all fixed). MINOR bump (4 feat). Full suite 1990 passed; pre-release-check PASSED; main CI GREEN; docs deployed (live site 2.52.0); Homebrew Release auto-updated tap; verify-surfaces 6/6 ALIGNED (refreshed local lag: brew formula 2.51→2.52 via `--formula`, marketplace-update-THEN-plugin-update for the Code pin); post-release sweep ALL CLEAN. Counts 115/39/8. Prior: v2.51.0 SHIPPED 2026-06-25 — governance retro (#205) + skill-standards release gate (PRs #211/#212→dev, #214 dev→main merge `180181f8`, tag v2.51.0; CLOSED #205 + #138 via default-branch auto-close): D1 **R09 status-not-drift** governance check (`governance/checks/status_drift.py`, warn) — audits `.STATUS` `version:` vs manifest + SHIPPED/tagged vX.Y.Z vs git tag, fixture `--selftest` + LIVE in `pre-release-check.sh` (this very release was self-gated by it: ".STATUS: version + release claims match" ✓); D3 ADR-003 (release-drift advisory not hard-gate + P7 freeze); skill-standards validator graduated to a **release-tier gate** (`CRAFT_MODE=release` blocks, advisory below) + fixed a latent CRAFT_MODE no-op (LLM-prose command never exported the gating var → gate+version-check silently dead). MINOR bump (2 feat: #211/#212). Full suite 1970 passed; pre-release-check PASSED; main CI GREEN; **verify-surfaces 6/6 ALIGNED** (after refreshing local lag: brew upgrade 2.50→2.51, `claude plugin update` 2.50→2.51, pulled stale `~/projects/dev-tools/homebrew-tap` checkout — the script reads that local clone FIRST, was the only "blocker"); post-release sweep ALL CLEAN. Counts 115/39/8. Process: merged #211/#212 (conflict-free despite branches behind dev — `--stat` looked conflicted but they authored ZERO changes to `.STATUS`/`CLAUDE.md`/`governance.md`; verified via `merge-tree --write-tree`), pushed a local-only gate branch first (data-loss risk), `git:clean` removed 2 worktrees (11+2 squash-merged branch refs left dangling — branch-guard blocks `-D`, harmless). Prior: v2.50.0 SHIPPED 2026-06-25 — v2.49.x issues sprint (PRs #206/#207/#208→dev, #209 dev→main --admin on phantom Validate Plugin Structure, tag v2.50.0): advisory skill-standards validator in `/craft:check` (hot-reload, never fails); homebrew dist-gates `verify_caveats.py`+`post_install_check.py` wired into `/release` Steps 10b/10c (advisory, strict via HOMEBREW_GATE_STRICT) + 10d (BLOCKING aggregator-sync + Cowork verify) (#200/#199); SessionEnd insights facet hook + idempotent installer, per-session-id dedup (#183); ADR-001 Workflow-vs-worktree (#171); +14 e2e/dogfood tests pinning locked decisions. Built via craft:grill (10 decisions D1–D10) → superpowers writing-plans (4 TDD tracks) → 2 dynamic Workflows (impl+verify, ran from dev session under branch-guard bypass, re-protected after). MINOR bump (3 feat: commits). Full suite 1958 passed; pre-release-check PASSED; main CI GREEN; tap+brew+Code+marketplace all 2.50.0 (verify-surfaces 6/6 ALIGNED); post-release sweep CLEAN. KEY INCIDENTS (recovered): (1) #206 squash folded UNPUSHED local-dev base commits → #207/#208 phantom conflicts → rebased --onto origin/dev to untangle (memory squash-folds-unpushed-dev-base-commits; push dev before PRing off it); (2) GitHub comma-list `Closes #200, #199, #183, #171` only auto-closed #200 (+#171 via Track-0 trailer) — each issue needs its OWN keyword; closed #183/#199 manually. Counts 115/39/8. Prior: v2.49.0 SHIPPED 2026-06-23 — skill-standards auditor + /done memory/settings + ADR-002 + release re-split (PR #203 auditor→dev, #204 release dev→main --admin, tag v2.49.0): `/craft:code:skill-standards` batch-audits `skills/**/SKILL.md` vs a vendored copy of Anthropic's standards (score+exit 0/1/2, `--json`/`--markdown`, `--fix` = version-tag-strip + frontmatter-normalize + TOC-stub, never prose; `--refresh-standards`); `/done` Memory Optimize (Step 1.12) + Settings Sync (Step 1.10.5); ADR-002 consolidated `/done` into the `adhd-workflow` skill (`references/done.md` SoT + thin shim); 4 oversized skills split to `references/` (release re-split loss-gated after a lossy first attempt, 0 lines lost) → 39 skills 100/100. Counts 114→115. Release: dev was unsynced with main (v2.48.0 release merge commits) → dev-wins sync merge resolved the PR #204 conflict; full suite 1929 passed; main CI GREEN; docs deployed; tap synced. POST-RELEASE TOOLING (homebrew-tap + savant): root-caused the local Code-registered lag — the formula `post_install` ran `claude plugin update` WITHOUT first `claude plugin marketplace update local-plugins` (no-op on a stale cache); fixed in `homebrew-tap/generator/generate.py` for ALL claude-plugin formulas; reconciled manifest-drift across 6 formulas (rforge-orchestrator's `deprecate!` was at risk of silent un-deprecation; himalaya-mcp 12 skills/22 MCP tools; scholar 28→34; craft `{command_count}` single-source token) gated by regen==committed; added a Formula Drift Guard CI workflow (regen-vs-committed, first run GREEN); savant `CLAUDE.md` 19→25 skills. Memories: post-install-marketplace-refresh-before-update, deprecated-command-rich-body-trap, skill-authoring-tooling-landscape. Prior: v2.48.0 SHIPPED 2026-06-23 (grill + orchestrate Step 0.5 Clarify). Prior: v2.47.0 SHIPPED 2026-06-21 — Governance roadmap COMPLETE (PR #196/#197→dev, #198→main, tag v2.47.0): R04 automated as content-drift (manual→checks/no_drifted_copy.py — installed SKILL.md must be byte-identical to canon; DISTINCT from R07 version-pin, catches a hand-edited body even when the version matches; gates session, vacuous-skip otherwise) + #184 release pre-flight advisory (pre-release-check.sh runs run_rules.py --json, prints RED count, NEVER blocks — gentle-ramp; TestReleaseGuard184 invariant) + live-flow test coverage (e2e soak lifecycle hook→ledger→promote-check + R04-in-audit; dogfood ledger edges). Governance suite 47→59. KEY FINDING (memory r04-content-drift-not-filetype): naïve "real-dir=copy" heuristic was dead (installs are real dirs, no symlinks) → content-drift; always fire a new WARN checker on the live env first (a false positive poisons the soak ledger). #198 merged CLEAN (no --admin). Homebrew tap synced to v2.47.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 / 39 / 8. Governance now: R01–R04+R06 automated · R05 advisory · R07 external · R08; 5 surfaces (visibility·prevention·promotion·cross-repo·release-advisory). Prior: v2.46.0 SHIPPED 2026-06-21 — Governance Phase 2 PR #3 (PR #194→dev, #195→main, tag v2.46.0): soak-then-flip promotion + cross-repo wrapper, completing Phase 2 (visibility · prevention · promotion · cross-repo). soak.py = LOCAL gitignored STATE.json ledger (first_seen/last_seen/last_red per rule) fed by the SessionStart hook each audit; run_rules.py --promote-check (--window 14d, --state) lists warn rules soaked clean long enough to recommend a human warn→error flip (machinery recommends, human promotes; advisory exit 0). run.sh = cross-repo wrapper (consumers invoke the one installed engine, cwd-portable, no drift, R07). 8 dogfood tests (injected today → no flake), 47 governance suite green. Bundled #193: test_performance_cached_run → xfail(strict=False) (wall-clock microbenchmark flaked on shared CI, forced --admin; now a soft signal) — and #195 MERGED CLEAN with NO --admin (the xfail paid off). Homebrew tap synced to v2.46.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 / 39 / 8. R04 automation + #184 release-pre-flight guard remain deferred. Prior: v2.45.0 SHIPPED 2026-06-21 — Governance Phase 2 PR #2 (PR #190→dev, #192→main, tag v2.45.0): SessionStart visibility hook (governance/session_hook.py) — audits the live ~/.claude/skills tree at session open and injects a compact RED-only summary into context via SessionStart additionalContext; silent when clean, mtime-cached, no-op where skills tree absent, never breaks a session on error. Visibility not prevention (SessionStart hooks inject context, can't block). Completes the three-surface model: pre-commit + CI (prevention) · SessionStart (visibility). Implemented direct-to-worktree from the dev session (worktree-path-aware). #192 admin-merged (strict up-to-date BEHIND from the v2.44.0 merge-commit divergence; PR-context full suite GREEN). Homebrew tap synced to v2.45.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 / 39 / 8. Global install of the hook DEFERRED to post-v2.45.0 (point at released plugin path; docs in governance/README.md). PR #3 (soak-then-flip + cross-repo wrapper) + R04 deferred. Also: removed leaked ORCHESTRATE-guard-suite.md (PR #191). Prior: v2.44.0 SHIPPED 2026-06-21 — Governance Phase 2 PR #1 (PR #188→dev, #189→main, tag v2.44.0): R03 automated (manual→script — checks/no_private_in_public_marketplace.py scans a marketplace manifest, fails on a public-marketplace ref to a private/PII repo, denylist {savant}, dual-mode file/dir + good/bad fixtures; closes the last error-rule enforcement gap), pre-commit governance-gate hook (^governance/ → selftest + render --check drift) + CI gate in ci.yml (with explicit "CI does NOT evaluate R01/R07 live-env" note), engine {marketplace} substitution through audit()+selftest() + --marketplace. Built from a draft→adversarial-verify→ultra-finalize workflow chain (verified packet); implemented directly into the worktree from the dev session (branch-guard is worktree-path-aware — corrected a stale assumption). Also corrected stale "not yet wired" governance docs. #189 admin-merged (only failure was the flaky test_performance_cached_run timing test, unrelated to the diff; PR-context full suite GREEN). Homebrew tap synced to v2.44.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 commands / 39 skills / 8 agents. Phase 2 PR #2 (SessionStart hook + soak-then-flip + cross-repo wrapper) NEXT; R04 deferred. Prior: v2.43.0 SHIPPED 2026-06-20 — Skill-Ecosystem Governance (Phase 0) (PR #185→dev, #187→main, tag v2.43.0): policy-as-code under governance/ — RULES.yaml (8 skill-location rules, single source of truth), run_rules.py (audit + --selftest meta-validation, fail-closed), render_rules.py (CLAUDE.md block generation + --check drift gate), portable checkers + good/bad fixtures, guide + Plugin Release Runbook, 24 e2e/dogfood tests + governance pytest marker. PR #185 review hardening (all 5 issues): fail-closed gating (broken checker on error rule exits 1), recursive broken-symlink detection, R01 gates session (not ci) + visible canon-skip, --check exit-2, --selftest surfaces external rules, UTF-8. Homebrew tap synced to v2.43.0, main CI GREEN, post-release sweep ALL CLEAN. Counts: 113 commands / 39 skills / 8 agents. Phase 2 enforced-gates roadmap spec open in PR #186. Prior: v2.42.0 SHIPPED 2026-06-19 — /craft:code:fewer-prompts curated read-only Bash allowlist (PR #181). Prior: v2.41.1 SHIPPED 2026-06-19 — Documentation Coverage (PR #180): 25 tutorial stubs for all Phase 8 commands + broken link fix in TUTORIAL-smart-help.md. Prior: v2.41.0 SHIPPED 2026-06-19 — token-efficiency (quota gate, engine flag, parity runbook). v2.40.0 SHIPPED 2026-06-19 — Guard Suite (PR #174): /craft:git:guard + guards.json registry & ADHD toggle UX, no-switch-guard promoted to scripts/, guard-consistency validator, install-guards.sh. Destructive-restore reconciled to no-switch-guard; branch-guard rm-rf-.git/reset--hard softened to confirm (catastrophic ops stay NON-muteable). 111 commands. Review found 1 CRITICAL (catastrophic-bypass) + 6 issues — ALL fixed pre-merge; +5 registry tests; ci.yml now installs both guards + runs the no-switch suite. Prior: v2.39.0 SHIPPED 2026-06-19 — doc-coverage enforcement (PR #172→dev, #173→main). Prior: v2.38.2 SHIPPED 2026-06-15 — multi-surface release arc (3 patches). v2.38.0: multi-surface engine (verify-surfaces Step 13.6, cache-prune Step 13.7, Data-Wise aggregator marketplace) + branch-guard draft=dev (PRs #158/#159/#160/#161). Adversarial review found 3 HIGH + 6 MEDIUM/LOW bugs → v2.38.1 (PR #163: branch-aware dogfood tests fixed RED main CI; cache-prune data-loss; verify-surfaces git-tag no-op) → v2.38.2 (PR #165: corrupt-surface blocks, symlink GC, arg-parse, subshell). All 3 → main, main CI GREEN, tap + aggregator (Data-Wise/claude-plugins) pinned to 2.38.2, post-release sweep CLEAN. Counts: 112 commands / 39 skills / 8 agents. Prior: v2.37.0 SHIPPED 2026-06-13 — drift-prevention chain + CI triage/watch. Bundled brainstorm items 1-7 (PRs #151/#152/#153/#154/#155): #1 strict-markers guard + #4 workflow recipe discoverability; #7 bump-version categorical-subtotal sweep; #2 CI count-drift tripwire (README live counts); #5 /craft:ci:triage + #6 /craft:ci:watch. PR #156 → main 8c21a54d (merge commit), release tag v2.37.0. Downstream all GREEN: main CI verified, homebrew-release.yml updated tap to v2.37.0 (desc "112 commands", url v2.37.0.tar.gz). Post-release sweep ALL CLEAN (drift tooling paid off). Counts: 112 commands / 39 skills / 8 agents. Prior: v2.36.0 SHIPPED 2026-06-13 (PR #150 → main eb7e143d). +last_session: 2026-08-08 + +## Session Recap — 2026-08-08 — OpenCode MCP bridges + codex/opencode delegation SPEC; teaching-mode removed + +Set up + smoke-tested two Claude-Code-side MCP bridges to OpenCode (`opencode` npm +package, `opencode-async` local build) via direct ndjson stdio probes — user-registered +(`claude mcp add ... -s user`). Documented in new +`docs/tutorials/TUTORIAL-opencode-mcp-plugin.md` (bridge comparison, setup, gotchas, +routing table, mandatory-Monitor-when-delegating section), wired into mkdocs.yml. + +Ran `/craft:brainstorm` with real web research on codex/opencode delegation patterns +→ `BRAINSTORM-codex-opencode-delegation-2026-08-08.md`. Dogfooded the routing rule via +adversarial review of the session's own uncommitted diff on BOTH backends (`codex exec +review --json --uncommitted` and OpenCode/deepseek-v4-pro via `opencode_run`), each +monitored live for token usage; findings from both consolidated into the BRAINSTORM. +Authored `SPEC-codex-opencode-delegation-2026-08-08.md`, then ADHD-simplified it +(TL;DR, routing table, mandatory monitoring recipe). Ran `/craft:grill` (5 branches, +`GRILL-codex-opencode-delegation-2026-08-08.md`) — caught a real design flaw (implied +reuse of the separate `codex` plugin's `codex-rescue` agent, which craft can't call) +plus default-OFF policy (D7) and dogfood-test enforcement (D8); locked decisions +applied back into the SPEC. Final scope narrowed per user request (D9) to exactly 2 +use cases building now — `arch:review --delegate` (adversarial review) and brainstorm +research escalation — with `code:debug`/`code:refactor`/`code:test-gen`/`ci:triage` +explicitly parked, not cancelled. Committed + pushed `9bd5744ff` (6 files). + +Backgrounded an audit of whether the mkdocs "📚 Teaching" section was dead; verdict +REMOVE (teaching-mode commands pruned in the v4 command cuts, no live callers left). +Executed the removal: 34 files (5 insertions / 9567 deletions) — deleted +`docs/teaching/`, teaching tutorials/schema/migration docs, `commands/utils/ +teach_config.py` + 2 sibling teaching utils, `utils/detect_teaching_mode.py`, and 9 +teaching-only test files; stripped teaching cross-links/sections from README.md, +REFCARD.md, `docs/guide/integration-testing.md` (Category 3 removed, counts +renumbered), `commands/test.md`, `pyproject.toml`. A broader `grep -rl` sweep during +execution caught real dead files the background audit had missed (`test_site_publish.py`, +`examples/test_parser.py`, `MANUAL-TESTING-CHECKLIST.md`, 2 more test files) — all +removed in the same batch. `mkdocs build --strict` clean, markdownlint clean, +`validate-counts.sh` unaffected (48/41/2 — teaching was never a counted surface). Full +suite: 2462 passed / 20 failed (all pre-existing, unrelated — confirmed against dev +baseline before dismissing, per `pre-pr-testing.md`) / 15 skipped / 1 xfailed / 1 +xpassed. Committed + pushed `9b53e387e`. + +Re-verified doc health end of session: markdownlint (639 files) 0 errors, `mkdocs +build --strict` clean (only pre-existing INFO-level links into intentionally-excluded +`specs/`/`orch/` dirs). + +**Deferred, not built this session:** the actual `--delegate` flag implementation for +`arch:review` and brainstorm (SPEC's remaining acceptance criteria — scope was +explicitly grill+narrow only, D9). The 4 parked delegation use cases stay parked +until revisited. ## Session Recap — 2026-08-01 — craft-mcp bundling committed; cwd-marker bug found @@ -661,7 +709,9 @@ next occurrence, not a date. | Branch | Path | Purpose | |--------|------|---------| -| `feature/repo-triage` | `~/.git-worktrees/craft/feature-repo-triage` | DISPATCHED 2026-08-07. New `repo-triage` skill (batch issue/worktree/branch triage, thin orchestrator over `issue-check` + `dev/git` Op 4/5). 6-phase ORCHESTRATE, `orchestrate-dispatch` mode. Phase 0 (`dev/git` Op 5 `--dry-run` + output contract) blocks Phases 2-4. Planning: `docs/specs/BRAINSTORM-repo-triage-2026-08-07.md`, `docs/specs/GRILL-repo-triage-2026-08-07.md` (amended post-review), `docs/specs/REVIEW-repo-triage-2026-08-07.md`. | +| _(prior, MERGED)_ | — | `feature/prose-check-hardening` → dev via [#335](https://github.com/Data-Wise/craft/pull/335) (squash `7d792285`), 2026-08-15. Hardens PR #334's prose checks per `SPEC-prose-check-hardening-2026-08-15.md` + `GRILL-prose-check-hardening-2026-08-15.md` (11 locked branches). All 6 phases implemented and verified: F1-F9 fixed (trailer/floor/fix-payload safety, cross-file release-date redesign dropping the git-tag authority, `[e]xclude` record split, box-close discipline, REFCARD prose accuracy). **D11 promotion gate cleared**: check 1 promoted `warning`→`error` after a clean live-repo run (0 findings) across every tracked doc and both real claim sites (`docs/NEWS.md`, `docs/REFCARD.md`), plus a quoted transcript of the check firing on an injected mismatch. Check 2 stays `warning`. Full suite 2476 passed/0 failed (better than the 2462/20-failed dev baseline — includes 2 tripwire-test fixes found during Phase 6 verification, not in the original F1-F9 list: `PROSE_COUNT_TRAILER` was missing `*`, so markdown-bold README badges like `**777 commands**` never matched the broad scan; `bump-version.sh --verify`'s literal `Version: X.Y.Z` grep broke against the Phase 5 REFCARD wording). Independent architecture review (fresh-context agent, `/craft:arch:review` criteria) found 0 violations, 2 minor warnings (function-length cohesion drift, a dead test env var — fixed pre-merge). Rebased once onto `dev` mid-flight for a genuine `.STATUS`/`docs/REFCARD.md` conflict (2 dev-ahead commits from a parallel doc-currency pass), resolved and re-verified before pushing. Worktree removed, local + remote branch deleted; stale `feature/doc-staleness-prose-gaps` remote ref (from #334, never pruned) also deleted after confirming its merge-commit is an ancestor of `dev`. | +| _(prior, MERGED)_ | — | `feature/doc-staleness-prose-gaps` → dev via [#334](https://github.com/Data-Wise/craft/pull/334) (squash `b5de1ae39`), 2026-08-15. Two prose checks in `docs-staleness-check.sh` Phase 7 (release-date claims; count prose scoped to 4 structured line shapes, singular noun form included), ADR-007, and a 12-fixture table-driven harness. Four bugs were caught pre-merge by four different mechanisms — a 4× perf regression by the suite's own 30s timeout, an unparseable-authority false-positive storm by self-review, a release-date off-by-one by `/code-review`, and a pass-2 `[f]ix` that reported success without touching the file by reading adjacent code. **KNOWN, FIXED IN #335 ABOVE:** a high-effort review found 9 further defects (2 HIGH — hyphenated compounds like `command-line` read as counts, and accepting the offered fix rewrites that prose). Merged on explicit instruction, reversing grill decision D5. Worktree removed and local branch deleted 2026-08-15. | +| _(prior, MERGED)_ | — | `feature/repo-triage` → shipped in v4.5.0 (PR [#328](https://github.com/Data-Wise/craft/pull/328) squash to dev, released via [#329](https://github.com/Data-Wise/craft/pull/329)). Worktree removed. Original dispatch note: DISPATCHED 2026-08-07. New `repo-triage` skill (batch issue/worktree/branch triage, thin orchestrator over `issue-check` + `dev/git` Op 4/5). 6-phase ORCHESTRATE, `orchestrate-dispatch` mode. Phase 0 (`dev/git` Op 5 `--dry-run` + output contract) blocks Phases 2-4. Planning: `docs/specs/BRAINSTORM-repo-triage-2026-08-07.md`, `docs/specs/GRILL-repo-triage-2026-08-07.md` (amended post-review), `docs/specs/REVIEW-repo-triage-2026-08-07.md`. | | _(prior, MERGED)_ | — | `feature/prerelease-version-check-hardening` → dev via [#311](https://github.com/Data-Wise/craft/pull/311) (squash), 2026-07-25. Audited the repo for the "grep first bare vX.Y.Z string" bug class that broke the Homebrew release gate twice (v4.2.0, v4.3.0, fixed for `docs/index.md` in #308) — found and fixed 4 more live sites: `.github/workflows/docs.yml`'s live-site poll, `verify-surfaces.sh`'s `resolve_docs_site` (explicitly reused docs.yml's mechanism), `version-sync-precommit.sh`'s `.STATUS` check (a blocking pre-commit hook), and the manual verification steps in `skills/release/SKILL.md`/`downstream-verification.md`; all anchored to the actual version signal (badge slug / `^version:` field) instead of the first bare match. Confirmed 3 other sites already safe, left unchanged. New regression tests for each fix, each verified against a planted-defect positive control. Closes the recurrence risk flagged after v4.3.0. Worktree removed, local + remote branch deleted. | | _(prior, MERGED)_ | — | `feature/issue-309-branch-guard-docs` → dev via [#310](https://github.com/Data-Wise/craft/pull/310) (squash), 2026-07-25. The documented "Claude writes `.claude/allow-once`" one-shot bypass flow is circular when the approved action *is* creating/editing that marker itself (every tool path intercepts it with its own `[CONFIRM]`); added the `CRAFT_GUARD_ALLOW_DEV_EDIT` env-var hint (#281) to the 3 marker-specific confirms only, and reconciled `REFCARD-BRANCH-GUARD.md`/`branch-guard-smart-mode.md` with a caveat pointing at `skills/dev/git/SKILL.md` Operation 10. Code review caught the negative regression test was checking the wrong verbosity encounter (2nd-call brief mode never renders suggestions regardless of content, so it passed vacuously); fixed to assert against the 1st-encounter full-verbosity capture, verified via planted-defect positive control. Closes #309. Worktree removed, local + remote branch deleted. | | _(prior, MERGED)_ | — | `feature/issue-286-reference-scope-guard` → dev via [#306](https://github.com/Data-Wise/craft/pull/306) (squash), 2026-07-25. New advisory-only PreToolUse hook `reference-scope-guard.sh` warning on non-conforming filenames written under `~/.claude/reference/`; wired into `install-guards.sh`; 13-case e2e suite. Closes #286. Worktree removed, local + remote branch deleted. | diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 0b5a00e2b..51b3a2c24 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Full-stack developer toolkit for Claude Code with 48 commands, 2 agents, and 41 skills", - "version": "4.5.0" + "version": "4.6.0" }, "plugins": [ { @@ -16,7 +16,7 @@ "repo": "Data-Wise/craft" }, "description": "Full-stack developer toolkit for Claude Code with 48 commands, 2 agents, and 41 skills", - "version": "4.5.0", + "version": "4.6.0", "author": { "name": "Data-Wise", "email": "dt@stat-wise.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 23262935a..16615754b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "craft", - "version": "4.5.0", + "version": "4.6.0", "description": "Full-stack developer toolkit with integrated workflow automation - 48 commands, 2 agents, 41 skills with standardized dry-run preview mode (29 with --dry-run support). Code, git, site, docs, testing, architecture, CI, distribution, teaching workflows, planning, release pipeline, and ADHD-friendly features including brainstorming, task management, and spec capture", "author": { "name": "Data-Wise", diff --git a/CHANGELOG.md b/CHANGELOG.md index f9729581b..a2689980e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [4.5.0] - 2026-08-08 +## [Unreleased] + +--- + +## [4.6.0] - 2026-08-15 + +### Added + +- **Two prose-staleness checks in `docs-staleness-check.sh` Phase 7** + ([SPEC](docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md), + [ADR-007](docs/adr/ADR-007-pattern-scoped-prose-staleness-gating.md), + [#334](https://github.com/Data-Wise/craft/pull/334), + [#335](https://github.com/Data-Wise/craft/pull/335)) — a release-date check and a count-prose + check scoped to four structured line shapes (version box, TL;DR line, bolded count-summary + badge, structure-table row). The count check reads the **singular** noun form too, closing the + gap that let `CLAUDE.md`'s "8 agent definitions" read GREEN for five minors. + **Hardened same day (#335)** after a high-effort review found 9 defects, 2 HIGH, in #334's + initial ship: the release-date check was redesigned from a single git-tag authority (unreachable + in CI, doesn't exist at release time) to **cross-file consistency** — every release-date claim + in the repo compared against every other claim, majority wins, window only opens on a heading or + version-box line, never a bare prose mention. Promoted to `error` once a clean live-repo run + (0 findings, both real claim sites reached) confirmed it. The count-prose matcher's boundary + class was fixed so hyphenated compounds (`command-line`, `agent-facing`) and markdown-bold + badges (`**48 commands**`) both classify correctly instead of misreading as counts or evading + the broad drift-tripwire scan; the offered auto-fix now anchors to the full matched span so it + can no longer corrupt surrounding prose. Check 2 (count-prose) stays `warning`. +- **Prose-staleness test harness** — `tests/fixtures/prose-staleness/` (18 fixtures across + clean / planted-defect / known-false-positive) and `tests/test_docs_staleness_prose.py` + (33 tests), a table-driven runner asserting on `--json` findings. Includes a guard that every + check keeps a planted-defect fixture, and a live-repo assertion that `count_consistency` stays + clean. + +### Fixed + +- **`docs-staleness-check.sh` pass 2 `[e]xclude` was a silent no-op for every Phase 7/9 finding** + — a finding's `file` field was glued `path:lineno`, breaking `is_pattern_excluded`'s + first-colon split. Split into separate `file`/`locator` fields on the finding record (predates + #334; closed alongside it). +- **`docs-staleness-check.sh` pass 2 `[f]ix` applied nothing while reporting success** — it + printed `-> Fixed` and incremented the counter without touching the file. Pass 1 had the same + bug fixed earlier (BSD `sed -i` exits 0 on no match); the sibling pass was left behind. Both now + share one `apply_line_fix`, which reports success only when the file actually changed and + refuses to execute a non-substitution `fix_detail`. +- **An unparseable release-date authority flagged every claim in the repo** instead of skipping. + The check is now vacuous unless it can establish a comparison, matching the no-tag case. +- **A broad, unscoped count scan shared the same hyphen-boundary bug** as the shaped-line check + (`"7 agents-only"` misread as `"7 agents"`) and, separately, never matched markdown-bold badges + (`**777 commands**`) — both share one boundary-character constant now. +- `docs/specs/REVIEW-repo-triage-2026-08-07.md` pointed at a GRILL doc that `b1c4426e4` had + archived, failing `test_no_broken_links` on every branch since. +- `CLAUDE.md`'s Project Structure table claimed 8 agent definitions (actual: 2 since v4.0.0); + `README.md`'s highlight block still headlined v2.36.0 at v4.5.0, and its tagline carried a + stale hard test count. + +### Removed + +- **Teaching Mode feature** — dead since the v4 command-prune split craft's teaching surface out + to the `scholar` plugin; residual references removed. + +--- + +## [4.5.0] - 2026-08-07 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index b2c58913a..7a9b16649 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ > `/craft:finish` — renamed from `/craft:done` (ADR-006, v4.2.0, breaking) · `orchestrate:drive` — spec-driven autonomous /goal loop · `prompt-refiner` skill + `--refine` flag (9 commands) · `/craft:restore` — combined git+`.STATUS` recap -**Current Version:** v4.5.0 | **Tests:** full suite via `python3 -m pytest tests/` · tiers via `/craft:test ` +**Current Version:** v4.6.0 | **Tests:** full suite via `python3 -m pytest tests/` · tiers via `/craft:test ` > For project details, see `plugin.json` description and `scripts/validate-counts.sh` @@ -85,7 +85,7 @@ performance) · `release` (<300s, thorough). Example: `/craft:code:lint debug`. |-----------|---------| | `commands/` | 48 commands (auto-discovered, one `.md` per command) | | `skills/` | 41 skills (`SKILL.md` pattern) | -| `agents/` | 8 agent definitions | +| `agents/` | 2 agent definitions | | `tests/` | Unit, e2e, dogfood, and integration test suites | | `scripts/` | Release, validation, and maintenance scripts | | `utils/` | Python utilities (`claude_md_sync`, `validate-counts`, etc.) | diff --git a/README.md b/README.md index 554596ee7..7d76db945 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ **main:** [![Craft CI](https://github.com/Data-Wise/craft/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Data-Wise/craft/actions/workflows/ci.yml) [![Deploy Docs](https://github.com/Data-Wise/craft/actions/workflows/docs.yml/badge.svg)](https://github.com/Data-Wise/craft/actions/workflows/docs.yml) **dev:** [![Craft CI](https://github.com/Data-Wise/craft/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/Data-Wise/craft/actions/workflows/ci.yml) [![Documentation Quality](https://github.com/Data-Wise/craft/actions/workflows/docs-quality.yml/badge.svg?branch=dev)](https://github.com/Data-Wise/craft/actions/workflows/docs-quality.yml) -[![Version](https://img.shields.io/badge/version-4.5.0-brightgreen.svg)](https://github.com/Data-Wise/craft/releases) +[![Version](https://img.shields.io/badge/version-4.6.0-brightgreen.svg)](https://github.com/Data-Wise/craft/releases) > **Docs/publishing commands moved to [`folio`](https://github.com/Data-Wise/folio)** — see > [MIGRATION-v4.md](docs/MIGRATION-v4.md) for the old-command → new-location table. > -> **v2.36.0 — Deterministic Workflow Engine** 🚀 -> **48 commands** | **41 skills** | **2 agents** | **1994 tests passing** -> New `/craft:orch:workflow` mode runs coded, fixed-control-flow programs (`parallel`/`pipeline`/`verify`) with schema-gated agents, data-driven fan-out, a run-wide concurrency semaphore, and cached/resumable replay. Three orchestration modes now: improvise (`/craft:orch`), drive (`:drive`), and workflow (`:workflow`). +> **v4.5.0 — repo-triage skill + ecosystem-tool tutorials** 🚀 +> **48 commands** | **41 skills** | **2 agents** +> New `repo-triage` skill batch-grounds open GitHub issues and stale worktrees/branches against current repo state, offering confirmed (never automatic) deletion/closure with itemized evidence and sorting the remainder into grill-ready / plan-ready / defer buckets. Plus 8 ecosystem-tool cheat-sheet tutorials (Codex, Remember, ADHD Mode, Token Optimizer, Security Guidance, Claude HUD, Dropbox, Agent Skills). See [NEWS.md](docs/NEWS.md). A comprehensive production-ready toolkit for Claude Code featuring smart orchestration, ADHD-friendly workflows, multi-agent coordination, and complete documentation coverage. @@ -196,41 +196,6 @@ Commands support execution modes for different use cases: /craft:arch:analyze optimize # Performance analysis ``` -## Teaching Mode - -Specialized workflows for teaching projects with automated validation, semester tracking, and safe publishing. - -**Key Features:** - -- Auto-detection via `.flow/teach-config.yml` configuration -- Content validation (syllabus sections, schedule completeness, assignment files) -- Preview-before-publish workflow (draft → production branches) -- Semester progress tracking with automatic week calculation -- ADHD-friendly dashboards and output - -![Teaching Workflow Demo](docs/demos/teaching-workflow.gif) - -**Quick Start:** - -1. Create `.flow/teach-config.yml` with course info and semester dates -2. Run `/craft:site:validate` to check content completeness -3. Publish with confidence using `/craft:site:publish` - -**Example workflow:** - -```bash -# Check semester progress -/craft:site:progress - -# Validate content before publishing -/craft:site:validate - -# Publish draft → production (with preview) -/craft:site:publish -``` - -**[Full Tutorial](https://data-wise.github.io/craft/tutorials/teaching-mode-setup/)** • **[Config Schema](https://data-wise.github.io/craft/teaching-config-schema/)** - ## Commands (110 total) ### Workflow Commands (12) diff --git a/commands/dist/homebrew.md b/commands/dist/homebrew.md index 3bad8a2b9..33024a5ca 100644 --- a/commands/dist/homebrew.md +++ b/commands/dist/homebrew.md @@ -81,6 +81,16 @@ Detects project type and generates appropriate formula or cask: > **Plugin formulas** are generated by the [homebrew-tap formula generator](https://github.com/Data-Wise/homebrew-tap/tree/main/docs/generator), which supports `libexec_copy_map` for nested source directories and generates the 3-step `post_install` pattern (schema cleanup, timed install, registry sync). +> **Content-only formula edits need a `revision` bump.** Editing a formula's non-version content +> in `homebrew-tap/generator/manifest.json` (e.g. fixing caveat text) without a version bump +> will not be picked up by `brew upgrade` on already-installed machines — Homebrew only checks +> `version`/`revision`, never diffs formula body content. `homebrew-tap`'s "Formula Drift Guard" +> CI check catches this and blocks the PR (`generator/check-revision-bump.sh`); fix by adding/ +> incrementing `"revision": N` on the formula's manifest entry, then +> `python3 generator/generate.py `. This is separate from `/release` Step 10a's +> version+sha256 bump above, which doesn't need a revision field (the version change itself is +> what `brew upgrade` detects). + ### Example Output ``` diff --git a/commands/hub.md b/commands/hub.md index 8c73508b2..097d4284a 100644 --- a/commands/hub.md +++ b/commands/hub.md @@ -171,7 +171,7 @@ Display template: ``` ┌─────────────────────────────────────────────────────────────────────────┐ -│ CRAFT - Full Stack Developer Toolkit v4.5.0 │ +│ CRAFT - Full Stack Developer Toolkit v4.6.0 │ │ [PROJECT_NAME] ([PROJECT_TYPE]) on [GIT_BRANCH] │ │ {stats['total']} commands | {skill_count} skills | {agent_count} agents | {test_count} tests passing │ ├─────────────────────────────────────────────────────────────────────────┤ diff --git a/commands/test.md b/commands/test.md index 6f5eb9ea1..1ecbbe7d6 100644 --- a/commands/test.md +++ b/commands/test.md @@ -69,7 +69,6 @@ Filter tests by tier or domain using pytest markers defined in `pyproject.toml`: | `claude_md` | CLAUDE.md sync, audit, fix | | `branch_guard` | Branch protection hooks | | `orchestrator` | Orchestrator workflows | -| `teaching` | Teaching mode and config | | `commands` | Command parsing, discovery | | `structure` | Plugin structure validation | | `docs` | Documentation link checking | diff --git a/commands/utils/semester_progress.py b/commands/utils/semester_progress.py deleted file mode 100644 index 7c35ba75b..000000000 --- a/commands/utils/semester_progress.py +++ /dev/null @@ -1,387 +0,0 @@ -""" -Semester Progress Calculation Utilities - -Calculates current week, progress percentage, and milestone information -for teaching projects based on semester dates and break schedules. - -Author: Craft Plugin Teaching Workflow -Created: 2026-01-16 -""" - -from __future__ import annotations - -from datetime import datetime, timedelta -from typing import Any - - -def calculate_current_week( - config: dict[str, Any], - current_date: str | None = None -) -> dict[str, Any]: - """ - Calculate current week and semester progress. - - Args: - config: Teaching configuration dictionary with dates structure: - { - "dates": { - "start": "YYYY-MM-DD", - "end": "YYYY-MM-DD", - "breaks": [ - {"name": str, "start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}, - ... - ] - }, - "progress": { - "current_week": "auto" | int # Optional manual override - } - } - current_date: ISO date string (YYYY-MM-DD). If None, uses today. - - Returns: - Dictionary with progress information: - { - "current_week": int, # 1-indexed week number (0 if before start) - "total_weeks": int, # Total weeks in semester - "percent_complete": float, # 0-100 - "on_break": bool, # True if currently on break - "break_name": str | None, # Name of current break or None - "days_elapsed": int, # Days since start (excluding breaks) - "days_remaining": int, # Days until end (excluding future breaks) - "semester_start": str, # ISO date - "semester_end": str, # ISO date - "week_start": str, # ISO date (start of current week) - "week_end": str # ISO date (end of current week) - } - - Examples: - >>> config = { - ... "dates": { - ... "start": "2026-01-19", - ... "end": "2026-05-08", - ... "breaks": [ - ... {"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"} - ... ] - ... } - ... } - >>> result = calculate_current_week(config, "2026-02-10") - >>> result["current_week"] - 4 - >>> result["on_break"] - False - """ - # Parse dates - dates = config.get("dates", {}) - semester_start = datetime.fromisoformat(dates["start"]).date() - semester_end = datetime.fromisoformat(dates["end"]).date() - breaks = dates.get("breaks", []) - - # Parse current date - if current_date is None: - current = datetime.now().date() - else: - current = datetime.fromisoformat(current_date).date() - - # Check for manual override - progress = config.get("progress", {}) - manual_week = progress.get("current_week") - if isinstance(manual_week, int): - # Manual override - calculate other fields but use manual week - total_weeks = _calculate_total_weeks(semester_start, semester_end, breaks) - on_break, break_name = is_on_break(current.isoformat(), breaks) - - # Calculate week boundaries for manual week - week_start, week_end = get_week_boundaries(manual_week, semester_start.isoformat(), breaks) - - # Calculate days - days_elapsed = _count_days_excluding_breaks(semester_start, current, breaks) - days_remaining = _count_days_excluding_breaks(current, semester_end, breaks) - - return { - "current_week": manual_week, - "total_weeks": total_weeks, - "percent_complete": min(100.0, (manual_week / total_weeks * 100) if total_weeks > 0 else 0), - "on_break": on_break, - "break_name": break_name, - "days_elapsed": max(0, days_elapsed), - "days_remaining": max(0, days_remaining), - "semester_start": semester_start.isoformat(), - "semester_end": semester_end.isoformat(), - "week_start": week_start, - "week_end": week_end - } - - # Before semester starts - if current < semester_start: - total_weeks = _calculate_total_weeks(semester_start, semester_end, breaks) - return { - "current_week": 0, - "total_weeks": total_weeks, - "percent_complete": 0.0, - "on_break": False, - "break_name": None, - "days_elapsed": 0, - "days_remaining": _count_days_excluding_breaks(semester_start, semester_end, breaks), - "semester_start": semester_start.isoformat(), - "semester_end": semester_end.isoformat(), - "week_start": semester_start.isoformat(), - "week_end": (semester_start + timedelta(days=6)).isoformat() - } - - # After semester ends - if current > semester_end: - total_weeks = _calculate_total_weeks(semester_start, semester_end, breaks) - final_week_start, final_week_end = get_week_boundaries(total_weeks, semester_start.isoformat(), breaks) - return { - "current_week": total_weeks, - "total_weeks": total_weeks, - "percent_complete": 100.0, - "on_break": False, - "break_name": None, - "days_elapsed": _count_days_excluding_breaks(semester_start, semester_end, breaks), - "days_remaining": 0, - "semester_start": semester_start.isoformat(), - "semester_end": semester_end.isoformat(), - "week_start": final_week_start, - "week_end": final_week_end - } - - # During semester - calculate current week - on_break, break_name = is_on_break(current.isoformat(), breaks) - - # Count days elapsed excluding breaks - days_elapsed = _count_days_excluding_breaks(semester_start, current, breaks) - - # Calculate week number (1-indexed) - current_week = (days_elapsed // 7) + 1 - - # Calculate total weeks - total_weeks = _calculate_total_weeks(semester_start, semester_end, breaks) - - # Cap at total weeks - current_week = min(current_week, total_weeks) - - # Calculate week boundaries - week_start, week_end = get_week_boundaries(current_week, semester_start.isoformat(), breaks) - - # Calculate remaining days - days_remaining = _count_days_excluding_breaks(current, semester_end, breaks) - - # Calculate percent complete - total_days = _count_days_excluding_breaks(semester_start, semester_end, breaks) - percent_complete = (days_elapsed / total_days * 100) if total_days > 0 else 0 - percent_complete = min(100.0, percent_complete) - - return { - "current_week": current_week, - "total_weeks": total_weeks, - "percent_complete": round(percent_complete, 2), - "on_break": on_break, - "break_name": break_name, - "days_elapsed": days_elapsed, - "days_remaining": days_remaining, - "semester_start": semester_start.isoformat(), - "semester_end": semester_end.isoformat(), - "week_start": week_start, - "week_end": week_end - } - - -def _calculate_total_weeks( - semester_start: datetime.date, - semester_end: datetime.date, - breaks: list[dict[str, str]] -) -> int: - """Calculate total weeks in semester excluding breaks.""" - total_days = _count_days_excluding_breaks(semester_start, semester_end, breaks) - # Round up to include partial weeks - return (total_days + 6) // 7 - - -def _count_days_excluding_breaks( - start_date: datetime.date, - end_date: datetime.date, - breaks: list[dict[str, str]] -) -> int: - """ - Count days between start and end, excluding break days. - - Args: - start_date: Start date (inclusive) - end_date: End date (exclusive) - breaks: List of break dictionaries with "start" and "end" keys - - Returns: - Number of non-break days - """ - if start_date >= end_date: - return 0 - - total_days = (end_date - start_date).days - - # Subtract break days that fall within the range - break_days = 0 - for brk in breaks: - break_start = datetime.fromisoformat(brk["start"]).date() - break_end = datetime.fromisoformat(brk["end"]).date() - - # Check if break overlaps with range - overlap_start = max(start_date, break_start) - overlap_end = min(end_date, break_end + timedelta(days=1)) # +1 because end is inclusive - - if overlap_start < overlap_end: - break_days += (overlap_end - overlap_start).days - - return max(0, total_days - break_days) - - -def count_break_days( - start_date: str, - end_date: str, - breaks: list[dict[str, str]] -) -> int: - """ - Count total break days between two dates. - - Args: - start_date: Start date in YYYY-MM-DD format - end_date: End date in YYYY-MM-DD format - breaks: List of break dictionaries with "start" and "end" keys - - Returns: - Number of break days in the range - - Examples: - >>> breaks = [{"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"}] - >>> count_break_days("2026-01-01", "2026-12-31", breaks) - 5 - """ - start = datetime.fromisoformat(start_date).date() - end = datetime.fromisoformat(end_date).date() - - total_days = (end - start).days - days_excluding_breaks = _count_days_excluding_breaks(start, end, breaks) - - return total_days - days_excluding_breaks - - -def is_on_break( - current_date: str, - breaks: list[dict[str, str]] -) -> tuple[bool, str | None]: - """ - Check if a date falls during a break period. - - Args: - current_date: Date to check in YYYY-MM-DD format - breaks: List of break dictionaries with "name", "start", and "end" keys - - Returns: - Tuple of (is_on_break, break_name). break_name is None if not on break. - - Examples: - >>> breaks = [{"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"}] - >>> is_on_break("2026-03-18", breaks) - (True, 'Spring Break') - >>> is_on_break("2026-02-10", breaks) - (False, None) - """ - current = datetime.fromisoformat(current_date).date() - - for brk in breaks: - break_start = datetime.fromisoformat(brk["start"]).date() - break_end = datetime.fromisoformat(brk["end"]).date() - - if break_start <= current <= break_end: - return (True, brk["name"]) - - return (False, None) - - -def get_week_boundaries( - week_num: int, - start_date: str, - breaks: list[dict[str, str]] -) -> tuple[str, str]: - """ - Get start and end dates for a given week number. - - Week 1 starts on semester start date. Each week is 7 days long. - Break weeks are skipped - days during breaks don't count toward week progression. - - Args: - week_num: Week number (1-indexed) - start_date: Semester start date in YYYY-MM-DD format - breaks: List of break dictionaries - - Returns: - Tuple of (week_start_date, week_end_date) in YYYY-MM-DD format - - Examples: - >>> get_week_boundaries(1, "2026-01-19", []) - ('2026-01-19', '2026-01-25') - >>> get_week_boundaries(2, "2026-01-19", []) - ('2026-01-26', '2026-02-01') - """ - semester_start = datetime.fromisoformat(start_date).date() - - if week_num <= 0: - week_num = 1 - - # Calculate how many days to advance (accounting for breaks) - days_to_advance = (week_num - 1) * 7 - - # Start from semester start and advance day by day, skipping breaks - current = semester_start - days_advanced = 0 - - while days_advanced < days_to_advance: - current += timedelta(days=1) - # Check if this day is in a break - on_break, _ = is_on_break(current.isoformat(), breaks) - if not on_break: - days_advanced += 1 - - week_start = current - - # Find week end (6 days after start, skipping breaks) - days_in_week = 0 - current = week_start - - while days_in_week < 6: - current += timedelta(days=1) - on_break, _ = is_on_break(current.isoformat(), breaks) - if not on_break: - days_in_week += 1 - - week_end = current - - return (week_start.isoformat(), week_end.isoformat()) - - -def format_date_range(start: str, end: str) -> str: - """ - Format date range as 'Jan 27 - Feb 2' (ADHD-friendly). - - Args: - start: Start date in YYYY-MM-DD format - end: End date in YYYY-MM-DD format - - Returns: - Formatted date range string - - Examples: - >>> format_date_range("2026-01-27", "2026-02-02") - 'Jan 27 - Feb 2' - >>> format_date_range("2026-03-16", "2026-03-20") - 'Mar 16-20' - """ - start_date = datetime.fromisoformat(start).date() - end_date = datetime.fromisoformat(end).date() - - # Same month - use compact format - if start_date.month == end_date.month: - return f"{start_date.strftime('%b')} {start_date.day}-{end_date.day}" - - # Different months - return f"{start_date.strftime('%b')} {start_date.day} - {end_date.strftime('%b')} {end_date.day}" diff --git a/commands/utils/teach_config.py b/commands/utils/teach_config.py deleted file mode 100644 index 19ec3ce4d..000000000 --- a/commands/utils/teach_config.py +++ /dev/null @@ -1,476 +0,0 @@ -#!/usr/bin/env python3 -""" -Teaching Configuration Parser - -Parses and validates .flow/teach-config.yml files for teaching projects. -Provides robust error handling and comprehensive validation of dates, breaks, -and required fields. - -Usage: - from commands.utils.teach_config import load_teach_config - - config = load_teach_config() - if config: - print(f"Course: {config['course']['number']}") - print(f"Current week: {config['progress']['current_week']}") -""" - -import os -import sys -from datetime import datetime, timedelta -from pathlib import Path -from typing import Dict, List, Optional, Any - -try: - import yaml -except ImportError: - print("Error: PyYAML not installed. Run: pip install pyyaml", file=sys.stderr) - sys.exit(1) - - -# Default values for optional fields -DEFAULTS = { - "deployment": { - "production_branch": "production", - "draft_branch": "draft", - }, - "progress": { - "current_week": "auto", - }, - "validation": { - "required_sections": ["grading", "policies", "objectives", "schedule"], - "strict_mode": True, - }, - "dates": { - "breaks": [], - }, -} - - -# Valid semester values -VALID_SEMESTERS = ["Spring", "Fall", "Winter", "Summer"] - - -def _normalize_config(raw_config: Dict[str, Any]) -> Dict[str, Any]: - """ - Normalize flow-cli schema to craft-native format. - - Non-destructive, idempotent, silent. Uses setdefault() merge pattern: - only fills gaps — never overwrites existing craft-native keys. - Original flow-cli keys stay in dict (other tools may read them). - - Pipeline: load YAML -> _normalize_config() -> apply_defaults() -> validate() - - Field mappings: - semester_info.start_date -> dates.start (if dates.start missing) - semester_info.end_date -> dates.end (if dates.end missing) - semester_info.breaks -> dates.breaks (if dates.breaks missing) - course.name -> course.number (if number missing) - course.full_name -> course.title (if title missing) - course.semester -> capitalize (spring -> Spring) - branches.production -> deployment.production_branch (if missing) - branches.draft -> deployment.draft_branch (if missing) - """ - # --- Course normalization --- - course = raw_config.setdefault("course", {}) - if "number" not in course and "name" in course: - course["number"] = course["name"] - if "title" not in course and "full_name" in course: - course["title"] = course["full_name"] - if "semester" in course and isinstance(course["semester"], str): - sem_word = course["semester"].split()[0] if " " in course["semester"] else course["semester"] - capitalized = sem_word.capitalize() - if capitalized in VALID_SEMESTERS: - course["semester"] = capitalized - - # --- Dates normalization (merge strategy: fill gaps only) --- - si = raw_config.get("semester_info", {}) - if si: - dates = raw_config.setdefault("dates", {}) - if "start" not in dates and "start_date" in si: - dates["start"] = si["start_date"] - if "end" not in dates and "end_date" in si: - dates["end"] = si["end_date"] - if "breaks" not in dates and "breaks" in si: - dates["breaks"] = si["breaks"] - - # --- Deployment normalization --- - branches = raw_config.get("branches", {}) - if branches: - deploy = raw_config.setdefault("deployment", {}) - if "production_branch" not in deploy and "production" in branches: - deploy["production_branch"] = branches["production"] - if "draft_branch" not in deploy and "draft" in branches: - deploy["draft_branch"] = branches["draft"] - - return raw_config - - -def get_config_path(cwd: str = ".") -> Optional[str]: - """ - Find the teaching config file. - - Priority: - 1. .flow/teach-config.yml - 2. teach-config.yml (root) - - Args: - cwd: Current working directory (default: ".") - - Returns: - Absolute path to config file, or None if not found - """ - cwd_path = Path(cwd).resolve() - - # Priority 1: .flow/teach-config.yml - flow_config = cwd_path / ".flow" / "teach-config.yml" - if flow_config.exists(): - return str(flow_config) - - # Priority 2: teach-config.yml (root) - root_config = cwd_path / "teach-config.yml" - if root_config.exists(): - return str(root_config) - - return None - - -def validate_date(date_str: str) -> bool: - """ - Validate date string is in YYYY-MM-DD format with zero padding. - - Args: - date_str: Date string to validate - - Returns: - True if valid, False otherwise - """ - if not isinstance(date_str, str): - return False - - # Check exact format (YYYY-MM-DD with zero padding) - if len(date_str) != 10: - return False - - parts = date_str.split("-") - if len(parts) != 3: - return False - - # Check each part has correct length - if len(parts[0]) != 4 or len(parts[1]) != 2 or len(parts[2]) != 2: - return False - - # Now validate it's a real date - try: - datetime.strptime(date_str, "%Y-%m-%d") - return True - except (ValueError, TypeError): - return False - - -def parse_date(date_str: str) -> Optional[datetime]: - """ - Parse date string to datetime object. - - Args: - date_str: Date string in YYYY-MM-DD format - - Returns: - datetime object or None if invalid - """ - try: - return datetime.strptime(date_str, "%Y-%m-%d") - except (ValueError, TypeError): - return None - - -def validate_breaks(breaks: List[Dict[str, str]], start: str, end: str) -> List[str]: - """ - Validate break periods. - - Checks: - - Each break has required fields (name, start, end) - - Break dates are valid YYYY-MM-DD format - - Break start < break end - - Breaks fall within semester dates - - Breaks don't overlap - - Args: - breaks: List of break dictionaries - start: Semester start date (YYYY-MM-DD) - end: Semester end date (YYYY-MM-DD) - - Returns: - List of error messages (empty if valid) - """ - errors = [] - - if not breaks: - return errors - - semester_start = parse_date(start) - semester_end = parse_date(end) - - if not semester_start or not semester_end: - errors.append("Cannot validate breaks: invalid semester dates") - return errors - - parsed_breaks = [] - - for i, break_period in enumerate(breaks, 1): - # Check required fields - if not isinstance(break_period, dict): - errors.append(f"Break {i}: must be a dictionary") - continue - - if "name" not in break_period: - errors.append(f"Break {i}: missing 'name' field") - if "start" not in break_period: - errors.append(f"Break {i}: missing 'start' field") - if "end" not in break_period: - errors.append(f"Break {i}: missing 'end' field") - - if len(errors) > 0 and f"Break {i}:" in errors[-1]: - continue # Skip further validation if missing required fields - - name = break_period["name"] - break_start_str = break_period["start"] - break_end_str = break_period["end"] - - # Validate date formats - if not validate_date(break_start_str): - errors.append(f"Break '{name}': invalid start date format (expected YYYY-MM-DD)") - continue - if not validate_date(break_end_str): - errors.append(f"Break '{name}': invalid end date format (expected YYYY-MM-DD)") - continue - - break_start = parse_date(break_start_str) - break_end = parse_date(break_end_str) - - # Validate break start <= end (single-day breaks like holidays are valid) - if break_start > break_end: - errors.append(f"Break '{name}': start date must be before or equal to end date") - continue - - # Validate break falls within semester - if break_start < semester_start: - errors.append(f"Break '{name}': starts before semester begins ({start})") - if break_end > semester_end: - errors.append(f"Break '{name}': ends after semester ends ({end})") - - parsed_breaks.append({ - "name": name, - "start": break_start, - "end": break_end, - }) - - # Check for overlapping breaks - parsed_breaks.sort(key=lambda x: x["start"]) - for i in range(len(parsed_breaks) - 1): - current = parsed_breaks[i] - next_break = parsed_breaks[i + 1] - - if current["end"] >= next_break["start"]: - errors.append( - f"Breaks '{current['name']}' and '{next_break['name']}' overlap" - ) - - return errors - - -def apply_defaults(config: Dict[str, Any]) -> Dict[str, Any]: - """ - Apply default values for optional fields. - - Args: - config: Parsed configuration dictionary - - Returns: - Configuration with defaults applied - """ - # Apply deployment defaults - if "deployment" not in config: - config["deployment"] = {} - for key, value in DEFAULTS["deployment"].items(): - if key not in config["deployment"]: - config["deployment"][key] = value - - # Apply progress defaults - if "progress" not in config: - config["progress"] = {} - for key, value in DEFAULTS["progress"].items(): - if key not in config["progress"]: - config["progress"][key] = value - - # Apply validation defaults - if "validation" not in config: - config["validation"] = {} - for key, value in DEFAULTS["validation"].items(): - if key not in config["validation"]: - config["validation"][key] = value - - # Apply dates defaults - if "dates" in config and "breaks" not in config["dates"]: - config["dates"]["breaks"] = DEFAULTS["dates"]["breaks"] - - return config - - -def validate_config(config: Dict[str, Any]) -> List[str]: - """ - Validate configuration structure and values. - - Args: - config: Parsed configuration dictionary - - Returns: - List of error messages (empty if valid) - """ - errors = [] - - # Check required top-level sections - required_sections = ["course", "dates"] - for section in required_sections: - if section not in config: - errors.append(f"Missing required section: '{section}'") - - if errors: - return errors # Can't continue without required sections - - # Validate course section - course = config.get("course", {}) - required_course_fields = ["number", "title", "semester", "year"] - for field in required_course_fields: - if field not in course: - errors.append(f"Missing required field: 'course.{field}'") - - # Validate semester value - if "semester" in course: - if course["semester"] not in VALID_SEMESTERS: - errors.append( - f"Invalid semester: '{course['semester']}' " - f"(must be one of: {', '.join(VALID_SEMESTERS)})" - ) - - # Validate year - if "year" in course: - year = course["year"] - if not isinstance(year, int): - errors.append(f"Invalid year: must be an integer (got {type(year).__name__})") - elif year < 2000 or year > 2100: - errors.append(f"Invalid year: {year} (must be between 2000 and 2100)") - - # Validate dates section - dates = config.get("dates", {}) - required_date_fields = ["start", "end"] - for field in required_date_fields: - if field not in dates: - errors.append(f"Missing required field: 'dates.{field}'") - - if "start" in dates and "end" in dates: - start = dates["start"] - end = dates["end"] - - # Validate date formats - if not validate_date(start): - errors.append(f"Invalid start date format: '{start}' (expected YYYY-MM-DD)") - if not validate_date(end): - errors.append(f"Invalid end date format: '{end}' (expected YYYY-MM-DD)") - - # Validate date order - if validate_date(start) and validate_date(end): - start_dt = parse_date(start) - end_dt = parse_date(end) - if start_dt >= end_dt: - errors.append(f"Semester end date must be after start date") - - # Validate breaks if present - if "breaks" in dates: - break_errors = validate_breaks(dates["breaks"], start, end) - errors.extend(break_errors) - - # Validate progress.current_week - if "progress" in config and "current_week" in config["progress"]: - current_week = config["progress"]["current_week"] - if current_week != "auto" and not isinstance(current_week, int): - errors.append( - f"Invalid current_week: must be 'auto' or integer " - f"(got {type(current_week).__name__})" - ) - if isinstance(current_week, int) and (current_week < 1 or current_week > 52): - errors.append(f"Invalid current_week: {current_week} (must be between 1 and 52)") - - # Validate validation.strict_mode - if "validation" in config and "strict_mode" in config["validation"]: - strict_mode = config["validation"]["strict_mode"] - if not isinstance(strict_mode, bool): - errors.append( - f"Invalid strict_mode: must be boolean " - f"(got {type(strict_mode).__name__})" - ) - - return errors - - -def load_teach_config(cwd: str = ".") -> Optional[Dict[str, Any]]: - """ - Load and validate teaching configuration. - - Finds config file, parses YAML, validates required fields and dates, - and applies defaults for optional fields. - - Args: - cwd: Current working directory (default: ".") - - Returns: - Configuration dictionary with defaults applied, or None if not found/invalid - - Raises: - ValueError: For critical validation failures (invalid dates, etc.) - """ - # Find config file - config_path = get_config_path(cwd) - if not config_path: - return None # No config file found (not an error) - - # Parse YAML - try: - with open(config_path, "r") as f: - config = yaml.safe_load(f) - except yaml.YAMLError as e: - print(f"Warning: Malformed YAML in {config_path}: {e}", file=sys.stderr) - return None - except Exception as e: - print(f"Warning: Failed to read {config_path}: {e}", file=sys.stderr) - return None - - if not isinstance(config, dict): - print(f"Warning: Config must be a YAML dictionary (got {type(config).__name__})", file=sys.stderr) - return None - - # Normalize flow-cli schema to craft-native format - config = _normalize_config(config) - - # Apply defaults - config = apply_defaults(config) - - # Validate configuration - errors = validate_config(config) - if errors: - error_msg = "\n".join(f" - {error}" for error in errors) - raise ValueError(f"Configuration validation failed:\n{error_msg}") - - return config - - -if __name__ == "__main__": - """CLI for testing config parser""" - import json - - config = load_teach_config() - if config: - print(json.dumps(config, indent=2, default=str)) - else: - print("No teaching config found") - sys.exit(1) diff --git a/commands/utils/teaching_validation.py b/commands/utils/teaching_validation.py deleted file mode 100644 index b4cbf7fb4..000000000 --- a/commands/utils/teaching_validation.py +++ /dev/null @@ -1,379 +0,0 @@ -"""Teaching content validation utilities. - -Validates course content for completeness and readiness to publish: -- Syllabus required sections -- Schedule completeness -- Assignment file existence - -Designed for ADHD-friendly, scannable output. -""" - -import os -import re -from dataclasses import dataclass, field -from pathlib import Path -from typing import Optional - - -@dataclass -class ValidationResult: - """Result of teaching content validation. - - Attributes: - valid: Overall validation status (no errors) - errors: Blocking issues that prevent publishing - warnings: Non-blocking issues to review - checks: Detailed check results (name → passed) - """ - valid: bool - errors: list[str] = field(default_factory=list) - warnings: list[str] = field(default_factory=list) - checks: dict[str, bool] = field(default_factory=dict) - - def can_publish(self) -> bool: - """Returns True if no errors (warnings are OK).""" - return len(self.errors) == 0 - - def format_report(self) -> str: - """Format ADHD-friendly validation report.""" - lines = [] - - # Header - status = "✅ READY TO PUBLISH" if self.can_publish() else "❌ BLOCKED" - lines.append(f"\n{'=' * 60}") - lines.append(f"TEACHING CONTENT VALIDATION: {status}") - lines.append('=' * 60) - - # Errors (critical) - if self.errors: - lines.append("\n🚫 ERRORS (must fix before publishing):") - for i, error in enumerate(self.errors, 1): - lines.append(f" {i}. {error}") - - # Warnings (recommended) - if self.warnings: - lines.append("\n⚠️ WARNINGS (recommended to fix):") - for i, warning in enumerate(self.warnings, 1): - lines.append(f" {i}. {warning}") - - # Detailed checks - if self.checks: - lines.append("\n📋 DETAILED CHECKS:") - for name, passed in self.checks.items(): - icon = "✓" if passed else "✗" - lines.append(f" [{icon}] {name}") - - # Summary - lines.append(f"\n{'=' * 60}") - total_checks = len(self.checks) - passed_checks = sum(1 for v in self.checks.values() if v) - lines.append(f"Summary: {passed_checks}/{total_checks} checks passed") - - if self.can_publish(): - lines.append("Status: Ready to publish ✅") - else: - lines.append(f"Status: {len(self.errors)} error(s) blocking publish ❌") - - lines.append('=' * 60 + '\n') - - return '\n'.join(lines) - - -def find_file(cwd: str, patterns: list[str]) -> Optional[Path]: - """Find first matching file from pattern list. - - Args: - cwd: Working directory - patterns: File path patterns to search for - - Returns: - Path to first found file, or None - """ - base = Path(cwd) - for pattern in patterns: - path = base / pattern - if path.exists() and path.is_file(): - return path - return None - - -def validate_syllabus(cwd: str) -> dict[str, bool]: - """Validate syllabus contains required sections. - - Checks for essential syllabus sections using flexible heuristics: - - Grading/assessment information - - Course policies - - Learning objectives/outcomes - - Schedule/calendar - - Args: - cwd: Course directory path - - Returns: - Dict mapping section name → found (bool) - """ - # Find syllabus file - syllabus_patterns = [ - 'syllabus/index.qmd', - 'syllabus.qmd', - 'syllabus/syllabus.qmd', - 'index.qmd', # Sometimes syllabus is the main page - ] - - syllabus_path = find_file(cwd, syllabus_patterns) - - # Default result: all sections missing - sections = { - 'grading': False, - 'policies': False, - 'objectives': False, - 'schedule': False, - } - - if not syllabus_path: - return sections - - # Read syllabus content - try: - content = syllabus_path.read_text(encoding='utf-8').lower() - except Exception: - return sections - - # Check for section patterns (case-insensitive) - # Use word boundaries and context to avoid false positives - # For policies: must be a header starting with "polic" or "course polic" - # Not just any mention of "policy" (like "grading policy") - patterns = { - 'grading': [r'\bgrading\b', r'\bgrade\b', r'\bassessment\b'], - 'policies': [r'^#{1,4}\s+(course\s+)?polic(y|ies)\b'], # Header starting with policy/policies - 'objectives': [r'\bobjective', r'\blearning outcome', r'\bgoal'], - 'schedule': [r'\bschedule\b', r'\bcalendar\b', r'\btimeline\b'], - } - - for section, section_patterns in patterns.items(): - for pattern in section_patterns: - # For policies, use MULTILINE to match line starts - flags = re.MULTILINE if section == 'policies' else 0 - if re.search(pattern, content, flags): - sections[section] = True - break - - return sections - - -def validate_schedule(cwd: str) -> dict: - """Validate schedule completeness. - - Checks that: - - Schedule file exists - - All weeks are defined - - Each week has meaningful content (>20 chars) - - Args: - cwd: Course directory path - - Returns: - Dict with keys: - - total_weeks: Number of weeks found - - complete_weeks: Number of weeks with content - - gaps: List of week numbers missing content - """ - result = { - 'total_weeks': 0, - 'complete_weeks': 0, - 'gaps': [], - } - - # Find schedule file - schedule_patterns = [ - 'schedule.qmd', - 'schedule/index.qmd', - 'calendar.qmd', - ] - - schedule_path = find_file(cwd, schedule_patterns) - - if not schedule_path: - return result - - try: - content = schedule_path.read_text(encoding='utf-8') - except Exception: - return result - - # Find all week sections (flexible patterns) - # Matches: "## Week 1", "### Week 1:", "Week 1 -", etc. - week_pattern = re.compile( - r'^#{1,4}\s*Week\s+(\d+)', - re.MULTILINE | re.IGNORECASE - ) - - matches = list(week_pattern.finditer(content)) - - if not matches: - return result - - result['total_weeks'] = len(matches) - - # Check content for each week - for i, match in enumerate(matches): - week_num = int(match.group(1)) - start_pos = match.end() - - # Find content until next week or end - if i + 1 < len(matches): - end_pos = matches[i + 1].start() - else: - end_pos = len(content) - - week_content = content[start_pos:end_pos].strip() - - # Week is complete if it has meaningful content (>20 chars) - if len(week_content) > 20: - result['complete_weeks'] += 1 - else: - result['gaps'].append(week_num) - - return result - - -def validate_assignments(cwd: str) -> dict: - """Validate assignment files exist. - - Finds assignment references in schedule and checks if files exist. - - Args: - cwd: Course directory path - - Returns: - Dict with keys: - - referenced: List of assignment names found in schedule - - missing: List of assignment files that don't exist - - found: List of assignment files that exist - """ - result = { - 'referenced': [], - 'missing': [], - 'found': [], - } - - # Find schedule file - schedule_patterns = [ - 'schedule.qmd', - 'schedule/index.qmd', - 'calendar.qmd', - ] - - schedule_path = find_file(cwd, schedule_patterns) - - if not schedule_path: - return result - - try: - content = schedule_path.read_text(encoding='utf-8') - except Exception: - return result - - # Find assignment references - # Matches: "HW 1", "HW1", "Assignment 1", "Homework 1", etc. - assignment_patterns = [ - re.compile(r'\bHW\s*(\d+)\b', re.IGNORECASE), - re.compile(r'\bAssignment\s+(\d+)\b', re.IGNORECASE), - re.compile(r'\bHomework\s+(\d+)\b', re.IGNORECASE), - ] - - assignment_numbers = set() - for pattern in assignment_patterns: - for match in pattern.finditer(content): - assignment_numbers.add(int(match.group(1))) - - # Check if assignment files exist - base = Path(cwd) - assignments_dir = base / 'assignments' - - for num in sorted(assignment_numbers): - name = f"HW {num}" - result['referenced'].append(name) - - # Check multiple possible locations - possible_paths = [ - assignments_dir / f'hw-{num}.qmd', - assignments_dir / f'hw{num}.qmd', - assignments_dir / f'hw-{num}' / 'index.qmd', - assignments_dir / f'assignment-{num}.qmd', - assignments_dir / f'homework-{num}.qmd', - ] - - exists = any(p.exists() for p in possible_paths) - - if exists: - result['found'].append(name) - else: - result['missing'].append(name) - - return result - - -def validate_teaching_content(cwd: str) -> ValidationResult: - """Run all teaching content validations. - - Combines syllabus, schedule, and assignment validations into - a single comprehensive result. - - Args: - cwd: Course directory path - - Returns: - ValidationResult with errors, warnings, and detailed checks - """ - result = ValidationResult(valid=True) - - # Validate syllabus - syllabus_sections = validate_syllabus(cwd) - - required_sections = ['grading', 'policies', 'objectives', 'schedule'] - missing_sections = [s for s in required_sections if not syllabus_sections[s]] - - if missing_sections: - result.errors.append( - f"Syllabus missing required sections: {', '.join(missing_sections)}" - ) - result.valid = False - - for section, found in syllabus_sections.items(): - result.checks[f'Syllabus: {section}'] = found - - # Validate schedule - schedule_info = validate_schedule(cwd) - - if schedule_info['total_weeks'] == 0: - result.errors.append("No schedule file found or no weeks defined") - result.valid = False - result.checks['Schedule: exists'] = False - else: - result.checks['Schedule: exists'] = True - - if schedule_info['gaps']: - gap_list = ', '.join(str(w) for w in schedule_info['gaps']) - result.errors.append( - f"Schedule has incomplete weeks (no content): Week {gap_list}" - ) - result.valid = False - - result.checks[f"Schedule: {schedule_info['complete_weeks']}/{schedule_info['total_weeks']} weeks complete"] = len(schedule_info['gaps']) == 0 - - # Validate assignments - assignment_info = validate_assignments(cwd) - - if assignment_info['missing']: - missing_list = ', '.join(assignment_info['missing']) - result.warnings.append( - f"Missing assignment files: {missing_list}" - ) - - if assignment_info['referenced']: - result.checks[f"Assignments: {len(assignment_info['found'])}/{len(assignment_info['referenced'])} found"] = len(assignment_info['missing']) == 0 - else: - result.checks['Assignments: referenced in schedule'] = False - - return result diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e1b42e885..7458e1b50 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,7 +7,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- -## [4.5.0] - 2026-08-08 +## [Unreleased] + +--- + +## [4.6.0] - 2026-08-15 + +### Added + +- **Two prose-staleness checks in `docs-staleness-check.sh` Phase 7** + ([SPEC](specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md), + [ADR-007](adr/ADR-007-pattern-scoped-prose-staleness-gating.md), + [#334](https://github.com/Data-Wise/craft/pull/334), + [#335](https://github.com/Data-Wise/craft/pull/335)) — a release-date check and a count-prose + check scoped to four structured line shapes (version box, TL;DR line, bolded count-summary + badge, structure-table row). The count check reads the **singular** noun form too, closing the + gap that let `CLAUDE.md`'s "8 agent definitions" read GREEN for five minors. + **Hardened same day (#335)** after a high-effort review found 9 defects, 2 HIGH, in #334's + initial ship: the release-date check was redesigned from a single git-tag authority (unreachable + in CI, doesn't exist at release time) to **cross-file consistency** — every release-date claim + in the repo compared against every other claim, majority wins, window only opens on a heading or + version-box line, never a bare prose mention. Promoted to `error` once a clean live-repo run + (0 findings, both real claim sites reached) confirmed it. The count-prose matcher's boundary + class was fixed so hyphenated compounds (`command-line`, `agent-facing`) and markdown-bold + badges (`**48 commands**`) both classify correctly instead of misreading as counts or evading + the broad drift-tripwire scan; the offered auto-fix now anchors to the full matched span so it + can no longer corrupt surrounding prose. Check 2 (count-prose) stays `warning`. +- **Prose-staleness test harness** — `tests/fixtures/prose-staleness/` (18 fixtures across + clean / planted-defect / known-false-positive) and `tests/test_docs_staleness_prose.py` + (33 tests), a table-driven runner asserting on `--json` findings. Includes a guard that every + check keeps a planted-defect fixture, and a live-repo assertion that `count_consistency` stays + clean. + +### Fixed + +- **`docs-staleness-check.sh` pass 2 `[e]xclude` was a silent no-op for every Phase 7/9 finding** + — a finding's `file` field was glued `path:lineno`, breaking `is_pattern_excluded`'s + first-colon split. Split into separate `file`/`locator` fields on the finding record (predates + #334; closed alongside it). +- **`docs-staleness-check.sh` pass 2 `[f]ix` applied nothing while reporting success** — it + printed `-> Fixed` and incremented the counter without touching the file. Pass 1 had the same + bug fixed earlier (BSD `sed -i` exits 0 on no match); the sibling pass was left behind. Both now + share one `apply_line_fix`, which reports success only when the file actually changed and + refuses to execute a non-substitution `fix_detail`. +- **An unparseable release-date authority flagged every claim in the repo** instead of skipping. + The check is now vacuous unless it can establish a comparison, matching the no-tag case. +- **A broad, unscoped count scan shared the same hyphen-boundary bug** as the shaped-line check + (`"7 agents-only"` misread as `"7 agents"`) and, separately, never matched markdown-bold badges + (`**777 commands**`) — both share one boundary-character constant now. +- `docs/specs/REVIEW-repo-triage-2026-08-07.md` pointed at a GRILL doc that `b1c4426e4` had + archived, failing `test_no_broken_links` on every branch since. +- `CLAUDE.md`'s Project Structure table claimed 8 agent definitions (actual: 2 since v4.0.0); + `README.md`'s highlight block still headlined v2.36.0 at v4.5.0, and its tagline carried a + stale hard test count. + +### Removed + +- **Teaching Mode feature** — dead since the v4 command-prune split craft's teaching surface out + to the `scholar` plugin; residual references removed. + +--- + +## [4.5.0] - 2026-08-07 ### Added diff --git a/docs/DEPENDENCY-ARCHITECTURE.md b/docs/DEPENDENCY-ARCHITECTURE.md index b2fd2e6c1..0a23e7825 100644 --- a/docs/DEPENDENCY-ARCHITECTURE.md +++ b/docs/DEPENDENCY-ARCHITECTURE.md @@ -761,5 +761,5 @@ flowchart TB --- **Last Updated**: 2026-02-19 -**Version**: 4.5.0 +**Version**: 4.6.0 **Status**: Production Ready diff --git a/docs/NEWS.md b/docs/NEWS.md index b935d06a6..e7b1801b4 100644 --- a/docs/NEWS.md +++ b/docs/NEWS.md @@ -4,9 +4,30 @@ Release announcements and notable changes for the Craft plugin. --- +## v4.6.0 — prose-staleness checks, hardened same day + +**Released:** 2026-08-15 · **Type:** Minor + +### Highlights + +- **Two new prose-staleness checks in `docs-staleness-check.sh` Phase 7** + ([#334](https://github.com/Data-Wise/craft/pull/334)) — a release-date consistency check and a + count-prose check scoped to four structured line shapes, closing a blind spot where + `CLAUDE.md`'s stale "8 agent definitions" read GREEN for five minor releases. +- **Hardened the same day** ([#335](https://github.com/Data-Wise/craft/pull/335)) after a + high-effort review found 9 defects (2 HIGH) in #334's initial ship. The release-date check was + redesigned from a single git-tag authority — unreachable in CI and nonexistent at release time + — to cross-file consistency, then promoted to `error` once a clean live-repo run confirmed it. + The count-prose matcher's boundary handling was fixed so hyphenated compounds and markdown-bold + badges both classify correctly. +- **Teaching Mode feature removed** — dead since the v4 command-prune split craft's teaching + surface out to the `scholar` plugin. + +--- + ## v4.5.0 — repo-triage skill + ecosystem-tool tutorials -**Released:** 2026-08-08 · **Type:** Minor +**Released:** 2026-08-07 · **Type:** Minor ### Highlights diff --git a/docs/REFCARD.md b/docs/REFCARD.md index 4856e1d89..c58235ac9 100644 --- a/docs/REFCARD.md +++ b/docs/REFCARD.md @@ -4,11 +4,11 @@ ┌─────────────────────────────────────────────────────────────┐ │ CRAFT PLUGIN QUICK REFERENCE │ ├─────────────────────────────────────────────────────────────┤ -│ Version: 4.5.0 (released 2026-07-17) │ -│ Commands: 48 | Agents: 2 | Skills: 40 │ -│ Tests: 2647+ passing │ +│ Version: 4.6.0 (v4.6.0, released 2026-08-15) │ +│ Commands: 48 | Agents: 2 | Skills: 41 │ +│ Tests: python3 -m pytest tests/ │ │ Docs: https://data-wise.github.io/craft/ │ -│ v4.5.0: /craft:restore + post-v4 doc-staleness cleanup │ +│ v4.6.0: prose-staleness checks, hardened same day │ └─────────────────────────────────────────────────────────────┘ ``` @@ -573,7 +573,6 @@ python3 scripts/mermaid-autofix.py docs/ --fix # Auto-fix safe patte - Auto-fix: `/folio:docs:lint --fix` (apply safe fixes) - Path targeting: `/folio:docs:lint docs/guide/` (check specific directories) - Pre-commit integration: Auto-fix on staged markdown -- All 1432 tests passing (100%) - [Release Notes](RELEASE-v2.8.0.md) | [Docs Command Reference](commands/docs.md) **Interactive Documentation Update** @@ -779,8 +778,6 @@ main (production branch) 3. Branch: `gh-pages` / `/ (root)` 4. Save -**See:** [Teaching Workflow Guide](guide/teaching-workflow.md) | [REFCARD-TEACHING.md](reference/REFCARD-TEACHING.md) - ## Check Commands (Pre-Flight Validation) **Main Command:** @@ -1499,7 +1496,6 @@ Comprehensive guides and references: | **[Claude Code 2.1](reference/REFCARD-claude-code-2.1-enhancements.md)** | Smart routing, orchestration hooks (341 lines) | | **[Docs Update](reference/REFCARD-DOCS-UPDATE.md)** | Documentation update workflows (330 lines) | | **[Interactive Commands](reference/REFCARD-INTERACTIVE-COMMANDS.md)** | "Show Steps First" pattern (261 lines) | -| **[Teaching](reference/REFCARD-TEACHING.md)** | Teaching mode workflows (247 lines) | | **[Branch Guard](reference/REFCARD-BRANCH-GUARD.md)** | Branch protection rules and bypass (231 lines) | | **[Release](reference/REFCARD-RELEASE.md)** | Release pipeline reference (211 lines) | | **[Testing](reference/REFCARD-TESTING.md)** | Unified test system, markers, templates (183 lines) | diff --git a/docs/TEACHING-DOCS-INDEX.md b/docs/TEACHING-DOCS-INDEX.md deleted file mode 100644 index 738006514..000000000 --- a/docs/TEACHING-DOCS-INDEX.md +++ /dev/null @@ -1,320 +0,0 @@ -# Teaching Workflow Documentation Index - -Complete documentation for Craft's teaching mode feature. - -![Teaching Workflow Demo](demos/teaching-workflow.gif) - -*Preview → Validate → Publish: Safe course site deployment in 3 minutes (was 15 minutes manually)* - -## Quick Start - -**New to teaching mode?** Start here: - -1. **[Teaching Mode Setup Tutorial](tutorials/teaching-mode-setup.md)** (25 min) - - First-time setup guide - - Step-by-step configuration - - Complete with examples and troubleshooting - -2. **[Teaching Config Schema](teaching-config-schema.md)** (5 min) - - YAML reference guide - - All configuration fields - - Validation rules - -3. **[Migration Guide](teaching-migration.md)** (15 min) - - Migrate from manual workflows - - Before/after patterns - - Team migration checklist - -## Documentation Structure - -### Tutorials (Step-by-Step Guides) - -| Document | Time | Level | Purpose | -|----------|------|-------|---------| -| [Teaching Mode Setup](tutorials/teaching-mode-setup.md) | 25 min | Beginner | First-time setup and configuration | - -**What you'll learn:** - -- Create `.flow/teach-config.yml` configuration -- Enable teaching mode auto-detection -- Run content validation -- Execute first publish workflow -- Track semester progress - -### Reference Guides - -| Document | Time | Level | Purpose | -|----------|------|-------|---------| -| [Teaching Config Schema](teaching-config-schema.md) | 5 min | Beginner | Complete YAML specification | - -**Complete reference for:** - -- Course information (number, title, semester, year) -- Semester dates (start, end, breaks) -- Instructor information (name, email, office hours) -- Deployment configuration (branches, GitHub Pages) -- Progress tracking (current week, auto-calculation) -- Validation rules (required sections, strict mode) - -### Migration Guides - -| Document | Time | Level | Purpose | -|----------|------|-------|---------| -| [Teaching Migration](teaching-migration.md) | 15 min | Intermediate | Transition from manual to automated workflows | - -**Covers:** - -- Before/after workflow comparison -- Step-by-step migration process -- Common migration patterns -- Troubleshooting migration issues -- Team migration checklist - -### Command Reference - -> All 4 below moved to the `folio` plugin as part of the v4.0.0 docs/publishing split — none -> remain in craft. - -| Command | Description | Link | -|---------|-------------|------| -| `/folio:site:publish` | Publish draft → production with preview | [Documentation](commands/site.md) | -| `/folio:site:progress` | Semester progress dashboard | [Documentation](commands/site.md) | -| `/folio:site:check` | Content/config validation (links, deployment readiness) | [Documentation](commands/site.md#foliositecheck) | -| `/folio:site:build` | Build site with teaching mode support | [Documentation](commands/site.md#foliositebuild) | - -## Documentation by Use Case - -### I'm Setting Up for the First Time - -**Start here:** - -1. Read [README Teaching Mode section](https://github.com/Data-Wise/craft#teaching-mode-new-in-v1180) (2 min) -2. Follow [Teaching Mode Setup Tutorial](tutorials/teaching-mode-setup.md) (25 min) -3. Bookmark [Config Schema](teaching-config-schema.md) for reference - -**Key commands:** - -```bash -# Setup -/folio:site:status # Verify detection - -# Validate -/folio:site:check # Check content - -# Publish -/folio:site:publish # Deploy to production -``` - -### I'm Migrating from Manual Workflows - -**Start here:** - -1. Review [Migration Guide](teaching-migration.md) (15 min) -2. Create config file following [Config Schema](teaching-config-schema.md) -3. Test workflows before retiring old scripts - -**Migration checklist:** - -- [ ] Create `.flow/teach-config.yml` -- [ ] Test detection with `/folio:site:status` -- [ ] Run validation and fix errors -- [ ] Test publish workflow on draft -- [ ] Archive old deployment scripts -- [ ] Update team documentation - -### I Need to Configure Something - -**Reference guides:** - -- [Config Schema](teaching-config-schema.md) - All YAML fields and options -- [Validation Rules](teaching-config-schema.md#validation-rules) - Required sections, strict mode -- [Deployment Config](teaching-config-schema.md#deployment-configuration) - Branches, GitHub Pages - -**Common configurations:** - -```yaml -# Basic configuration -course: - number: "STAT 440" - title: "Regression Analysis" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" - -# Add breaks -dates: - breaks: - - name: "Spring Break" - start: "2026-03-16" - end: "2026-03-20" - -# Configure validation -validation: - required_sections: - - grading - - policies - strict_mode: true -``` - -### I'm Having a Problem - -**Troubleshooting resources:** - -1. **Teaching mode not detected** - - See: [Setup Tutorial - Step 2](tutorials/teaching-mode-setup.md#step-2-test-detection-2-min) - - Check file location and YAML syntax - -2. **Validation errors** - - See: [Setup Tutorial - Step 3](tutorials/teaching-mode-setup.md#step-3-validate-content-3-min) - - Fix missing sections or incomplete content - -3. **Publish failures** - - See: [Publish Command - Error Handling](commands/site.md) - - Review merge conflicts, network errors - -4. **Progress calculation wrong** - - See: [Progress Command - Troubleshooting](commands/site.md) - - Verify dates and breaks in config - -5. **Migration issues** - - See: [Migration Guide - Troubleshooting](teaching-migration.md#troubleshooting-migration-issues) - - Handle existing content, branch names, team setup - -### I Want to Understand How It Works - -**Architecture and implementation:** - -**Teaching mode detection:** - -- Checks for `.flow/teach-config.yml` in project root -- Validates YAML schema -- Activates specialized workflows - -**Validation system:** - -- Parses syllabus for required sections -- Checks schedule completeness (all weeks have content) -- Verifies assignment files exist -- Reports errors (block publish) vs warnings (recommend fix) - -**Publish workflow:** - -1. Pre-publish validation -2. Categorized diff preview (critical, content, other) -3. User confirmation (yes, preview diff, cancel) -4. Backup branch creation -5. Fast-forward merge with rollback -6. Deployment verification - -**Progress tracking:** - -- Auto-calculates current week from semester dates -- Accounts for break periods -- Manual override with `--week` flag -- JSON output for scripting - -## Cross-Reference Matrix - -| From | To | Relationship | -|------|----|----| -| README | Setup Tutorial | Quick start → Full guide | -| README | Config Schema | Quick start → Reference | -| Setup Tutorial | Config Schema | Configuration → Specification | -| Setup Tutorial | Publish Command | Workflow → Command details | -| Setup Tutorial | Progress Command | Workflow → Dashboard | -| Setup Tutorial | Migration Guide | First-time → Advanced | -| Migration Guide | Setup Tutorial | Prerequisites | -| Migration Guide | Config Schema | Configuration | -| Config Schema | Setup Tutorial | Examples → Usage | -| Publish Command | Config Schema | Configuration reference | -| Progress Command | Config Schema | Configuration reference | - -## Completeness Checklist - -### Documentation Files - -- [x] README section (Teaching Mode) -- [x] Setup tutorial (tutorials/teaching-mode-setup.md) -- [x] Config schema (teaching-config-schema.md) -- [x] Migration guide (teaching-migration.md) -- [x] Command updates (publish.md, progress.md, build.md) -- [x] Documentation index (this file) - -### Content Coverage - -- [x] Quick start (README) -- [x] First-time setup (tutorial) -- [x] Configuration reference (schema) -- [x] Migration from manual (migration guide) -- [x] Common workflows (tutorial + migration) -- [x] Troubleshooting (all documents) -- [x] Error messages (command docs) -- [x] Examples (all documents) - -### Style Consistency - -- [x] TL;DR sections -- [x] Time estimates -- [x] Difficulty levels -- [x] ADHD-friendly formatting -- [x] Code examples -- [x] Visual hierarchy -- [x] Scannable layout -- [x] Cross-references - -### Cross-References - -- [x] README → Tutorial -- [x] README → Schema -- [x] Tutorial → Schema -- [x] Tutorial → Commands -- [x] Tutorial → Migration -- [x] Migration → Tutorial -- [x] Migration → Schema -- [x] Commands → Tutorial -- [x] Commands → Schema -- [x] Commands → Migration - -## Future Enhancements - -**Not in v1.0, but planned:** - -- [ ] LMS integration (Canvas, Moodle) -- [ ] Assignment due date parsing from schedule -- [ ] Lecture completion tracking -- [ ] Student progress analytics -- [ ] Email digest automation -- [ ] Multi-instructor workflows -- [ ] Course template library -- [ ] Semester rollover automation - -## Feedback - -**Help improve these docs:** - -- [Open an issue](https://github.com/Data-Wise/craft/issues) with suggestions -- Share what worked well -- Report confusing sections -- Suggest additional examples - -## Related Documentation - -**General Craft documentation:** - -- [Craft README](https://github.com/Data-Wise/craft#readme) - Full feature list -- [Quick Start](QUICK-START.md) - General Craft usage -- [ADHD Guide](ADHD-QUICK-START.md) - Neurodivergent-friendly workflows -- [Command Reference](REFCARD.md) - All 48 commands - -**Site commands:** - -- [Site Deploy](commands/site.md#craftsitedeploy) - Direct GitHub Pages deployment -- [Site Check](commands/site.md#foliositecheck) - Health validation - -**Git workflows:** - -- [Git Worktree](guide/worktree-advanced-patterns.md) - Parallel development -- [Git Branch](commands/git.md) - Branch management diff --git a/docs/adr/ADR-007-pattern-scoped-prose-staleness-gating.md b/docs/adr/ADR-007-pattern-scoped-prose-staleness-gating.md new file mode 100644 index 000000000..1e5337731 --- /dev/null +++ b/docs/adr/ADR-007-pattern-scoped-prose-staleness-gating.md @@ -0,0 +1,184 @@ +# ADR-007: Documentation prose staleness is gated by pattern-scoped line shapes + +**Status:** Accepted +**Date:** 2026-08-15 +**Implements:** [`SPEC-doc-staleness-prose-gaps-2026-08-07.md`](../specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md) +**Related:** ADR-003 (release drift is advisory, not a hard gate) — the severity +posture below follows its gentle-ramp precedent. + +## Context + +`scripts/docs-staleness-check.sh` Phase 7 anchors on structured patterns: badge +strings (`version-X.Y.Z`), declared count lines (`N commands`). That is enough to +keep *numbers* honest and nothing else. Four real staleness bugs shipped and sat +live while Phase 7 reported GREEN over every one of them: + +| Bug | Why Phase 7 missed it | +|---|---| +| `docs/REFCARD.md` — version box with the right number, wrong release date | dates were never checked | +| `docs/skills-agents.md` — TL;DR claiming "8 specialized agents" two lines above its own correct "2" | the intervening word broke the `N agents` pattern | +| `CLAUDE.md` — Project Structure row reading "8 agent definitions" for five minors | the scan matches the plural noun only | +| `README.md` — a v2.36.0 highlight block still headlining at v4.5.0 | highlight prose was never checked | + +The obvious wide fix — search all prose for `\d+ agents?` — was tried in review +and rejected: a grep over `docs/**/*.md` returned 90+ hits, the great majority +legitimate. Orchestration mode-limit prose ("2 agents max", "4 agents"), a +tutorial built around an intentionally fictional plugin, and a troubleshooting +page that prints a wrong count *on purpose* to teach the bug all look identical +to a stale total from a regex's point of view. + +## Decision + +Extend Phase 7 in place with **line-shape-scoped** regexes. A count in free prose +is not checked; a count inside one of four enumerated structured shapes is: + +| Shape | What it is | +|---|---| +| `version-box` | lines inside a box-drawing block (`┌` … `└`) | +| `tldr` | a TL;DR summary line | +| `count-summary` | the bolded badge line (`**48 commands** \| **41 skills**`) | +| `structure-table` | a table row whose first cell names a counted directory | + +Inside those shapes only, the singular noun form is checked alongside the plural, +and the `structure-table` shape compares against the type its own first cell +names. The existing 40%-of-expected floor applies to shaped lines too — structured +shapes still carry legitimate non-totals (category subtotals in a reference box, +a bolded subset count, a narrative count about another plugin). + +Release dates for the current version are checked **against each other**, with a +one-day agreement window — not against the git tag's local date, as originally +built. The tag doesn't exist at either point this check actually runs (the +release pipeline writes NEWS/REFCARD dates *before* the tag is created; CI's +checkout has no `fetch-tags`), so a tag-based check only ever fired on a +developer machine that had already pulled the tag for a *previous* release — +never a real gate. Not `.STATUS`'s `release_date:` either — claims legitimately +disagree by a day whenever a release publishes across the UTC boundary (v4.5.0: +NEWS.md 2026-08-08, REFCARD.md 2026-08-07), and a check that flags a correct repo +on its first run does not survive its first release. See the prose-check +hardening's `GRILL-prose-check-hardening-2026-08-15.md` D1 for the full account. + +Constraints held from the BRAINSTORM: no new script (D1), craft only (D2), no +semantic/NLP layer (D3), no external prose tool (D4). + +## Severity + +Split per check, not uniform across the phase — but check 1's `error` is **earned**, +not shipped by default. Both checks emitted `warning` initially. Check 1's +redesign (dropping the git-tag authority for cross-file comparison) and its +promotion to release-blocking could not land in the same change — the only +evidence it was sound would otherwise be tests written alongside it by the +same author in the same sitting, which is not independent evidence. + +**Promoted 2026-08-15.** The gate cleared: `docs-staleness-check.sh --json` +returned zero `count_consistency` findings across every tracked doc, reaching +both real claim sites (`docs/NEWS.md`, `docs/REFCARD.md`), and the check was +proven live by injecting a mismatch and watching it fire — + +``` +release date '2020-01-01' for v4.5.0 disagrees with other claims (majority: 2026-08-07) +``` + +— at `docs/REFCARD.md:7`, then reverted. Check 1 now emits `error`. Check 2 +stays `warning` (see below). + +An earlier version of this section claimed the choice barely mattered — that +`main()` exits 1 for warnings and errors alike, so both already fail +`/craft:check --for release` and `pre-release-check.sh`, and the only difference +was the RED/YELLOW label. **That claim was checked during the prose-check +hardening review (2026-08-15) and found false**, not softened: + +- `pre-release-check.sh:280` runs the whole script as `… || true`, discarding the + exit code — "Check 9: Docs staleness (warn-only, does not block release)". +- `.github/workflows/docs-quality.yml` sets `continue-on-error: true`. +- `skills/release/SKILL.md:143` is explicit: "RED findings block; YELLOW findings + warn but allow proceed." + +No consumer reads the process exit status. **The label is the only gate.** The +severity choice a finding gets *is* the block decision — not a cosmetic split of +an already-blocking outcome. + +Given that, severity now follows demonstrated precision per check rather than a +single phase-wide default: + +- **Check 1 is near-binary after the cross-file redesign** (release-date claims + either agree with each other or they do not), so it blocks — the evidence + gate above cleared 2026-08-15. +- **Check 2 matches prose patterns**, and produced three false-positive defects + in the PR that introduced it alone (a hyphenated-compound false match, a + zero-floor for the smallest count type, and five sub-threshold counts caught + pre-merge) — it has not earned `error` at all. `warning` here follows + ADR-003's gentle-ramp precedent: earn `error` after the check runs clean + across a few real releases, same as check 1's gate but without a fixed + promotion point yet. + +## A failed authority makes a check vacuous, never universal + +Every check here compares documentation against an authority — `plugin.json`'s counts for check 2, +and (after the check-1 redesign below) the other release-date claims in the repo for check 1, no +longer a git tag. When an authority is **missing**, the check skips: fewer than two release-date +claims to compare is the normal state on a feature branch, or in a repo where only one doc names a +date at all. The rule this ADR adds is that when an authority is **present but unusable**, the +check skips too. + +The first build did not do this, back when the authority was a git tag. An unparseable tag date +left an empty accept-window, and an empty window matches nothing, so every release-date claim in +the repo failed at once. One bad input became a repo-wide false-positive storm — the loudest +possible output from the least reliable possible input. The guard survives the redesign: an +unparseable authority (now impossible in practice, since it can only come from a claim string +already validated by the collection regex) still produces an empty window, and an empty window +still rejects a claim rather than accepting it. + +Stated generally, for any check added to this script later: **a check may only report a finding it +can positively establish.** Absence of a usable comparison is not evidence of drift. The failure +modes are not symmetric — a skipped check costs one missed bug, a check that fires on every +document costs the gate its credibility, and a gate nobody trusts gets bypassed. + +## Fixes must be applied, not announced + +A finding carries a `fix_detail`. Both prose checks emit `uncertain`, routing to pass 2's +interactive review rather than pass 1's auto-apply: the surrounding prose is hand-authored, so a +human should see the line before the number changes under it. The `fix_detail` is nonetheless a +real `s/…/…/` substitution rather than a human-readable note, so confirming one actually edits the +file — it swaps the digits only, leaving `agent definitions` intact. + +This is a recurrence guard, not a preference. Pass 1 shipped a version of this bug once already +(BSD `sed -i` exits 0 when nothing matched, so the script reported "Fixed: N items" having +modified nothing on every macOS run) and was fixed for it. Pass 2 kept the same bug in simpler +form — it printed `-> Fixed` and incremented the counter without calling anything at all. Patching +the second site would have left a third to find later, so the applier is now a single shared +function, `apply_line_fix`, which returns true only when the file actually changed and refuses to +execute a `fix_detail` that is not a substitution (Phase 8's `doc-coverage:surface:cmd` markers). + +Same family as the rule above: **do not report a result you have not established.** The vacuous +rule keeps the script from claiming drift it cannot demonstrate; this one keeps it from claiming a +repair it did not perform. + +## Consequences + +**Accepted:** + +- Coverage is exactly as good as the four enumerated shapes. A genuinely new + prose shape requires a code change, not a config change. +- `scripts/config/exclusions.txt` keeps growing. Raised in review, accepted: + it is already craft's mechanism for this class of exception, and shape-scoping + shrinks how fast the list has to grow. +- Two env overrides (`CRAFT_EXPECTED_*`, `CRAFT_RELEASE_DATE`) exist for test + hermeticity. Unset on every production path. + +**Rejected alternatives:** + +- **Vale / `drift` / an LLM-in-CI pass** — dependency weight, and + non-determinism in a release gate is worse than a missed prose bug. +- **A whole-file git-log-touch staleness proxy** — rejected as *falsified*, not + merely unattractive. `bump-version.sh` rewrites `docs/REFCARD.md`'s version + line on every release, so the proxy would report "recently touched, not stale" + on the very file whose staleness motivated this work. A per-line/per-span + version could work; it is real complexity for an advisory signal and is not + being built preemptively. + +## Revisit trigger + +A prose staleness bug that lands **despite** these checks and whose shape cannot +be expressed as a line-shape regex. That, and only that, reopens the D3/D4 +rejection of semantic tooling. A bug that merely needs a fifth shape is a shape, +not a reason to change approach. diff --git a/docs/commands/hub.md b/docs/commands/hub.md index 93b22e9aa..2b3e543de 100644 --- a/docs/commands/hub.md +++ b/docs/commands/hub.md @@ -171,7 +171,7 @@ Display template: ``` ┌─────────────────────────────────────────────────────────────────────────┐ -│ CRAFT - Full Stack Developer Toolkit v4.5.0 │ +│ CRAFT - Full Stack Developer Toolkit v4.6.0 │ │ [PROJECT_NAME] ([PROJECT_TYPE]) on [GIT_BRANCH] │ │ {stats['total']} commands | {skill_count} skills | {agent_count} agents | {test_count} tests passing │ ├─────────────────────────────────────────────────────────────────────────┤ diff --git a/docs/cookbook/common/deploy-course-website.md b/docs/cookbook/common/deploy-course-website.md index bf5705d59..3bb6e1dd0 100644 --- a/docs/cookbook/common/deploy-course-website.md +++ b/docs/cookbook/common/deploy-course-website.md @@ -6,15 +6,13 @@ level: "beginner" time_estimate: "3 minutes" related: - ../../commands/site.md - - ../../guide/teaching-workflow.md - - ../../TEACHING-DOCS-INDEX.md --- # Recipe: Deploy Course Website **Time:** 3 minutes **Level:** Beginner -**Prerequisites:** Teaching mode detected (course site with Quarto/MkDocs/pkgdown) +**Prerequisites:** Course/docs site (Quarto, MkDocs, or pkgdown) ## Problem @@ -51,9 +49,6 @@ I want to safely publish course website updates to GitHub Pages without breaking - Verify all pages load correctly - Test navigation and links -!!! tip "Flow-CLI Config Compatibility" - If your project uses flow-cli's `teach-config.yml` (with `semester_info`, `course.name`, `branches`), Craft reads it natively — no separate config needed. See [Config Schema](../../teaching-config-schema.md#flow-cli-config-compatibility) for details. - ## Explanation `/craft:site:deploy` implements a safe deployment workflow: @@ -129,5 +124,3 @@ I want to safely publish course website updates to GitHub Pages without breaking ## Related - [Site Commands](../../commands/site.md) — Deploy, build, check, and more -- [Teaching Workflow Guide](../../guide/teaching-workflow.md) — Complete teaching mode features -- [Teaching Docs Index](../../TEACHING-DOCS-INDEX.md) — All teaching documentation diff --git a/docs/cookbook/recipes/triage-your-repo.md b/docs/cookbook/recipes/triage-your-repo.md new file mode 100644 index 000000000..50146ba63 --- /dev/null +++ b/docs/cookbook/recipes/triage-your-repo.md @@ -0,0 +1,27 @@ +# Recipe: Triage your repo + +Open issues piling up, worktrees you forgot about, branches you're not sure are safe to +delete. Ground all of it against current repo state before acting on any of it. + +1. Ask naturally — "triage the repo" or "what needs attention here" — or invoke the + `repo-triage` skill directly. +2. It batch-checks: + - **Open GitHub issues** — still valid, or moot against current code? + - **Worktrees** — stale (no commits in N days) or already merged? + - **Branches** — merged-and-safe-to-delete, or still live work? +3. Read the buckets: + - **Confirmed-safe** → offered for deletion/closure, but never automatic — you + confirm each one. + - **Grill-ready** → issues/ideas with enough shape to take straight into + `/craft:grill`. + - **Plan-ready** → already scoped enough for `/craft:plan`. + - **Defer** → needs more context before any of the above. +4. Confirm only what you actually want gone. Nothing is deleted or closed without your + explicit yes. + +> **Why grounded, not just listed:** a plain `gh issue list` or `git worktree list` +> tells you what exists — it doesn't tell you what's still true. repo-triage checks +> each item against the current branch/commit state before bucketing it, so "closed as +> moot" and "still valid" are evidence-backed, not a guess. + +See [`skills/orchestration/repo-triage/SKILL.md`](https://github.com/Data-Wise/craft/blob/dev/skills/orchestration/repo-triage/SKILL.md). diff --git a/docs/getting-started/choose-path.md b/docs/getting-started/choose-path.md index 779e5857a..d2fd898a9 100644 --- a/docs/getting-started/choose-path.md +++ b/docs/getting-started/choose-path.md @@ -57,9 +57,6 @@ Welcome to Craft! Find the path that matches your experience level and goals. - **[Core Workflows](../guide/getting-started.md)** Best practices for documentation, site creation, releases, and development -- **[Teaching Mode](../TEACHING-DOCS-INDEX.md)** - Specialized workflow for managing course websites (80% time reduction!) - ### Common Tasks - **Check code quality:** `/craft:code:lint` or `/craft:ci:local` diff --git a/docs/guide/claude-code-2.1-integration.md b/docs/guide/claude-code-2.1-integration.md index a988e6628..b6327ace9 100644 --- a/docs/guide/claude-code-2.1-integration.md +++ b/docs/guide/claude-code-2.1-integration.md @@ -546,7 +546,6 @@ Result aggregation: < 1 minute ## See Also - [Complexity Scoring Algorithm](complexity-scoring-algorithm.md) - Deep dive into 7 factors -- [Teaching Workflow Guide](teaching-workflow.md) - Specialized workflow for courses - [Orchestrator Documentation](orchestrator.md) - Advanced multi-agent features - [Commands Reference](../commands.md) - All 48 commands diff --git a/docs/guide/integration-testing.md b/docs/guide/integration-testing.md index 173e1de6d..14e86d650 100644 --- a/docs/guide/integration-testing.md +++ b/docs/guide/integration-testing.md @@ -5,7 +5,7 @@ > **TL;DR** (30 seconds) > -> - **What:** Craft includes 44 integration tests across 4 categories (dependency system, orchestrator workflows, teaching workflow, branch guard) +> - **What:** Craft includes 28 integration tests across 3 categories (dependency system, orchestrator workflows, branch guard) > - **Why:** Ensure new features work end-to-end with all dependencies > - **How:** Run `python3 tests/test_integration_*.py` to test each category > - **Next:** Read about [Dependency Management](dependency-management-advanced.md) or [Claude Code 2.1.0 Integration](claude-code-2.1-guide.md) @@ -22,7 +22,6 @@ Craft has comprehensive integration tests that validate the entire system end-to # Run specific category python3 tests/test_integration_dependency_system.py python3 tests/test_integration_orchestrator_workflows.py - python3 tests/test_integration_teaching_workflow.py python3 tests/test_integration_branch_guard.py ``` @@ -133,61 +132,7 @@ test_13_agent_coordination ... ok Ran 13 tests in ~3-8 seconds ``` -### Category 3: Teaching Workflow (16 tests, 2 skipped) - -**File:** `tests/test_integration_teaching_workflow.py` - -**Purpose:** Validates the end-to-end teaching workflow including detection, config normalization, validation, and publish cycle. - -**What It Tests:** - -- ✅ Teaching mode detection (`.flow/teach-config.yml` presence) -- ✅ Configuration parsing and validation -- ✅ Flow-CLI config normalization (`semester_info` → `dates`, `course.name` → `course.number`) -- ✅ Mixed schema merge (craft-native keys win, gaps filled from flow-cli) -- ✅ Semester capitalization (`spring` → `Spring`) -- ✅ Single-day break validation (start == end accepted) -- ✅ Deployment branch normalization (`branches` → `deployment`) -- ✅ Config structure validation and edge cases -- ⏳ Teaching detection module (skipped - requires detect_teaching_mode.py) - -**Components Tested:** - -- `commands/utils/teach_config.py` - Config parsing + `_normalize_config()` -- `commands/utils/detect_teaching_mode.py` - Teaching detection -- `commands/utils/teaching_validation.py` - Content validation -- `commands/site/publish.md` - Publish workflow - -**Run It:** - -```bash -python3 tests/test_integration_teaching_workflow.py -``` - -**Expected Output:** - -``` -test_01_detection_with_config ... skipped (detection module) -test_02_detection_without_config ... skipped (detection module) -test_03_config_parsing ... ok -test_04_teaching_commands_exist ... ok -test_05_teaching_utilities_exist ... ok -test_06_config_structure_validation ... ok -test_branches_to_deployment ... ok -test_craft_native_unchanged ... ok -test_flowcli_schema_loads ... ok -test_full_name_to_title ... ok -test_mixed_schema_merge ... ok -test_name_to_number_mapping ... ok -test_semester_capitalization ... ok -test_single_day_break_valid ... ok -test_invalid_yaml_handling ... ok -test_missing_required_fields ... ok - -Ran 16 tests in ~0.07 seconds (14 passed, 2 skipped) -``` - -### Category 4: Branch Guard (6 tests) +### Category 3: Branch Guard (6 tests) **File:** `tests/test_integration_branch_guard.py` @@ -247,9 +192,6 @@ python3 tests/test_integration_dependency_system.py # Orchestrator tests only python3 tests/test_integration_orchestrator_workflows.py -# Teaching tests only -python3 tests/test_integration_teaching_workflow.py - # Branch guard tests only python3 tests/test_integration_branch_guard.py ``` @@ -323,18 +265,6 @@ The orchestration system enables smart task routing and agent delegation. Tests 9. **Parallelism** - Multiple agents run simultaneously 10. **Coordination** - Agents synchronize and aggregate results -### Teaching Tests Validate - -The teaching workflow system enables course management and publishing. Tests verify: - -1. **Detection** - Finding teaching projects automatically -2. **Config** - Parsing teaching configuration correctly -3. **Progress** - Calculating semester progress accurately -4. **Schedule** - Aligning course schedule with calendar -5. **Content** - Validating syllabus, assignments, schedule -6. **Safety** - Publish workflow prevents mistakes -7. **Deploy** - GitHub Pages integration works - ## Troubleshooting Integration Tests | Issue | Solution | @@ -342,7 +272,6 @@ The teaching workflow system enables course management and publishing. Tests ver | Tests fail with "module not found" | `python3 -m pip install -e .` in craft root | | Permission denied on scripts | `chmod +x scripts/*.sh` | | Tests timeout | Some dependency tests may take 5-10 seconds if tools need checking | -| Skipped tests | Expected for teaching workflow (3 modules not yet implemented) | | Dependency detection fails | Make sure tools being detected are in PATH or installed via homebrew/cargo | ## Key Files Reference @@ -366,13 +295,6 @@ The teaching workflow system enables course management and publishing. Tests ver | Validators | `.claude-plugin/skills/validation/` | Hot-reload validators | | Hooks | `.claude-plugin/hooks/orchestrate-hooks.sh` | Lifecycle hooks | | Orchestrator v2 | `agents/orchestrator-v2.md` | v2.3.0 agent | -| **Teaching** | | | -| Detection | `commands/utils/detect_teaching_mode.py` | Detects teaching projects | -| Config | `commands/utils/teach_config.py` | Parses configuration | -| Validation | `commands/utils/teaching_validation.py` | Validates content | -| Build | `commands/site/build.md` | Build command | -| Publish | `commands/site/publish.md` | Publish workflow | -| Progress | `commands/site/progress.md` | Progress tracking | | **Branch Guard** | | | | Hook script | `scripts/branch-guard.sh` | PreToolUse hook with jq parsing | | Config | `.claude/branch-guard.json` | Per-project branch protection config | @@ -388,15 +310,13 @@ The teaching workflow system enables course management and publishing. Tests ver 3. **Learn related features:** - [Dependency Management Advanced Guide](dependency-management-advanced.md) - Detailed workflow - [Claude Code 2.1.0 Integration Guide](claude-code-2.1-guide.md) - Complexity scoring details - - [Teaching Workflow Guide](teaching-workflow.md) - Course management details ## Summary -Craft's 44 integration tests validate four critical systems: +Craft's 28 integration tests validate three critical systems: - **Dependency System (9 tests)** - Tool detection, installation, and repair - **Orchestrator (13 tests)** - Smart routing, complexity scoring, agent delegation -- **Teaching (16 tests)** - Config normalization, detection, validation, and publishing - **Branch Guard (6 tests)** - Branch protection hooks, config loading, JSON parsing Run them regularly to ensure features continue working end-to-end. All tests pass with 100% success rate. diff --git a/docs/guide/teaching-workflow.md b/docs/guide/teaching-workflow.md deleted file mode 100644 index e8c740920..000000000 --- a/docs/guide/teaching-workflow.md +++ /dev/null @@ -1,845 +0,0 @@ -# Teaching Workflow System - -> **TL;DR**: Preview course changes before publishing, track semester progress, validate content automatically. - -The Teaching Workflow System provides specialized tools for managing course websites with safety, validation, and semester tracking. - -## Teaching Ecosystem - -The teaching workflow spans three tools. Each has clear ownership: - -| Capability | Tool | Command/Skill | -|---|---|---| -| Config & setup | flow-cli | `teach init`, `teach config`, `teach doctor` | -| Content generation | Scholar | `teach lecture`, `teach exam`, etc. (9 commands) | -| Content validation | All three | `teach validate` / `/scholar:validate` / `/folio:site:check` (moved to folio) | -| Deployment | flow-cli | `teach deploy` (history, rollback) | -| Semester tracking | flow-cli | `teach status`, `teach week` | -| Site management | folio (moved from Craft) | `/folio:site:publish`, `/folio:site:progress` | -| Shell speed | flow-cli | All `teach *` commands (<10ms dispatch) | - -### Which tool do I use? - -- **"Generate a lecture/exam/quiz"** — `teach lecture` (routes to Scholar) -- **"Deploy the course site"** — `teach deploy` (flow-cli handles rollback) -- **"Check if my content is ready"** — `teach check` (runs all validators) -- **"What week is it?"** — `teach status` or `tst` alias -- **"Publish with CI safety"** — `/folio:site:publish` (5-step workflow) -- **"See all available commands"** — `teach map` (ecosystem overview) - -### Config normalization - -Flow-cli is the canonical config owner. Its schema uses `semester_info.start_date`/`end_date` and `course.name`. Craft normalizes this silently to its internal format (`dates.start`/`end`, `course.number`) via `_normalize_config()` in `teach_config.py`. No warnings, no migration pressure — craft adapts to flow-cli, not the other way around. - -## Overview - -Teaching mode enables: - -- **Preview-before-publish workflow** - See changes before they go live -- **Branch-aware builds** - Automatically switch branches for preview vs publish -- **Content validation** - Check schedule, syllabus, assignments before deploy -- **Semester progress tracking** - Visual dashboard of course completion -- **Safe deployments** - Prevent publishing broken or incomplete content - -![Teaching Workflow Demo](../demos/teaching-workflow.gif) - -*Demo: Complete teaching workflow from status check to publish (3 minutes vs 15 minutes manually)* - -## Quick Start - -### 1. Enable Teaching Mode - -Create `.flow/teach-config.yml`: - -```yaml -teaching: - enabled: true - - branches: - preview: dev # Branch for testing changes - production: main # Branch for live site - - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -``` - -### 2. Test Your Changes - -```bash -# Build preview (on dev branch) -/folio:site:build -``` - -### 3. Publish to Production - -```bash -# Preview → Validate → Switch to main → Build → Deploy -/folio:site:publish -``` - -## Core Commands - -| Command | Purpose | Teaching Mode Behavior | -|---------|---------|------------------------| -| `/folio:site:build` | Build site | Branch validation, teaching-aware output | -| `/folio:site:publish` | Publish to production | Preview → Validate → Switch → Deploy | -| `/folio:site:progress` | Semester dashboard | Week-by-week completion tracking | -| ask "git status" (dev/git skill) | Git status | Shows deployment context, branch safety | - -## How It Works - -### Teaching Mode Detection - -The system auto-detects teaching mode by checking for: - -1. `.flow/teach-config.yml` exists -2. `teaching.enabled: true` in config -3. Required files exist (schedule, syllabus) - -```python -from utils.detect_teaching_mode import detect_teaching_mode - -mode = detect_teaching_mode() -if mode.is_teaching: - print(f"Teaching: {mode.semester_name}") - print(f"Preview branch: {mode.branches.preview}") - print(f"Production branch: {mode.branches.production}") -``` - -### Preview-Before-Publish Workflow - -```mermaid -flowchart TD - A[Working on dev] -->|/folio:site:build| B[Preview Build] - B --> C{Looks good?} - C -->|No| A - C -->|Yes| D["/folio:site:publish"] - D --> E[Run Validation] - E --> F{Valid?} - F -->|No| G[Show Errors] - G --> A - F -->|Yes| H[Switch to main] - H --> I[Build Production] - I --> J[Deploy to GitHub Pages] - J --> K[Auto-switch back to dev] -``` - -**Key Safety Features:** - -1. **Always preview first** - See changes before publish -2. **Validation checkpoint** - Blocks bad content -3. **Auto-branch management** - Handles git switching -4. **Rollback safety** - Returns to dev after publish - -### Content Validation - -Before publishing, the system checks: - -#### Schedule Validation - -```yaml -# Required fields in schedule.qmd -- week number -- dates (start/end) -- topics -- assignments (if applicable) -``` - -#### Syllabus Validation - -```yaml -# Required sections -- Course description -- Instructor info -- Grading policy -- Schedule overview -``` - -#### Assignment Validation - -```yaml -# For each assignment -- Due date (within semester) -- Points/weight -- Instructions -- Submission method -``` - -**Example validation output:** - -``` -╭─ Content Validation ────────────────────────────────────╮ -│ │ -│ ✓ Schedule: 15 weeks validated │ -│ - Week 1-8: Complete │ -│ - Week 9: Missing assignment due date │ -│ - Week 10-15: Complete │ -│ │ -│ ✓ Syllabus: All required sections present │ -│ │ -│ ✗ Assignments: 1 issue found │ -│ - Assignment 3: Due date (2024-12-20) after semester │ -│ │ -│ ❌ VALIDATION FAILED - Fix issues before publishing │ -│ │ -╰─────────────────────────────────────────────────────────╯ -``` - -### Semester Progress Tracking - -```bash -/folio:site:progress -``` - -Shows: - -- Current week in semester -- Weeks completed vs remaining -- Content status per week -- Upcoming deadlines -- Overall completion percentage - -``` -╭─ Semester Progress: Fall 2024 ──────────────────────────╮ -│ │ -│ Week 8 of 15 (53% complete) │ -│ │ -│ ████████████░░░░░░░░░░░░░░░ │ -│ │ -│ Status by Week: │ -│ Weeks 1-7: ✅ Complete & Published │ -│ Week 8: 🔄 Current (Oct 14-20) │ -│ Weeks 9-15: 📝 Planned │ -│ │ -│ Upcoming: │ -│ - Oct 18: Assignment 2 due │ -│ - Oct 20: Midterm exam │ -│ - Oct 25: Week 9 content publish │ -│ │ -│ Content Health: │ -│ Schedule: ✅ 15/15 weeks defined │ -│ Syllabus: ✅ Complete │ -│ Assignments: ⚠️ 1 needs review │ -│ │ -╰─────────────────────────────────────────────────────────╯ -``` - -## Configuration - -### Full Config Schema - -See [`docs/teaching-config-schema.md`](../teaching-config-schema.md) for complete reference. - -### Common Configurations - -#### Minimal (Required) - -```yaml -teaching: - enabled: true - branches: - preview: dev - production: main - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -``` - -#### Standard (Recommended) - -```yaml -teaching: - enabled: true - - branches: - preview: dev - production: main - - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - weeks: 15 - - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" - assignments_dir: "assignments/" - - validation: - strict: true - check_dates: true - check_links: true - - publishing: - auto_nav_update: true - auto_changelog: true -``` - -#### Advanced (Full Features) - -```yaml -teaching: - enabled: true - - branches: - preview: dev - production: main - - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - weeks: 15 - - breaks: - - name: "Thanksgiving" - start: "2024-11-25" - end: "2024-11-29" - - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" - assignments_dir: "assignments/" - lectures_dir: "lectures/" - - validation: - strict: true - check_dates: true - check_links: true - require_all_weeks: true - - publishing: - auto_nav_update: true - auto_changelog: true - create_backup: true - - progress: - show_percentage: true - show_upcoming: 3 - highlight_current: true -``` - -## Common Workflows - -### Weekly Content Update - -```bash -# 1. Work on dev branch -git checkout dev - -# 2. Update week 8 content -# ... edit schedule.qmd, add lecture notes ... - -# 3. Preview changes -/folio:site:build - -# 4. Check if it looks good -# Review in browser at localhost:8000 - -# 5. Publish to production -/folio:site:publish -``` - -**What happens:** - -1. Validates schedule has Week 8 complete -2. Checks syllabus references Week 8 -3. Switches to `main` branch -4. Builds production site -5. Deploys to GitHub Pages -6. Switches back to `dev` - -### Mid-Semester Review - -```bash -# Check overall progress -/folio:site:progress - -# Validate all content -/folio:site:publish --dry-run --validate-only -``` - -### Semester Setup (First Time) - -```bash -# 1. Create config -cat > .flow/teach-config.yml << 'EOF' -teaching: - enabled: true - branches: - preview: dev - production: main - semester: - name: "Spring 2025" - start_date: "2025-01-13" - end_date: "2025-05-09" - weeks: 15 - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -EOF - -# 2. Test detection -# ask "git status" — folded into the dev/git skill, 2026-07 v4 consolidation - -# 3. Build preview -/folio:site:build -``` - -## Teaching-Aware Commands - -### `/folio:site:build` - -**Standard mode:** - -```bash -/folio:site:build -# Builds MkDocs site -``` - -**Teaching mode:** - -```bash -/folio:site:build -# - Detects current branch (dev/main) -# - Shows teaching context -# - Validates if on production branch -# - Builds with semester metadata -``` - -**Output difference:** - -``` -╭─ Build Site (Teaching Mode) ────────────────────────────╮ -│ │ -│ Course: STAT 545 - Fall 2024 │ -│ Branch: dev (Preview) │ -│ Week: 8 of 15 │ -│ │ -│ Status: Safe to experiment ✅ │ -│ │ -│ Building preview site... │ -│ ✓ Built successfully │ -│ │ -│ Publish: /folio:site:publish │ -│ │ -╰─────────────────────────────────────────────────────────╯ -``` - -### Git status (dev/git skill) - -Folded into the `dev/git` skill (2026-07 v4 consolidation) — ask "git status" -naturally instead of `/craft:git:status`. In teaching mode it still shows -deployment context (production branch, what students see, what you're -editing), safety assessment, and modified files. See -[`skills/dev/git/SKILL.md`](https://github.com/Data-Wise/craft/blob/dev/skills/dev/git/SKILL.md). - -## Troubleshooting - -### Validation Failing - -**Problem:** `/folio:site:publish` fails validation - -**Solutions:** - -1. **Check what's wrong:** - - ```bash - /folio:site:publish --dry-run --validate-only - ``` - -2. **Common issues:** - - - **Missing week**: Add week to schedule.qmd - - **Date out of range**: Check assignment due dates - - **Broken links**: Run `/folio:docs:check-links` - - **Malformed YAML**: Validate teach-config.yml syntax - -3. **Skip validation (emergency only):** - - ```bash - /folio:site:publish --skip-validation - ``` - -### Branch Confusion - -**Problem:** Not sure which branch you're on - -**Solution:** - -```bash -# Teaching-aware status -# ask "git status" (dev/git skill) - -# Shows: -# - Current branch (dev/main) -# - What students see (main content) -# - What you're editing (current branch) -``` - -### Deployment Not Updating - -**Problem:** Published but site not updating - -**Checks:** - -1. **Verify production build:** - - ```bash - git checkout main - /folio:site:build - git push origin main - ``` - -2. **Check GitHub Pages:** - - Repo → Settings → Pages - - Verify source: `gh-pages` branch or `main` branch + `/docs` - -3. **Force rebuild:** - - ```bash - /folio:site:publish --force-rebuild - ``` - -### Week Numbers Off - -**Problem:** Progress shows wrong week - -**Fix:** - -Update semester dates in `teach-config.yml`: - -```yaml -semester: - start_date: "2024-08-26" # First day of Week 1 - weeks: 15 # Total weeks -``` - -## Configuration Reference - -### Complete teach-config.yml Schema - -For detailed configuration options, see [`docs/teaching-config-schema.md`](../teaching-config-schema.md). - -**Key sections:** - -| Section | Required | Purpose | -|---------|----------|---------| -| `teaching.enabled` | Yes | Enable/disable teaching mode | -| `teaching.branches` | Yes | Preview and production branch names | -| `teaching.semester` | Yes | Semester dates, name, weeks | -| `teaching.content` | Yes | Path to schedule and syllabus files | -| `teaching.validation` | No | Validation strictness options | -| `teaching.publishing` | No | Auto-update and backup options | -| `teaching.progress` | No | Dashboard display options | - -### Quick Config Validation - -```bash -# Validate teach-config.yml syntax -# ask "git status" (dev/git skill) - -# This command will: -# - Check if file exists -# - Validate YAML syntax -# - Verify required fields -# - Show any configuration errors -``` - -## Common Failure Scenarios - -### Scenario 1: Accidental Main Branch Modification - -**What happens:** - -- You edit files while on `main` branch -- Publish publishes immediately (no preview) -- Students see incomplete/broken content - -**Prevention:** - -```bash -# Always check your branch first -git branch --show-current -# Should show: dev - -# Use teaching-aware status -# ask "git status" (dev/git skill) -# Will show: ✅ Safe to experiment (if on preview branch) -``` - -**Recovery:** - -```bash -# 1. Revert changes on main -git checkout main -git reset --hard HEAD~1 - -# 2. Move changes to dev -git checkout dev -# Re-apply changes here - -# 3. Preview and publish properly -/folio:site:publish -``` - -### Scenario 2: Semester Dates Misalignment - -**What happens:** - -- Current week shows incorrectly in progress dashboard -- Assignment due dates appear out of order -- Validation rejects valid dates as "out of range" - -**Debug steps:** - -```bash -# Check config dates -cat .flow/teach-config.yml | grep -A 5 "semester:" - -# Check what the system thinks -/folio:site:progress - -# Calculate week number manually -# Today: Jan 17, 2026 -# Semester start: Jan 13, 2025 -# Week = floor((today - start) / 7) + 1 -``` - -**Fix:** - -```yaml -semester: - start_date: "2025-01-13" # Must be first day of Week 1 - end_date: "2025-05-09" # Must be last day of final week - weeks: 15 # Should match actual count -``` - -### Scenario 3: Assignment Due Date Validation Error - -**Error message:** - -``` -✗ Assignment due date (2025-12-20) is after semester end (2025-05-09) -``` - -**Common causes:** - -1. Copy-pasted due date with wrong year -2. Date format confusion (MM-DD vs DD-MM) -3. Semester dates don't match actual schedule - -**Fix:** - -```bash -# Check assignment file -grep -r "due.*date" assignments/ - -# Verify it's within semester -# semester.start_date → semester.end_date - -# Update assignment due date to valid range -``` - -### Scenario 4: Build Fails Silently - -**Symptom:** - -- `/folio:site:build` returns success -- But preview doesn't update -- Old content still visible in browser - -**Diagnosis:** - -```bash -# 1. Check build output -/folio:site:build --verbose - -# 2. Clear cache -rm -rf site/ -rm -rf .mkdocs_cache/ - -# 3. Rebuild -/folio:site:build - -# 4. Check browser cache -# Hard refresh: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Linux) -``` - -### Scenario 5: Config File Not Found - -**Error:** - -``` -❌ Teaching mode enabled but .flow/teach-config.yml not found -``` - -**Solutions:** - -```bash -# Create the directory -mkdir -p .flow - -# Create minimal config -cat > .flow/teach-config.yml << 'EOF' -teaching: - enabled: true - branches: - preview: dev - production: main - semester: - name: "Spring 2025" - start_date: "2025-01-13" - end_date: "2025-05-09" - weeks: 15 - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -EOF - -# Verify -# ask "git status" (dev/git skill) -``` - -## Debug Commands - -```bash -# View full config -cat .flow/teach-config.yml - -# Validate YAML syntax -python3 -m yaml .flow/teach-config.yml - -# Check detection -# ask "git status --verbose" (dev/git skill) - -# Dry-run publish (see what would happen) -/folio:site:publish --dry-run --validate-only - -# Build with verbose output -/folio:site:build --verbose - -# Check git branch (teaching-aware) -# ask "git status" (dev/git skill) -``` - -## Migration Guide - -### From Manual Git Workflow - -**Before:** - -```bash -# Manual process -git checkout main -mkdocs build -mkdocs gh-deploy -git checkout dev -``` - -**After:** - -```bash -# One command -/folio:site:publish -``` - -See [`docs/teaching-migration.md`](../teaching-migration.md) for complete migration guide. - -### From Other Systems - -If you're coming from: - -- **Jekyll**: Quarto is similar, adapt frontmatter -- **Hugo**: Content structure maps 1:1 -- **Wordpress**: Export to markdown first - -Teaching mode works with any static site generator that uses git branches. - -## Advanced Features - -### Custom Validation Rules - -```python -# utils/teaching_validation.py - -def validate_custom_content(config): - """Add your own validation logic""" - - # Example: Check lecture notes exist - for week in range(1, config.semester.weeks + 1): - lecture_file = f"lectures/week-{week:02d}.qmd" - if not Path(lecture_file).exists(): - yield ValidationError( - level="warning", - message=f"Lecture notes missing for Week {week}", - file=lecture_file - ) -``` - -### Semester Templates - -Create templates for rapid course setup: - -```yaml -# templates/teaching/semester-template.yml -teaching: - enabled: true - branches: { preview: dev, production: main } - semester: - weeks: 15 - # Dates filled in per semester - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -``` - -### Automated Publishing - -Set up GitHub Actions to publish on schedule: - -```yaml -# .github/workflows/weekly-publish.yml -name: Weekly Publish -on: - schedule: - - cron: '0 8 * * 1' # Every Monday at 8am - workflow_dispatch: - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Publish week's content - run: /folio:site:publish --auto -``` - -## See Also - -- [Teaching Mode Setup Tutorial](../tutorials/teaching-mode-setup.md) - Step-by-step setup -- [Teaching Config Schema](../teaching-config-schema.md) - Complete config reference -- [Migration Guide](../teaching-migration.md) - Moving from manual workflows -- [Manual Testing Checklist](https://github.com/Data-Wise/craft/blob/main/tests/MANUAL-TESTING-CHECKLIST.md) - QA procedures - -## Impact - -**Measured improvements:** - -- ⏱️ **80% time reduction**: 15 min → 3 min per publish -- 🐛 **Zero production bugs**: Content validation catches issues -- 🎯 **100% confidence**: Preview-before-publish eliminates anxiety -- 📊 **Better planning**: Semester progress shows what's ahead - -Teaching mode transforms course management from error-prone and stressful to automated and reliable. diff --git a/docs/index.md b/docs/index.md index f98d933a1..0377f19af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ [![Docs](https://github.com/Data-Wise/craft/actions/workflows/docs.yml/badge.svg?branch=dev)](https://github.com/Data-Wise/craft/actions/workflows/docs.yml) [![Homebrew Release](https://github.com/Data-Wise/craft/actions/workflows/homebrew-release.yml/badge.svg?branch=dev)](https://github.com/Data-Wise/craft/actions/workflows/homebrew-release.yml) [![Validate Dependencies](https://github.com/Data-Wise/craft/actions/workflows/validate-dependencies.yml/badge.svg?branch=dev)](https://github.com/Data-Wise/craft/actions/workflows/validate-dependencies.yml) -[![Version](https://img.shields.io/badge/version-4.5.0-brightgreen.svg)](https://github.com/Data-Wise/craft/releases) +[![Version](https://img.shields.io/badge/version-4.6.0-brightgreen.svg)](https://github.com/Data-Wise/craft/releases) | Branch | CI | Docs | |--------|----|----- | @@ -16,7 +16,7 @@ > > - **What:** Full-stack developer toolkit with 48 commands, 2 AI agents, and 41 auto-triggered skills > - **Why:** Automate documentation, testing, git workflows, CLAUDE.md management, and site creation with one command -> - **How:** Install via `claude plugin install craft@local-plugins` +> - **How:** Install via `brew install data-wise/tap/craft` > - **Next:** Run `/craft:do "your task"` and let AI route to the best workflow > Full-stack developer toolkit for Claude Code — 48 commands, 2 agents, 41 skills with smart orchestration and ADHD-friendly workflows @@ -25,7 +25,7 @@
-- :rocket:{ .lg .middle } **47 Commands** +- :rocket:{ .lg .middle } **48 Commands** Smart commands, docs, code, testing, git, CI, architecture, distribution (Homebrew), planning, and workflow automation — all in one toolkit. Docs-authoring/site-publishing commands live in the sibling [`folio`](https://github.com/Data-Wise/folio) plugin. @@ -54,11 +54,19 @@ ## Quick Start ```bash -# Install via Claude Code -claude plugin install craft@local-plugins +# Install via Homebrew (recommended) +brew install data-wise/tap/craft -# Or create symlink -ln -s ~/projects/dev-tools/claude-plugins/craft ~/.claude/plugins/craft +# Or via the Data-Wise marketplace, inside Claude Code +claude plugin marketplace add Data-Wise/claude-plugins +claude plugin install craft@data-wise +``` + +**Developing craft itself?** Clone the repo and symlink it in instead: + +```bash +git clone https://github.com/Data-Wise/craft.git ~/projects/dev-tools/craft +ln -s ~/projects/dev-tools/craft ~/.claude/plugins/craft ``` **First command:** @@ -70,7 +78,7 @@ ln -s ~/projects/dev-tools/claude-plugins/craft ~/.claude/plugins/craft The universal `/craft:do` command routes your task to the best workflow automatically. !!! success "Quick Win: Try It Now" - Run `/craft:hub` to see all 47 commands organized by category - takes 5 seconds and shows everything craft can do. + Run `/craft:hub` to see all 48 commands organized by category - takes 5 seconds and shows everything craft can do. ## Feature Highlights @@ -110,7 +118,7 @@ The universal `/craft:do` command routes your task to the best workflow automati ## API Reference -Reference documentation for all 47 Craft commands: +Reference documentation for all 48 Craft commands:
@@ -134,7 +142,7 @@ Reference documentation for all 47 Craft commands: **Quick Links:** -- [All 47 Commands, Indexed by Category](API-REFERENCE-COMMANDS.md) +- [All 48 Commands, Indexed by Category](API-REFERENCE-COMMANDS.md) - [Parameters Quick Reference](reference/COMMAND-PARAMETERS.md) - [Error Recovery Guide](reference/ERROR-SCENARIOS.md) - [Real-World Examples](reference/COMMAND-EXAMPLES.md) @@ -181,7 +189,7 @@ Reference documentation for all 47 Craft commands: - :books:{ .lg .middle } **[Commands Overview](commands/overview.md)** - All 47 commands organized + All 48 commands organized - :sparkles:{ .lg .middle } **[Skills & Agents](skills-agents.md)** @@ -213,7 +221,7 @@ Reference documentation for all 47 Craft commands: | **Site** | 1 | Deploy to GitHub Pages (site build/status/check moved to `folio` in v4.0.0) | | **Total** | **48** | **Complete development workflow coverage** | -!!! info "Latest: v4.5.0 — repo-triage skill + ecosystem-tool tutorials" +!!! info "Latest: v4.6.0 — repo-triage skill + ecosystem-tool tutorials" New `repo-triage` skill batch-grounds open GitHub issues and stale worktrees/branches against current repo state, offering confirmed (never automatic) deletion/closure. 8 new cheat-sheet tutorials cover installed ecosystem tools (Codex, Remember, ADHD Mode, Token Optimizer, diff --git a/docs/reference/REFCARD-DOCS-STALENESS.md b/docs/reference/REFCARD-DOCS-STALENESS.md index a616e7c6c..634d12af1 100644 --- a/docs/reference/REFCARD-DOCS-STALENESS.md +++ b/docs/reference/REFCARD-DOCS-STALENESS.md @@ -45,12 +45,59 @@ | Phase | Name | What It Checks | |-------|------|---------------| | 6 | Nav Completeness | Files in `docs/` missing from `mkdocs.yml` nav; nav entries pointing to missing files | -| 7 | Count Consistency | Stale `N commands`, `N skills`, `N agents` strings across all docs | +| 7 | Count Consistency | Stale `N commands`, `N skills`, `N agents` strings across all docs, plus the two prose checks below | | 8 | Skill/Agent Coverage | Skills and agents not listed in `docs/skills-agents.md` | | 9 | Cross-Doc Freshness | Stale version strings in REFCARDs, stale counts in "See Also" sections, `site_description` drift | --- +## Phase 7 prose checks + +Added 2026-08-15, hardened the same day (`SPEC-prose-check-hardening-2026-08-15.md`) — +[ADR-007](../adr/ADR-007-pattern-scoped-prose-staleness-gating.md), +[SPEC](../specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md). Check 1 emits `error` +(promoted 2026-08-15 — see ADR-007's Severity section for the evidence gate); +check 2 emits `warning`. + +| Check | What It Catches | +|-------|-----------------| +| Release-date claims | A `Released: YYYY-MM-DD` claim on a markdown heading line or a version-box (`┌`…`└`) line — never a bare prose mention — compared against every **other** release-date claim in the repo, not a single external authority. A claim more than one day off the **majority** date (ties break to the later date) is flagged, and the finding names both disagreeing sites. Vacuous with fewer than 2 claims — nothing to compare. | +| Count prose in structured lines | A stale count — **singular or plural** — inside one of four line shapes. Free prose is never checked. | + +The four line shapes: + +| Shape | Matches | +|-------|---------| +| `version-box` | lines inside a `┌` … `└` box-drawing block — closes at the first line with no box-drawing character at all, even without a matching `└`, so a truncated box can't leak into later shapes | +| `tldr` | a line that opens with `TL;DR` (after optional blockquote/emphasis markers) — not merely one that mentions it, so a doc describing this bug isn't itself flagged for the example it quotes | +| `count-summary` | the bolded badge line, e.g. `**48 commands** \| **41 skills**` | +| `structure-table` | a table row whose first cell is a counted directory, e.g. `` \| `agents/` \| `` — compared only against the type that cell names | + +Why shape-scoped: a blanket `N agents?` search over `docs/` returns 90+ hits, nearly +all legitimate (orchestration mode-limit prose, a fictional-plugin tutorial, a +troubleshooting page printing a wrong count on purpose). Shaped lines are additionally +held to the same 40%-of-expected floor as the broad scan, because boxes and badges +still carry category subtotals and subset counts. + +Both prose findings are `uncertain`, so they surface in **Pass 2** (interactive), not Pass 1's +auto-apply — the surrounding prose is hand-authored, so a human sees the line before the number +changes under it. Choosing `[f]ix` there really does edit the file; it swaps the digits only, +leaving wording like `agent definitions` intact. + +Fixtures and the table-driven runner: `tests/fixtures/prose-staleness/` + +`tests/test_docs_staleness_prose.py`. Every check has a `defect/` fixture as its +positive control; a check without one is a rejected change. + +### Test-only environment overrides + +| Variable | Effect | +|----------|--------| +| `CRAFT_EXPECTED_CMDS` / `_SKILLS` / `_AGENTS` | Declare expected counts instead of deriving them from `commands/`, `skills/`, `agents/`. Lets a fixture skip materializing 48 command files. | + +Unset on every production path — the derived values are what actually run. + +--- + ## Traffic Light Output ```text @@ -84,7 +131,13 @@ When `--fix` is specified: | `s` | Skip -- leave unchanged | | `e` | Exclude -- add to `exclusions.txt` permanently | -Pass 2 is skipped when `--non-interactive` is set. +`f` reports `Cannot auto-fix (manual edit needed)` when the finding carries no substitution to +run -- Phase 8's doc-coverage findings, for instance. Both passes apply fixes through the same +`apply_line_fix`, which reports success only when the file actually changed; see +[ADR-007](../adr/ADR-007-pattern-scoped-prose-staleness-gating.md) on why that is one shared +function and not two. + +Pass 2 is skipped when `--non-interactive` is set, and when stdin is not a TTY (CI). --- diff --git a/docs/reference/REFCARD-DOCS-UPDATE.md b/docs/reference/REFCARD-DOCS-UPDATE.md index d8f1a1caf..30c1d06cc 100644 --- a/docs/reference/REFCARD-DOCS-UPDATE.md +++ b/docs/reference/REFCARD-DOCS-UPDATE.md @@ -312,7 +312,6 @@ python3 tests/test_docs_utilities.py - [Full Documentation](../commands/docs/update.md) - Complete command reference - [Example Walkthrough](../examples/docs-update-interactive-example.md) - Step-by-step example - [Implementation Summary](https://github.com/Data-Wise/craft/blob/main/docs/specs/_archive/SPEC-docs-update-interactive-2026-01-22.md) - Technical details -- [Teaching Workflow Guide](../guide/teaching-workflow.md) - Teaching mode integration ## Quick Tips diff --git a/docs/reference/REFCARD-TEACHING.md b/docs/reference/REFCARD-TEACHING.md deleted file mode 100644 index 6ca7da807..000000000 --- a/docs/reference/REFCARD-TEACHING.md +++ /dev/null @@ -1,247 +0,0 @@ -# Teaching Workflow - Quick Reference - -> **Preview → Validate → Publish** - Safe course site deployment - -![Teaching Workflow Demo](../demos/teaching-workflow.gif) - -## Essential Commands - -> Site/docs commands below moved to the `folio` plugin. - -```bash -/folio:site:build # Build preview (current branch) -/folio:site:publish # Validate → Switch to main → Deploy -/folio:site:progress # Semester progress dashboard -ask "git status" (dev/git skill) # Teaching-aware git status -``` - -## Quick Start - -```bash -# 1. Create config -cat > .flow/teach-config.yml << 'EOF' -teaching: - enabled: true - branches: { preview: dev, production: main } - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -EOF - -# 2. Test -ask "git status" (dev/git skill) # Should show teaching mode - -# 3. Preview -/folio:site:build - -# 4. Publish -/folio:site:publish -``` - -## Common Workflows - -### Weekly Content Update - -```bash -git checkout dev # Start on preview -# ... edit content ... -/folio:site:build # Preview changes -/folio:site:publish # Deploy to production -``` - -### Check Progress - -```bash -/folio:site:progress # Semester dashboard -``` - -### Emergency Fix - -```bash -git checkout main # Go to production -# ... fix issue ... -/folio:site:build # Build -git push origin main # Deploy -git checkout dev # Back to preview -``` - -## Validation - -### Check Before Publish - -```bash -/folio:site:publish --dry-run --validate-only -``` - -### Common Issues - -| Issue | Fix | -|-------|-----| -| Missing week | Add to schedule.qmd | -| Date out of range | Check assignment due dates | -| Broken links | `/folio:docs:check-links` | -| Bad YAML | Validate teach-config.yml | - -### Skip Validation (Emergency) - -```bash -/folio:site:publish --skip-validation -``` - -## Branch Strategy - -``` -main (production) ← Students see this - ↑ -dev (preview) ← You edit here -``` - -**Rules:** - -- Edit on `dev` -- Preview with `/folio:site:build` -- Publish with `/folio:site:publish` (auto-switches branches) -- Asking "git status" (dev/git skill) shows which branch you're on - -## Configuration - -### Minimal - -```yaml -teaching: - enabled: true - branches: { preview: dev, production: main } - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" -``` - -### Standard - -```yaml -teaching: - enabled: true - branches: { preview: dev, production: main } - semester: - name: "Fall 2024" - start_date: "2024-08-26" - end_date: "2024-12-13" - weeks: 15 - content: - schedule: "schedule.qmd" - syllabus: "syllabus.qmd" - assignments_dir: "assignments/" - validation: - strict: true - publishing: - auto_nav_update: true -``` - -## Flow-CLI Config Compatibility - -If your project uses flow-cli's schema, Craft reads it natively — no migration needed: - -| Flow-CLI | Craft reads as | -|----------|---------------| -| `course.name` | `course.number` | -| `course.full_name` | `course.title` | -| `course.semester: "spring"` | `course.semester: "Spring"` | -| `semester_info.start_date` | `dates.start` | -| `semester_info.end_date` | `dates.end` | -| `branches.production` | `deployment.production_branch` | - -Single-day breaks (`start == end`) are supported for holidays like MLK Day. - -See [Config Schema](../teaching-config-schema.md#flow-cli-config-compatibility) for details. - -## Flags - -### `/folio:site:build` - -```bash -/folio:site:build # Normal build -/folio:site:build --force # Force rebuild -``` - -### `/folio:site:publish` - -```bash -/folio:site:publish # Full workflow -/folio:site:publish --dry-run # Preview what would happen -/folio:site:publish --validate-only # Just run validation -/folio:site:publish --skip-validation # Emergency publish -/folio:site:publish --force-rebuild # Force site rebuild -``` - -### `/folio:site:progress` - -```bash -/folio:site:progress # Full dashboard -/folio:site:progress --json # JSON output -``` - -## Troubleshooting - -### Not Detecting Teaching Mode - -```bash -# Check config exists -ls .flow/teach-config.yml - -# Verify enabled -grep "enabled: true" .flow/teach-config.yml - -# Test detection -ask "git status" (dev/git skill) # Should show teaching context -``` - -### Wrong Branch - -```bash -ask "git status" (dev/git skill) # Shows current branch + context -git checkout dev # Switch to preview -git checkout main # Switch to production -``` - -### Validation Errors - -```bash -# See what's wrong -/folio:site:publish --dry-run --validate-only - -# Fix issues, then try again -/folio:site:publish -``` - -### Site Not Updating - -```bash -# 1. Check GitHub Pages settings -# Repo → Settings → Pages - -# 2. Force rebuild -git checkout main -/folio:site:build --force -git push origin main -``` - -## Impact - -- ⏱️ **80% faster**: 15 min → 3 min per publish -- 🐛 **Zero bugs**: Validation catches issues -- 🎯 **100% confidence**: Preview before publish - -## See Also - -- [Full Guide](../guide/teaching-workflow.md) -- [Config Schema](../teaching-config-schema.md) -- [Migration Guide](../teaching-migration.md) -- [Setup Tutorial](../tutorials/teaching-mode-setup.md) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 4efbe8fe0..7dd14dcad 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -96,7 +96,7 @@ Execution modes can be customized per project. Version is managed across 13 files atomically using `bump-version.sh`: ```bash -./scripts/bump-version.sh 4.5.0 # Full bump +./scripts/bump-version.sh 4.6.0 # Full bump ./scripts/bump-version.sh --verify # Check for drift ./scripts/bump-version.sh --counts-only # Sync counts only ``` diff --git a/docs/reports/REPORT-doc-staleness-prose-gaps-2026-08-07.md b/docs/reports/REPORT-doc-staleness-prose-gaps-2026-08-07.md new file mode 100644 index 000000000..73f324ca5 --- /dev/null +++ b/docs/reports/REPORT-doc-staleness-prose-gaps-2026-08-07.md @@ -0,0 +1,75 @@ +# Report: Doc-Staleness Prose Gaps + +**Source:** [`SPEC-doc-staleness-prose-gaps-2026-08-07.md`](../specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md) +**Status:** draft — revised post adversarial review + +## tl;dr + +| Metric | Value | +|---|---| +| Checks proposed | 3 | +| Checks kept after review | 2 | +| Blocking review findings | 3 | +| New dependencies | 0 | +| New scripts | 0 | +| Acceptance criteria | 7 | +| Test tiers covered | 5 | + +## Problem + +`docs-staleness-check.sh`'s count-consistency phase (Phase 7) anchors on specific structured +patterns — badges (`version-X.Y.Z`), declared count lines (`N commands`) — and reported GREEN +while two real staleness bugs sat live in `docs/REFCARD.md` and `docs/skills-agents.md`: a +version box with the right number but a wrong release date and a stale highlight line +describing the prior release, and a TL;DR line claiming "8 specialized agents" two lines above +its own correct "2 specialized agents." Both are free prose the gate doesn't watch. + +## Locked Decisions + +| # | Decision | +|---|---| +| D1 | Extend `docs-staleness-check.sh` directly — no new script, no new dependency. | +| D2 | Scope: craft only, not the dev-tools-wide shared tooling. | +| D3 | Targeted prose-pattern regexes, not general NLP/semantic diffing. | +| D4 | No external tool adoption (Vale/drift/LLM-in-CI) — pattern-first shape borrowed, not the tools themselves. | +| D5 | Concrete checks, each with a clean-fixture and planted-defect-fixture regression test. | + +## What's Being Built (post-review) + +1. **Release-date claims** — prose matching `released? YYYY-MM-DD` tied to the current version + must match `.STATUS`'s `release_date:` field. Blocking (RED). +2. **Agent/skill count prose, structured lines only** — TL;DR lines, quick-reference/version + boxes, and badge/count-summary lines must match `plugin.json`'s current counts. Historical-log + files stay excluded via `scripts/config/exclusions.txt`. Blocking (RED). + +## Review Outcome — Adversarial Review Findings + +| Finding | Problem | Fix | +|---|---|---| +| Check 3's proxy falsified by its own target | `bump-version.sh` touches `docs/REFCARD.md`'s version line on every release, so a whole-file "last git-log touch" proxy would always read "not stale" on the exact file that caused this SPEC to exist | Dropped check 3 entirely — a per-line proxy would work but isn't worth the complexity for an advisory-only signal until checks 1–2 prove insufficient | +| Check 3's target already excluded | `docs/index.md` was already whole-file excluded from Phase 7 via `exclusions.txt`'s "curated hub page" entry | Moot once check 3 dropped | +| Check 2's false-positive surface far larger than proposed exclusions | A grep of `docs/**/*.md` found 90+ hits outside the 3-file exclusion list, including orchestration mode-limit prose, a fictional-plugin tutorial, and a troubleshooting page that intentionally shows a wrong count | Rescoped from blanket `\d+ agents?` search to structured line shapes only (TL;DR / version box / badge lines) | + +## Acceptance Criteria + +- [ ] Checks 1 and 2 implemented as blocking (RED) findings in Phase 7. +- [ ] Check 2's line-shape scoping tested against the 3 false-positive sources the review found — must NOT flag any of them. +- [ ] Regression fixtures restore today's exact bugs (REFCARD.md's stale date, skills-agents.md's "8 specialized agents") and prove the new checks catch them. +- [ ] Current (already-fixed) `dev` HEAD stays GREEN — no false positive on corrected content. +- [ ] `docs/reference/REFCARD-DOCS-STALENESS.md` documents the 2 new checks. +- [ ] `CHANGELOG.md` / `docs/CHANGELOG.md` `[Unreleased]` gets a one-line entry. +- [ ] No new script, no new external dependency. + +## Test Plan + +| Tier | Coverage | +|---|---| +| `unit` | New pattern-matcher functions, tested against clean + planted-defect fixtures for both checks, plus the 3 false-positive-source fixtures the review found for check 2. | +| `dogfood` | `docs-staleness-check.sh` on current `dev` HEAD → GREEN. On a git-stash of the pre-fix REFCARD.md/skills-agents.md → RED (regression proof). | +| `e2e` | `/craft:check --for release` runtime unaffected. | +| `integration` | N/A — no cross-command data flow. | +| `dependency` | N/A — no new dependency. | + +## Next Steps + +1. `/craft:plan docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md` — routes to `plan-orchestrator` for implementation. Grill optional (low ambiguity, decisions already locked). diff --git a/docs/skills-agents.md b/docs/skills-agents.md index c8eb7b283..fdcb25cc9 100644 --- a/docs/skills-agents.md +++ b/docs/skills-agents.md @@ -1,6 +1,6 @@ # Craft Skills & Agents -> **TL;DR**: 41 skills auto-activate from conversation context (no command to memorize) and 8 specialized agents handle deep work. Browse by category below; use [`/craft:hub`](commands/hub.md) to discover commands. +> **TL;DR**: 41 skills auto-activate from conversation context (no command to memorize) and 2 specialized agents handle deep work. Browse by category below; use [`/craft:hub`](commands/hub.md) to discover commands. Craft includes 41 auto-activating skills and 2 specialized agents for comprehensive development support. diff --git a/docs/specs/BRAINSTORM-codex-opencode-delegation-2026-08-08.md b/docs/specs/BRAINSTORM-codex-opencode-delegation-2026-08-08.md new file mode 100644 index 000000000..a2af57b02 --- /dev/null +++ b/docs/specs/BRAINSTORM-codex-opencode-delegation-2026-08-08.md @@ -0,0 +1,271 @@ +# BRAINSTORM: Delegating to Codex and OpenCode + +**Date:** 2026-08-08 · **Depth:** deep · **Focus:** ops +**Branch:** dev · **Categories:** tech, risks, timeline + +## Origin + +Seed request (refined via `--refine`): research (web) best practices for +delegating coding tasks from Claude Code to OpenAI Codex (via the `codex` +plugin) and to OpenCode (via the `opencode`/`opencode-async` MCP bridges +just installed this session — both promoted to `user` scope, tested working +over stdio JSON-RPC: `opencode` exposes ~80 tiered tools, `opencode-async` +exposes 7 fire-and-forget tools). Not scoping a new feature — scoping how to +*use* three delegation surfaces craft's user now has installed: + +| Surface | Style | +|---|---| +| `codex` plugin (`codex:codex-rescue` agent) | Synchronous hand-off — Claude drives `codex exec` as a subprocess, one broker per cwd | +| `opencode` MCP bridge | Synchronous-first, tiered (`opencode_setup` → `opencode_ask`/`opencode_run` → fine-grained session control) | +| `opencode-async` MCP bridge | Fire-and-forget — submit, poll `opencode_sessions`, `opencode_respond` on `input_required` | + +## Context Scan + +- **No existing craft spec covers delegation strategy.** `grep -ril + "codex\|opencode" docs/specs/` (excluding `_archive/`) found nothing — + this is genuinely new ground for craft's own docs, even though the tools + themselves were just wired up this session. +- **`docs/tutorials/TUTORIAL-opencode-mcp-plugin.md`** (added this session) + covers *setup and mechanics* of the two OpenCode bridges — scope, gotchas, + smoke-test method. It does not cover *when/how to use them well*, which is + this brainstorm's subject. +- **`agents/codex-rescue`** (via the `codex` plugin) already encodes one + delegation trigger informally: "proactively use when Claude Code is + stuck, wants a second implementation or diagnosis pass." No equivalent + trigger doc exists for either OpenCode bridge yet. + +## Research Findings (web) + +**Task selection — when delegation is worth it at all.** The dominant +framing across sources: delegate when *describing* the task is cheaper than +*doing* it. A one-line rename isn't worth a delegation round-trip; a +codebase-wide migration is. Good delegation candidates: test generation, +boilerplate, documentation, simple refactors, well-scoped SWE tasks. Poor +candidates: security logic, architectural decisions, unfamiliar stacks, +anything the delegator can't verify afterward. Subagents/delegated calls +are "not free" — startup cost, coordination overhead, and failure modes +that don't exist when you just do the work yourself. + +**Model/tier selection.** Not every delegated task needs the strongest +model. A repeated pattern: cheap/fast models for narrow workers (routing, +formatting, simple extraction), stronger reasoning models reserved for the +orchestrator or for genuinely hard sub-tasks. `opencode`'s own tool +description matches this — it explicitly warns against assuming a provider +is available and tells the caller to discover providers/models before +picking one per task. + +**Structuring the delegated prompt.** Practical guidance converges on: +single-line messages (no embedded newlines — when driving an interactive +TUI, a newline can act as Enter and fragment the prompt; this does not +apply to `codex exec`, which takes the prompt as a plain argument/stdin +string, not keystrokes into a TUI), explicit scope statements, and — for +OpenCode specifically — picking the right agent mode (`explore` for +investigation, `plan` for structured analysis, `build` for direct +execution) rather than defaulting to one mode for everything. + +**Orchestration pattern.** The Manager-Worker / hierarchical pattern shows +up repeatedly as the best fit for coding delegation: one agent plans and +assigns, worker agents execute narrow scoped pieces, the manager reviews. +Each worker gets a fresh context and a clean failure boundary, which +contains cascading failures rather than propagating them back through the +whole session. + +**Token cost — directly answers the "token usage" risk flagged below.** +Every hop in a delegation chain that invokes an LLM is a billed API call — +a topology that looks cheap in-process can multiply spend once each +specialist is a real request. The mechanism to watch: the orchestrator's +own context grows with every round-trip result that flows back to it — +sources report a 5-10 sub-agent pipeline can push the orchestrator itself +to roughly 30-50k tokens by the end, independent of what the workers +spent (single-source estimate, not independently measured here). Two +mitigations, with tradeoffs reported the same way: (1) pass a small +structured context object instead of full conversation history (roughly +200-500 tokens vs. 5,000-20,000), or (2) summarize at the handoff (roughly +a 70-90% reduction) at the cost of ~500ms-1.5s added latency and some +information loss. Execution budgeting (max tokens, max steps, max time per +delegated call, with alerts on threshold breach) is the other lever named +repeatedly. + +**Failure handling.** Consistent theme: isolate the blast radius. A failed +worker should fail cleanly within its own context rather than corrupting +the manager's state. The two OpenCode bridges use *different* status +vocabularies, not a shared one: `opencode-async`'s task status is +`working` / `input_required` / `completed` / `failed` / `cancelled` +(`opencode_cancel` reaches the last one); the npm `opencode` bridge's +`opencode_check` reports `running` / `completed` / `error` instead — do +not assume interchangeability between the two when writing status-handling +code against either. + +Sources: + +- [GitHub - eddiearc/codex-delegator](https://github.com/eddiearc/codex-delegator) +- [Codex Task Dispatcher - Claude Code Skill for OpenAI](https://mcpmarket.com/tools/skills/codex-task-dispatcher) +- [Claude codex-cli skill: 10 ways to bridge Claude Code and OpenAI Codex CLI](https://mcp.directory/blog/claude-codex-cli-skill-guide) +- [GitHub - Traves-Theberge/opencode-mcp](https://github.com/Traves-Theberge/opencode-mcp) +- [Best practices for Mastering AI Agents, Subagents, Skills & MCP](https://foojay.io/today/best-practices-for-working-with-ai-agents-subagents-skills-and-mcp/) +- [Cost Management for LLM Agents](https://apxml.com/courses/multi-agent-llm-systems-design-implementation/chapter-6-system-evaluation-debugging-tuning/managing-llm-agent-costs) +- [Multi-Agent Orchestration Patterns: A Practical Guide](https://www.glukhov.org/ai-systems/architecture/multi-agent-orchestration-patterns/) +- [AI Agent Subagent Orchestration: When to Spawn vs When to Do It Yourself](https://dev.to/bobrenze/ai-agent-subagent-orchestration-when-to-spawn-vs-when-to-do-it-yourself-4opg) +- [The Delegation Decision: When to Use an Agent vs Do It Yourself](https://agentpatterns.ai/agent-design/delegation-decision/) + +## Expert Questions & Answers + +**Technical constraints/preferences** (multi-select): Use existing stack +(research the 3 tools already installed, don't evaluate new ones) + New +tooling needed (stay open to surfacing other options if research turns them +up) + Architectural pattern (define routing rules for which task type goes +where). → Net: research the installed stack deeply, but the output should +still propose a routing rule, and flag credible alternatives if the +research surfaces them unprompted. + +**Integration target** (multi-select): craft's own commands/skills + +Ad-hoc manual use only. → Net: findings should be usable standalone today +(manual delegation decisions), but written so a future craft skill/command +could consume the routing rule directly without a rewrite. + +**Timeline:** Flexible. **First usable outcome:** Flexible. No deadline +pressure — quality over speed on this one. + +**Biggest risks** (multi-select): Technical complexity (routing logic) + +Integration issues (auth/model availability) + Performance concerns +(latency/cost) + **token usage for codex and opencode models** (user +addition, not from the question bank — see Token Cost research above, +directly responsive). + +**Edge cases** (multi-select): Failure scenarios (delegated agent +hangs/errors/garbage output) + Concurrent access (codex's per-cwd broker + +opencode-async's session model, both need to survive multiple in-flight +delegations) + Empty/invalid input (vague delegated prompts producing +useless output). + +## Proposed Routing Rule (ops output) + +Based on research + the answers above, a first-cut decision rule — not yet +adopted as policy, offered for the follow-up spec if this gets built out: + +1. **Do it yourself** when: the task is smaller to describe than to do, it + touches security/architecture/unfamiliar-stack decisions, or you can't + independently verify the result. +2. **Delegate to `codex`** when: it's a genuinely hard, well-scoped SWE + task (complex refactor, deep debugging) and you want a second + implementation/diagnosis pass — matches the existing `codex-rescue` + agent's own trigger condition, no new logic needed there. +3. **Delegate to `opencode` (sync)** when: the task is a one-shot + question/lookup that benefits from a different model's perspective, and + you want the answer inline, in this turn — `opencode_ask` after + `opencode_setup`/`opencode_provider_models` discovery. +4. **Delegate to `opencode-async` (fire-and-forget)** when: the task is + long-running and you don't want to block this session on it — submit, + keep working, poll `opencode_sessions` later. +5. **Always budget before firing:** state an explicit stop condition (max + turns/time) in the delegated prompt itself where the target tool + supports it: uncontrolled delegation chains are the direct cause of the + token-cost risk flagged above. + +## Risks & Edge Cases (recorded, not yet mitigated in code) + +- Token spend compounding across delegation hops — no craft-side budget + enforcement exists yet for either bridge. +- No craft-side timeout/hang detection wired to either `opencode` bridge + today (craft's own `orchestrate-dispatch` mode has this for its own + subagents — worth reusing the pattern rather than inventing a new one, + per the "reuse, don't duplicate" instinct that shaped the `codex` plugin + integration). +- Concurrent delegation to the same cwd via `codex` (single broker per + directory) vs. multiple parallel `opencode-async` sessions — not + stress-tested. + +## Test Plan + +| Tier | Status | +|---|---| +| unit | N/A — no new parser/script proposed by this brainstorm | +| integration | N/A — no cross-command data flow proposed yet | +| e2e / dogfood | N/A — no craft command/skill change proposed yet; this is a research artifact, not an implementation | +| dependency | N/A — no external dependency change | +| count-cascade | N/A — no new command/skill/agent | + +Nothing here is code-shaped yet — the routing rule above is a candidate for +a future skill, not a shipped one. Re-run test-plan inference once/if this +becomes a `/craft:plan` implementation. + +## Documentation + +Doc-impact scorer (threshold ≥3) applied against the routing rule above, +should it ship as a skill later: + +- [ ] Guide — N/A, no implementation yet +- [ ] REFCARD entry — N/A, no implementation yet +- [ ] Demo — N/A, no implementation yet +- [ ] Mermaid diagram — N/A, no implementation yet + +This BRAINSTORM itself is the documentation artifact for now. + +## Adversarial Review (dogfooding the routing rule on itself) + +Both delegation surfaces this brainstorm is about were used to review the +uncommitted diff that produced it (this file + the tutorial + the mkdocs +nav line) — the routing rule ate its own dog food same-session. + +| Reviewer | Model | Cost | Tokens | Findings | +|---|---|---|---|---| +| `codex` (`codex exec review --uncommitted`) | GPT-5-based (Codex default) | not reported by this CLI | 1,010,356 total (1,002,654 in @ 91% cache hit, 7,702 out) | 2 (both real: markdownlint gap, wrong tool count) | +| `opencode` (`opencode_run`, provider `opencode-go`) | `deepseek-v4-pro` | $0.0003 | 1,555 total (267 in, 1,288 out) | 6 (see below) | + +**Cost delta is ~650× by token count** for this task — not a general claim +about the models' relative capability, just this one review of a +~250-line diff. Codex's number is inflated by a large, mostly-cached +system-prompt/tool context (skills + instructions bundle), not by doing +more review work; `opencode`'s dispatch had no comparable fixed overhead +in this setup. **Not a dollar comparison** (added on adversarial +re-review, GRILL Branch 4): codex ran on a flat subscription (sunk cost, +no marginal $ per call), while `opencode-go` bills per call — token count +is still a real signal (context-window pressure, rate limits apply under +a subscription too), just don't read "650×" as a cash-cost claim. + +**opencode/deepseek-v4-pro findings** (both fixed in this doc + the +tutorial as part of this same session): + +1. **F1 (High):** this brainstorm's own Origin section still said "6 + fire-and-forget tools" after the tutorial's copy had already been fixed + to 7 — the two docs contradicted each other. Real bug: codex's review + never caught this because it only reviewed the diff, and this line + hadn't changed in the diff it saw (it was wrong from the first version). +2. **F2 (High):** the Failure Handling section asserted `opencode` and + `opencode-async` share one status vocabulary + (`working`/`input_required`/`completed`/`failed`). False — that set is + `opencode-async`-only; `opencode`'s `opencode_check` reports + `running`/`completed`/`error`. Also caught a missing `cancelled` status. +3. **F3 (Medium):** tutorial said the usage guide lives at + `serverInfo.instructions` — it's actually a top-level `instructions` + field, sibling to `serverInfo`, not nested in it. +4. **F4 (Medium):** the newline-as-Enter caveat (from web research) was + misattributed to `codex exec` specifically — that mode takes the prompt + as a plain argument, not TUI keystrokes, so the hazard doesn't apply + there. Softened to the general interactive-TUI case. +5. **F5 (Low):** missing caveats — `OPENCODE_AUTO_SERVE=false` disables + auto-start; `cancelled` status wasn't surfaced (see F2); the two docs + should cross-link each other. Not individually actioned (low value, + noted here for completeness). +6. **F6 (Low):** several web-research numbers (30-50k token orchestrator + growth, 70-90% summarization reduction, etc.) were stated as fact + without a confidence hedge or citation in the Sources list — corrected + to "sources report" / "roughly" framing. + +**codex/GPT-5 findings** (both fixed earlier in this session, see the +commit history): missing blank line before the Sources markdown list +(markdownlint), and the same wrong tool-count bug F1 re-caught in a +different file. + +**Takeaway for the routing rule:** two independent reviewers with +different strengths caught non-overlapping bugs — codex caught a lint gate +failure (mechanical, would have failed CI); opencode caught two factual +inconsistencies a lint gate can't see (contradicting docs, a misattributed +technical claim). Neither was redundant with the other. This is itself +evidence for the "use both, not just one" instinct in the routing rule +below rather than a full head-to-head model comparison (n=1 diff, not a +benchmark). + +## Next + +- **Now spec'd.** See `docs/specs/SPEC-codex-opencode-delegation-2026-08-08.md`. diff --git a/docs/specs/BRAINSTORM-doc-staleness-prose-gaps-2026-08-07.md b/docs/specs/BRAINSTORM-doc-staleness-prose-gaps-2026-08-07.md new file mode 100644 index 000000000..a88e94261 --- /dev/null +++ b/docs/specs/BRAINSTORM-doc-staleness-prose-gaps-2026-08-07.md @@ -0,0 +1,149 @@ +# BRAINSTORM: Doc-Staleness Prose Gaps (closing what count-consistency misses) + +**Date:** 2026-08-07 · **Depth:** deep · **Focus:** ops +**Branch:** dev · **Categories:** req, existing, tech, risks, scope, success + +## Origin + +Seed request (refined via `--refine`): research how doc-staleness detection is handled +elsewhere (other Claude Code plugin/skill projects, adjacent coding-agent ecosystems), then +compare against craft's own tooling to find patterns worth adopting. + +Directly triggered by two bugs found and fixed *this session*, both while +`docs-staleness-check.sh` reported GREEN: + +- `docs/REFCARD.md`'s quick-reference box had the correct version **number** (4.5.0 — + `bump-version.sh`'s literal-string target) but a stale release **date** (2026-07-17, + actually v4.1.0's) and a stale highlight line (still describing v4.1.0's `/craft:restore`, + not v4.5.0's repo-triage skill). +- `docs/skills-agents.md`'s TL;DR line said "8 specialized agents" two lines above its own + correct "2 specialized agents" — stale since the v4.0.0 folio split, never caught. + +Both are **free prose**, not the structured badges/counts `docs-staleness-check.sh`'s +count-consistency phase (Phase 7) anchors on. The gate did its job on every surface it +watches; these two just weren't watched surfaces. + +## Context Scan + +- **No existing spec on this topic.** `find`/`grep` across `docs/specs/` for + `staleness`/`drift`/`doc-track` found nothing prior — genuinely new ground for craft, + despite already having the most mature doc-tooling stack of the ~25 dev-tools sibling repos. +- **Existing tooling inventory** (all already in craft, none of it is a green-field build): + - `scripts/docs-staleness-check.sh` (35.7K) — 4-phase gate: nav completeness, count + consistency, skill/agent/cmd coverage, cross-doc freshness. Anchors on specific + badge/count patterns (`version-X.Y.Z`, `N commands`, `N skills`), not arbitrary prose. + - `scripts/doc-coverage-check.sh` — blocks release on missing REFCARD rows / mkdocs nav + entries for new commands. + - `utils/claude_md_sync.py` — CLAUDE.md-specific drift detector (documented-command + existence, error/warning/info tiers); this session's `/craft:done` run caught 1 real + error (`/craft:done` still referenced, file deleted) via this tool, not the staleness + gate. + - `utils/docs_detector.py` / `utils/docs_update_orchestrator.py` — semantic doc-update + orchestration for `/craft:docs:update`. + - None of these do free-prose semantic staleness checking today — every one of them + matches a structured pattern (a badge, a count line, a declared command name). + +## External Research (WebSearch, 2026-08-07) + +Two search passes: general doc-drift-in-CI practice, and Claude-Code-specific tooling. + +**General CI doc-drift practice:** + +- **[fiberplane/drift](https://github.com/fiberplane/drift)** — anchors markdown docs to + source code via tree-sitter + git; on a supported language, hashes a normalized AST + fingerprint (node kinds + token text) and flags the doc when the anchored code changes. + Closest match to craft's own `bump-version.sh`-touches-N-files pattern, but source-code-anchored + rather than count/version-anchored — not directly reusable (craft's staleness is prose-vs-metadata, + not prose-vs-code). +- **[dosu.dev: doc freshness scoring](https://dosu.dev/blog/score-documentation-freshness-in-ci)** — + a 0–100 freshness signal from three deterministic checks plus a semantic layer for edge + cases; the "deterministic checks first, semantic layer only for what's left" shape is the + most directly transferable idea here. +- **[jbrockSTL/doc-drift](https://github.com/jbrockSTL/doc-drift)** — LLM-in-CI stale-doc + catcher on every PR via GitHub Actions; cost-controlled via label-gating, cron batching, and + path filters rather than running on every push. +- **[Vale](https://vale.sh/)** — YAML-rule prose linter (existence/repetition/spelling + extension points); proves custom prose-pattern rules are a well-trodden, low-tooling-cost + approach — closer in spirit to extending `docs-staleness-check.sh`'s existing grep-based + phases than adopting a new dependency. + +**Claude Code / coding-agent specific:** + +- **[dosu.dev: catching doc drift with Claude Code + GitHub Actions](https://dosu.dev/blog/how-to-catch-documentation-drift-claude-code-github-actions)** — + triggers Claude Code itself as the drift-detector in CI, reading diffs and flagging + affected docs semantically. +- **[dev.to: "How Claude Skills Replaced Our Documentation"](https://dev.to/magnusrodseth/how-claude-skills-replaced-our-documentation-emi)** — + reframes skills as living docs: traditional docs rot because they're write-once/read-rarely, + but a skill the agent actually executes gets exercised constantly, so drift surfaces + immediately. Doesn't apply directly to REFCARD/skills-agents.md (reference docs, not + executable skill bodies) but validates craft's own pattern of thin command-shims pointing + at canonical skill files (already this repo's convention). + +**Takeaway:** nothing found is a drop-in replacement for `docs-staleness-check.sh` — the +external tools solve *code*-anchored drift (source changed, doc didn't) or run a full LLM +pass per PR (cost/latency for a check craft already does deterministically in seconds). The +one directly transferable idea is Vale/dosu's "deterministic pattern checks first, escalate +only what's left" shape — which is exactly what today's bugs argue for: add deterministic +prose-pattern checks for the *specific* phrase shapes that already bit us, not a general +semantic-diff engine. + +## Locked Decisions + +| # | Question | Decision | +|---|---|---| +| 1 | Trigger for this work | **Both** — the gate missed real bugs today AND external research should inform the fix, not just today's incident. | +| 2 | Integration approach | **Extend `docs-staleness-check.sh`**, not a separate scan — same gate, wider prose-pattern coverage. No new script, no new dependency. | +| 3 | Scope | **Craft only.** Craft already has the most mature doc-tooling of the ~25 dev-tools sibling repos; solve here first, consider porting the pattern later if it proves out. | +| 4 | Rigor level | **Targeted prose-pattern additions**, not general NLP/semantic staleness detection. Low false-positive risk, addresses today's exact bug class (version-box "released DATE" lines, "N specialized agents"/"N skills" free-prose count mentions, stale highlight/callout lines that repeat a phrase from a prior release). | +| 5 | External-tool adoption | **None wholesale.** No new dependency (Vale, drift, LLM-in-CI) — the deterministic-pattern-first shape is worth copying, the tools themselves aren't a fit for craft's count/version-anchored (not code-anchored) staleness problem. | + +## New Prose-Pattern Checks (Phase 7 extension) + +> **Superseded by adversarial review** (2026-08-07) — the SPEC's "Review Outcome" section +> has the corrected version: check 3 (version-highlight proxy) was dropped as falsified by +> `bump-version.sh`'s own touch pattern, and check 2 was rescoped from free-text search to +> structured line shapes after a false-positive sweep found 90+ non-bug matches. This section +> is left as-is for the historical record of what was first proposed; see the SPEC for what's +> actually being built. + +Concrete patterns to add to `docs-staleness-check.sh`'s count-consistency phase, each +regex-anchored the same way existing checks are (compare against the single source of truth — +`plugin.json`'s command/skill/agent counts and the current version): + +1. **"released YYYY-MM-DD" / "(released ...)" mentions** — any doc claiming a release date + for the *current* version must match `.STATUS`'s `release_date:` field. Today's REFCARD.md + bug is exactly this pattern. +2. **"N specialized agent(s)" / "N agent(s)"** free-prose mentions (excluding files that are + historical logs by convention — `VERSION-HISTORY.md`, `CHANGELOG.md` entries about past + versions) — must match the current agent count. +3. **Version "highlight"/"latest"/callout lines** (`docs/index.md`'s `!!! info "Latest: ..."`, + `docs/REFCARD.md`'s box highlight line) — flag if the line's version token matches current + but hasn't been touched in the same commit range as the last version bump (a cheap git-log + proxy for "this prose is stale even though the number is current"). + +## Test-Plan + +| Tier | Coverage | +|---|---| +| `unit` | New regex/pattern-matcher functions for each of the 3 checks above, tested against both a clean fixture and a planted-defect fixture (today's exact REFCARD.md/skills-agents.md bugs, restored as regression fixtures). | +| `dogfood` | `docs-staleness-check.sh` run against the current repo state must stay GREEN; run against a git-stash of today's pre-fix REFCARD.md/skills-agents.md must go RED on the new checks (positive control — proves the fix would have caught this exact incident). | +| `e2e` | Full `/craft:check --for release` pass unaffected in runtime (new checks stay in Phase 7's existing budget, no new phase). | +| `integration` | N/A — no cross-command data flow introduced. | +| `dependency` | N/A — no external dependency added (explicit decision #5). | + +## Documentation + +| Doc type | Needed? | +|---|---| +| Guide/reference | `[x]` `docs/reference/REFCARD-DOCS-STALENESS.md` — add the 3 new pattern checks to its existing check inventory. | +| REFCARD (main) | N/A — score <3, no new command surface. | +| Demo/GIF | N/A — score <3, no new interactive flow. | +| Mermaid | N/A — score <3, no new architecture shape (extends an existing phase, doesn't add one). | +| CHANGELOG `[Unreleased]` | `[x]` One-line entry once implemented: "docs-staleness-check.sh: 3 new prose-pattern checks (release-date claims, agent-count mentions, stale highlight lines) — closes the REFCARD.md/skills-agents.md gap found 2026-08-07." | + +## Next Step + +`/craft:plan docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md` to scope implementation +(the 3 pattern checks + fixtures + REFCARD-DOCS-STALENESS.md update) — small enough that grill +is optional (low ambiguity, no unresolved dependency), but available if any pattern's exact +regex/false-positive tradeoff needs interrogation before building. diff --git a/docs/specs/GRILL-codex-opencode-delegation-2026-08-08.md b/docs/specs/GRILL-codex-opencode-delegation-2026-08-08.md new file mode 100644 index 000000000..3a0e788b2 --- /dev/null +++ b/docs/specs/GRILL-codex-opencode-delegation-2026-08-08.md @@ -0,0 +1,41 @@ +# GRILL: Codex/OpenCode Delegation Routing — Adversarial Review + +**Date:** 2026-08-08 · **Target:** [SPEC-codex-opencode-delegation-2026-08-08.md](SPEC-codex-opencode-delegation-2026-08-08.md) +**Branches interrogated:** 5 (riskiest assumption, weakest recommendation ×2, benefit honesty, workflow discipline/default policy) + +Codebase-first sweep found the load-bearing issue before any question was asked: `codex-rescue` +is owned by the separately-installed `codex` plugin, not craft — no cross-plugin agent +invocation exists in this repo (established by craft's own prior router-consolidation audit). +Three of the SPEC's five candidate rows cited it as reuse precedent; Branch 1 corrects this. + +## Decision Ledger + +| # | Branch | Decision | +|---|---|---| +| 1 | delegation mechanism | Craft's `--delegate` flag shells to `codex exec` directly (same mechanism as the tutorial's Monitor template), never through the `codex-rescue` agent — that agent belongs to a different plugin and can't be invoked cross-plugin. SPEC's "why" column for `code:debug`/`code:refactor`/`ci:triage` needs rewording to drop the implied reuse; the underlying candidate choices stand, only the mechanism description was wrong. | +| 2 | monitoring enforcement | The "mandatory monitor" step is documentation-only today (copy-paste templates, nothing enforces use). New acceptance criterion: any future command implementing `--delegate` must ship with a test asserting its dispatch path includes a status/token check — same enforcement pattern as `--refine`'s `test_refine_default_policy_table_exhaustive`. Craft's own hooks-as-defense-in-depth precedent says prompts/docs alone get silently skipped; a doc-only requirement here would regress to fire-and-forget exactly the way the SPEC's own Problem statement complains about. | +| 3 | ci:fix vs ci:triage | Only `ci:triage` gets `--delegate`, not `ci:fix`. Triage is diagnosis-only (advisory output, human decides) — safe to delegate. `ci:fix` already mutates the repo; delegating a write-capable command's decision-making to an external model raises the blast radius with no safeguard proposed in this pass. Candidate list is now 4 distinct commands, not 5 (`code:debug`, `code:refactor`, `code:test-gen`, `arch:review`, `ci:triage` — `ci:fix` dropped). | +| 4 | cost-comparison honesty | The "650× cheaper" figure compares codex's raw token count against opencode's metered dollar cost — not like-for-like, since codex ran on a flat Plus subscription (sunk cost) while `opencode-go` bills per call. Token count is still a valid signal (context-window pressure, rate limits are real under a subscription too), but the SPEC/BRAINSTORM must state plainly that "650× cheaper" is a token-count fact, not necessarily a cash-cost fact, so a future reader doesn't misapply it as a dollar comparison. | +| 5 | default policy | `--delegate` defaults **OFF** on all 4 candidate commands — matches `--refine`'s existing OFF-default precedent for "execution engine" commands (`orch`/`orch:workflow`). Delegation costs real money/tokens and hits an external network dependency, unlike `--refine` (free, local, no network) — it should never fire silently. This also makes Branch 2's monitoring requirement easier to guarantee: an explicit, deliberate flag is easier to gate a test on than a heuristic auto-trigger. | + +## Open Questions (not locked, hand to `/craft:plan`) + +- Exact wording/placement of the corrected "why" column text for Branch 1 (mechanical doc fix, + not a design decision — small enough to do inline when Task 1 is picked up). +- Whether the dogfood-test requirement from Branch 2 should live in a shared test helper (one + test parametrized over all 4 commands) or 4 separate per-command tests — implementation + detail, not blocking. +- Whether `code:test-gen`'s and `arch:review`'s delegation targets should also default to a + specific `providerId`/`modelId` (e.g. always `deepseek-v4-pro` for review-shaped delegation, + matching tonight's dogfood run) or require the user to specify one each time — not raised + during this grill, worth a quick decision at implementation time. + +## Documentation Plan + +- `docs/specs/SPEC-codex-opencode-delegation-2026-08-08.md` — apply Branches 1, 3, 4, 5 (reword + candidate table, drop `ci:fix` row, add cost-honesty caveat, add default-OFF decision + new + acceptance criterion for the dogfood-test requirement). +- `docs/specs/BRAINSTORM-codex-opencode-delegation-2026-08-08.md` — Branch 4's cost-honesty + caveat applies to its "Cost delta is ~650×" line too (same claim, same fix). +- No `commands/*.md` changes yet — `--delegate` itself is still unbuilt (SPEC D2/out-of-scope); + this ledger only corrects and locks the design it will follow when built. diff --git a/docs/specs/GRILL-prose-check-hardening-2026-08-15.md b/docs/specs/GRILL-prose-check-hardening-2026-08-15.md new file mode 100644 index 000000000..3b26dd462 --- /dev/null +++ b/docs/specs/GRILL-prose-check-hardening-2026-08-15.md @@ -0,0 +1,232 @@ +# GRILL: Prose-Check Hardening + +**Date:** 2026-08-15 · **Spec:** [`SPEC-prose-check-hardening-2026-08-15.md`](SPEC-prose-check-hardening-2026-08-15.md) +**Branches resolved:** 11 (D1–D6 design · D7–D11 plan) · **Status:** locked, ready for implementation + +Interrogates the 9 defects a high-effort review found in PR #334. Two branches were reframed by +evidence gathered during the grill rather than by argument — see D1 and the correction under D4. + +--- + +## D1 — Check 1's authority: **cross-file consistency**, not the git tag + +**Locked:** drop the external authority. Compare every release-date claim for the current version +against each other and flag disagreement. The one-day window survives as the agreement tolerance +between claims, absorbing the UTC-boundary case that motivated it. + +**Why the tag was rejected — evidence, not preference.** The tag is absent in both environments +the check exists to protect: + +| Environment | Why no tag | +|---|---| +| Release gate | `skills/release/SKILL.md` runs Step 3b (write doc dates) → **Step 3b.5 (staleness gate)** → Step 8 (create tag). The tag for the version being released does not exist when the gate runs. | +| CI | `docs-quality.yml` uses `actions/checkout@v5` with no `fetch-tags`, so `git for-each-ref refs/tags/vX.Y.Z` returns empty. | + +Net: as built, check 1 fires only on a developer machine that has already pulled the tag — which +is not a gate. Cross-file consistency needs no external authority, so it works in both. + +**Accepted cost:** cannot catch a date that is uniformly wrong in every file, and says nothing +when only one claim exists. Both are strictly better than never firing. + +## D2 — Severity: **split — check 1 RED, check 2 YELLOW** + +**Locked:** severity follows demonstrated precision, per-check rather than per-phase. + +**Corrects a factually wrong claim in ADR-007.** The ADR justified `warning` by asserting that +warnings and errors both fail the release gate, so only the label differs. Verified false: + +- `scripts/pre-release-check.sh:280` — "Check 9: Docs staleness (warn-only, does not block + release)", and the call is `… || true`, discarding the exit code. +- `.github/workflows/docs-quality.yml` — `continue-on-error: true`. +- `skills/release/SKILL.md:143` — "RED findings block; YELLOW findings warn but allow proceed." + +No consumer reads the exit status. The **label is the only gate**, so the severity choice *is* the +block decision the ADR claimed it was not. ADR-007's Severity section must be rewritten with the +verified behavior — removed, not softened. + +Check 1 after D1 is near-binary (two claims agree or they do not), so it can afford to block. +Check 2 matches prose patterns and produced three false-positive defects in this PR alone +(F1, F6, and the five sub-threshold counts caught pre-merge), so it has not earned a blocker. + +## D3 — F1/F2/F6: **boundary + span-anchored substitution + real floor**, all three + +**Locked:** + +| Change | From | To | +|---|---|---| +| Noun trailer | `([^a-z]\|$)` | whitespace / closing punctuation / end-of-line | +| Fix payload | substitution built from the bare `N noun` prefix | anchored on the full matched span | +| Floor | `expected * 40 / 100` | `max(2, expected * 40 / 100)` | + +**Why all three, not just the trailer.** F1's agent half is *caused* by F6 — the floor is +`$((2 * 40 / 100))` == 0, so the guard ADR-007 and two code comments cite as what makes shaped +lines safe does not exist for the smallest count type. Fixing the trailer alone leaves that root +cause live. And F2 corrupts the author's prose, a worse outcome than any false warning, so the +substitution is anchored independently of the matcher: a future boundary regression then produces +a wrong warning rather than a wrong edit. + +**Must stay pinned:** the E1 case (`8 agent definitions`) survives both changes — the noun is +followed by a space, and `8 ≥ max(2, 0)`. Existing `clean/structure-table-correct.md` and +`defect/structure-table-singular.md` already cover it; they must keep passing unchanged. + +## D4 — F4 `[e]xclude` no-op: **fix at the record level, both phases** + +**Correction made during the grill.** This was initially reported, and relayed, as a regression +introduced by PR #334's change of the finding's `file` field to `path:lineno`. It is not. +`git show dev:scripts/docs-staleness-check.sh` has `add_finding 7 "warning" "${file}:${lineno}"` +at line 341 and the same shape in Phase 9 at 497 and 548. `[e]xclude` has been a no-op for **all** +Phase 7 and Phase 9 findings, predating this PR. + +**Locked:** carry the line number as its own field instead of glued into `file`, so `[e]` writes +`docs/x.md:30 command` and `is_pattern_excluded` can match it. + +**Why the larger blast radius is accepted.** The bug prints "Excluded (added to exclusions.txt)" +and the finding returns on the next run — a live instance of the exact +reports-success-changes-nothing class ADR-007 states as a rule. Shipping that rule while the same +file violates it twice is worse than either fixing it or not writing the rule. Fixing it in the +`[e]` branch alone would leave the underlying ambiguity (is `file` a path or a location?) for the +next person. + +## D5 — Delivery: **amend PR #334 before merging** + +**Locked:** all fixes land on `feature/doc-staleness-prose-gaps`; nothing merges until the checks +are correct. + +**Why not merge-then-harden.** As it stands the check false-positives on any hyphenated compound, +and pass 2's `[f]` would rewrite the author's prose around it. craft's own docs are hyphen-heavy, +so "merge now, harden in #335" means `dev` carries a `--fix` that can mangle documents for as long +as the follow-up takes. The PR has not shipped and nothing depends on it, so the cost of holding is +only review surface. + +**Accepted cost:** #334 becomes a large single review, and the D4 record change touches all four +phases. + +## D6 — Window discipline: **the version token must be in a heading** + +**Added 2026-08-15, after the initial 5 branches.** Surfaced while authoring +`ORCHESTRATE-prose-check-hardening.md`: the spec listed 9 decisions and the grill locked 5, leaving +the release-date **window discipline** (spec D4 / finding F3) unresolved. D1 settled where the +authority comes from, not how claims are collected — so the false positive survived the redesign +in a new place. + +**Locked:** the window opens only when the version token appears in a markdown heading or a +version-box line. Prose mentioning the version — "Upgrading to v4.5.0 is a drop-in change" — does +not open it, so a nearby unrelated date is never collected as a claim for that version. + +**Why structural over numeric.** Shrinking the window (spec D4c) narrows the class without closing +it: a date two lines under a prose mention still false-positives. Requiring version and date on the +same line (D4b) closes it completely but misses the real `docs/NEWS.md` layout, where the heading +carries the version and the date sits two lines below — i.e. it would not catch the bug the check +exists for. A heading requirement excludes running prose by construction, and every site carrying +a real release entry today is a heading or a box. + +**Raised stakes:** D2 makes check 1 RED, so each surviving false positive blocks a release. That +rules out spec D4d (accept and exclude case-by-case). + +--- + +## Plan-level branches — grilling `ORCHESTRATE-prose-check-hardening.md` + +D1–D6 interrogated the *design*. These interrogate the *plan to build it*, and four of the five +found defects in the plan rather than confirming it. + +### D7 — Phase order: **the record change goes first** + +**Locked:** reorder to record-change → matcher → check 1 → severity → low findings → verify. + +As written, Phases 1–3 authored `add_finding` calls and tests against the old signature and Phase 4 +then rewrote all **12 call sites** (Phase 6 ×2, Phase 7 ×3, Phase 8 ×3, Phase 9 ×4). Every prose +finding would be written twice and its tests touched twice, with the mechanical 12-site sweep +landing last — where a missed site is likeliest and hardest to spot against three phases of other +changes. Going first also lands the widest change against an unmodified baseline, so the full suite +is a clean control. + +**Accepted cost:** the largest-blast-radius change precedes the bug fixes, so stopping halfway +leaves the HIGH findings open. + +### D8 — Fixtures: **rewrite, never delete** + +**Locked:** the plan's "delete … or rewrite them" is narrowed to rewrite-only. + +`defect/release-date-far-edge.md` is the positive control for the off-by-one `/code-review` caught. +D6 keeps a window (a heading opens it, claims are collected below), so **the off-by-one class is +still live** — deleting the fixture would re-open a bug a reviewer already found once, and the plan +permitted exactly that with an "or". Rewritten as: heading at line N, disagreeing claim at the +window's last line → still RED. `defect/version-box-stale-date.md` becomes an in-box disagreement, +since boxes are where the original REFCARD bug lived. + +**Generalized into Friction Prevention:** never delete a positive control to make a redesign +easier; if its model is obsolete, rewrite it so the boundary it pinned stays pinned. + +### D9 — Normalize the two claim sites + +**Locked:** make `docs/NEWS.md` and `docs/REFCARD.md` agree exactly, and record the convention. + +Evidence gathered while grilling the plan — craft has exactly two release-date claim sites for the +current version, and they already disagree: + +| Site | Claim | +|---|---| +| `docs/NEWS.md:9` | `**Released:** 2026-08-08` (UTC date) | +| `docs/REFCARD.md:7` | `│ Version: 4.5.0 (released 2026-08-07)` (tag-local date) | + +Both are reachable under D6, so check 1 fires on craft today and **only the one-day tolerance keeps +it GREEN**. With D2 making check 1 RED, the repo would sit permanently one day from a blocked +release, with no warning beforehand because one day always reads GREEN. Normalizing returns the +tolerance to headroom; it still exists for the real UTC-boundary cause D1 preserved. + +**Rejected:** widening the tolerance to 2 days buys headroom by blunting the check; dropping it +entirely throws away the UTC handling D1 deliberately kept. + +### D10 — Phase 6 must carry the workflow steps + +**Locked:** Phase 6 refreshes the `.STATUS` worktree row, and the ORCHESTRATE file is deleted at +merge. + +Both are required by craft's own `CLAUDE.md` and **neither is enforced** — `ORCHESTRATE-*.md` is +not gitignored, not in `exclusions.txt`, and no test guards it. The `.STATUS` row still describes +this branch as only the parent SPEC and its harness. Shipping a doc-staleness feature while leaving +a stale status row and a working artifact on `dev` would be its own small joke. + +### D11 — RED promotion is **evidence-gated** + +**Locked:** check 1 ships as `warning` in the severity phase; promotion to `error` happens in +Phase 6 only after it runs clean across every tracked `.md` and both known claim sites, transcript +quoted. If not clean, it stays `warning` and promotion becomes a follow-up. + +Check 1 is being *redesigned* (D1, D6) and *promoted to release-blocking* (D2) in the same PR. The +only evidence it is sound would otherwise be tests written alongside it by the same author in the +same sitting. A false-positive class in a freshly-redesigned blocking check is discovered at the +worst possible moment — mid-release. + +This does not reverse D2; it makes RED earned rather than assumed. + +**Also recorded:** the plan's original 2-hour estimate was optimistic. Phase 1 alone is 12 call +sites plus JSON rendering plus both fix passes plus an end-to-end exclusion test. Revised to ~3 +hours. + +--- + +## Not branched (no genuine alternative) + +- **F8** — unclosed `┌` leaks `version-box` mode into the rest of the file, losing the + `structure-table` type restriction. Fix: close the box on the first line carrying no box + character. +- **F9** — REFCARD says `tldr` matches "a line containing `TL;DR`"; the regex requires the line to + *open* with it. The regex is deliberate (`falsepos/tldr-mentioned-not-claimed.md` depends on + it). Fix the doc. + +## Open questions + +- **D1 leaves single-claim files unguarded.** If only `docs/NEWS.md` carries a release date for the + current version, cross-file consistency says nothing. Worth revisiting once a second claim site + is guaranteed — not blocking, since the status quo catches nothing anywhere. +- **Phase 9's severity is untouched.** D2 sets per-check severity in Phase 7 only; whether Phase 9 + should follow is unexamined. +- **The floor's 40% constant is still unjustified.** D3 puts a minimum under it but does not ask + whether 40% was ever the right shape. Inherited from the pre-existing broad scan. + +## Next step + +`/craft:plan docs/specs/SPEC-prose-check-hardening-2026-08-15.md` → `plan-orchestrator` → +`ORCHESTRATE-*.md`, then implement on `feature/doc-staleness-prose-gaps` per D5. diff --git a/docs/specs/REVIEW-repo-triage-2026-08-07.md b/docs/specs/REVIEW-repo-triage-2026-08-07.md index 060a21de5..a8a53552a 100644 --- a/docs/specs/REVIEW-repo-triage-2026-08-07.md +++ b/docs/specs/REVIEW-repo-triage-2026-08-07.md @@ -1,6 +1,6 @@ # ADVERSARIAL REVIEW: Repo Triage -**Date:** 2026-08-07 · **Reviewed:** [`BRAINSTORM-repo-triage-2026-08-07.md`](BRAINSTORM-repo-triage-2026-08-07.md), [`GRILL-repo-triage-2026-08-07.md`](GRILL-repo-triage-2026-08-07.md) +**Date:** 2026-08-07 · **Reviewed:** [`BRAINSTORM-repo-triage-2026-08-07.md`](BRAINSTORM-repo-triage-2026-08-07.md), [`GRILL-repo-triage-2026-08-07.md`](_archive/GRILL-repo-triage-2026-08-07.md) **Method:** 4 independent parallel lenses (backend/systems, frontend/interaction, architecture, ADHD-friendly cognitive-load), each grounded against the real codebase, not just the planning docs. ## Verdict: 2 of the GRILL's 8 locked decisions rest on factually wrong premises diff --git a/docs/specs/SPEC-codex-opencode-delegation-2026-08-08.md b/docs/specs/SPEC-codex-opencode-delegation-2026-08-08.md new file mode 100644 index 000000000..0000643d7 --- /dev/null +++ b/docs/specs/SPEC-codex-opencode-delegation-2026-08-08.md @@ -0,0 +1,137 @@ +# Codex/OpenCode Delegation Routing — Spec + +**Generated:** 2026-08-08 · **Status:** draft — reviewed +**Sources:** [`BRAINSTORM-codex-opencode-delegation-2026-08-08.md`](BRAINSTORM-codex-opencode-delegation-2026-08-08.md) · +[`GRILL-codex-opencode-delegation-2026-08-08.md`](GRILL-codex-opencode-delegation-2026-08-08.md) (5 branches locked) + +--- + +## TL;DR + +3 delegation tools installed (`codex`, `opencode`, `opencode-async`), zero guidance on which +one to use, no way to watch cost or status while a delegated task runs. This spec adds both: +a short routing table + a mandatory "always monitor" step. + +## Problem + +3 delegation surfaces, no documented rule for which to use, no budget/failure guardrails. +Each tool's own docs cover *how* to call it — none cover *when*, or how to watch it while it +runs (tonight's session had to hand-build a token/status monitor from scratch each time). + +## Routing Table (the core deliverable) + +| Situation | Use | Why | +|---|---|---| +| Faster to just do it yourself | **Don't delegate** | Round-trip costs more than the task | +| Security / architecture / can't verify the result | **Don't delegate** | Not verifiable after the fact | +| Hard, well-scoped SWE task (deep bug, tricky refactor) | `codex` | Same shape of task the `codex` plugin's own `codex-rescue` agent proactively targets — craft calls `codex exec` directly, not that agent (different plugin, no cross-plugin dispatch) | +| One-shot question, need the answer this turn | `opencode` (`opencode_ask`/`opencode_run`) | Sync tier, blocks until done | +| Long task, don't want to block the session | `opencode-async` | Fire-and-forget, poll later | + +## Mandatory: Monitor Every Delegation (new — the ADHD-friendly part) + +Never fire-and-forget silently. Every delegation gets a live status+token watch, using the +`Monitor` pattern already proven tonight (codex + opencode reviews were both watched this way): + +1. **Fire the call in the background** — never block the session on a delegated task. +2. **Watch for 3 things only** — status change, token/cost usage, error/failure. Nothing else + is worth a notification (matches ADHD rule: filter to signal, not raw logs). +3. **One summary line per event** — what happened, in plain language, not raw JSON. +4. **Report cost + findings together at the end** — see tonight's side-by-side table + (codex: 1.01M tokens; opencode/deepseek-v4-pro: 1,555 tokens, ~650× cheaper **by token + count** on the same task) as the model for what "done" looks like. Caveat: that 650× + figure compares tokens, not dollars — codex ran on a flat subscription (sunk cost), + `opencode-go` bills per call. Token count is still a real signal (context pressure, rate + limits), just don't read it as "codex costs 650× more in cash." + +This closes the gap the Problem section names — "no way to watch cost while it runs" — as a +required step, not an afterthought. + +## Decisions (from BRAINSTORM answers + this review) + +| # | Decision | +|---|---| +| D1 | Only the 3 installed tools — no new tooling evaluation this pass | +| D2 | Must work standalone today AND be structured for a future skill to consume | +| D3 | Routing table above (corrected post-review — brainstorm had 2 factual bugs, both fixed) | +| D4 | Flexible timeline, quality over speed | +| D5 | **Monitoring is mandatory**, not optional — see section above (elevated from "budget practice" to a concrete required step this rewrite) | +| D6 | OpenCode's two bridges use **different** status words — don't conflate them: `opencode-async` = `working`/`input_required`/`completed`/`failed`/`cancelled`; `opencode` = `running`/`completed`/`error` | +| D7 | `--delegate` defaults **OFF** wherever it ships — matches `--refine`'s OFF-default precedent for execution-engine commands. Delegation costs real money/tokens and hits a network dependency; it must never fire silently. | +| D8 | Any future command implementing `--delegate` **must** ship a test asserting its dispatch path includes a status/token check — same enforcement pattern as `--refine`'s `test_refine_default_policy_table_exhaustive`. Doc-only guidance isn't enough (craft's own hooks-as-defense-in-depth precedent). | +| D9 | **Scope narrowed (2026-08-08, post-grill):** only 2 use cases move forward — adversarial review and brainstorm research. The 4-command `--delegate` rollout below is parked, not cancelled. | + +## Use Cases (narrowed scope — 2 in, 4 parked) + +### 1. Adversarial review — `arch:review` + +Already dogfooded tonight (both reviewers, on this exact spec's source docs). `arch:review` +is the one command that gets `--delegate` now: routes to `opencode` (sync tier, +`opencode_ask`/`opencode_run`) for a cheap second opinion, `codex` available as an explicit +override for a deeper pass. Same routing-table row and monitoring requirement (D5/D8) as +before — narrowing is about *which commands*, not the routing/monitoring design itself. + +### 2. Brainstorm research + +The `brainstorm` skill's existing "Research Findings (web)" step (used tonight via +`WebSearch`) gets an optional escalation: when a topic needs deeper investigation than a web +search provides, delegate to `opencode`/`codex` the same way, still monitored per D5. Not a +new command — an enhancement to `skills/workflow/brainstorm/SKILL.md`'s existing research +step. + +### Parked (not built, not cancelled) + +`code:debug`, `code:refactor`, `code:test-gen`, `ci:triage` — all 4 were scoped and grilled +(GRILL Branches 1 and 3 apply to them), but user chose to limit rollout to the 2 use cases +above for now. Revisit this list before building any of them; nothing here expires. + +**Explicitly excluded (not just parked):** `orch`, `orch:drive`, `orch:workflow`. These already +run their own native multi-agent delegation (wave checkpoints, model routing, +confirm-before-spawn). Adding external delegation here would recreate the exact anti-pattern +the `--refine`/brainstorm redesign already removed once — "one delegation mechanism, not two" +(see `skills/workflow/brainstorm/SKILL.md`'s "Going Deeper" section). + +## Scope + +**In:** + +- One guidance doc: routing table + monitor-every-delegation step + the two-vocabulary note. +- Placement: fold into the existing 2 tutorials (`TUTORIAL-opencode-mcp-plugin.md`, + `TUTORIAL-codex-plugin.md`), cross-linked — cheapest option, no new skill needed for a doc + this short. +- A copy-paste `Monitor` command template for each of the 3 tools (codex, opencode sync, + opencode-async), so "watch this delegation" isn't rebuilt from scratch again. + +**Out:** + +- The 4 parked commands (`code:debug`, `code:refactor`, `code:test-gen`, `ci:triage`) — scoped + and grilled, not built (D9). +- A working `arch:review --delegate` / brainstorm-escalation dispatch implementation — D2 only + needs the rule structured for future use; this pass is docs + design, not code. +- Evaluating tools beyond the 3 installed. +- Wiring `orchestrate-dispatch`'s existing hang/budget detection onto these external tools — + real gap, real follow-up, not this spec. + +## Acceptance Criteria + +- [ ] Routing table + monitor-every-delegation step live in both tutorials (or one shared + location both link to) +- [ ] A ready-to-copy `Monitor` template exists for all 3 tools +- [ ] Two-vocabulary note present and correct (guards against reintroducing finding F2) +- [ ] `arch:review`'s eventual `--delegate` flag defaults OFF (D7), enforced by a test once + built (D8) — the brainstorm-research escalation (use case 2) has no flag to default, + it's a skill-internal choice already gated by the skill's own interactive flow +- [ ] `markdownlint` + `mkdocs build --strict` clean + +## Test Plan + +| Tier | Status | +|---|---| +| e2e / dogfood | N/A — doc-only change, no new command/skill | +| everything else | N/A — no code, no parser, no data flow | + +## Next + +1. Implement into the 2 tutorials + Monitor templates. +2. Verify acceptance criteria. +3. `/craft:finish` — closes tonight's delegation-tooling thread. diff --git a/docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md b/docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md new file mode 100644 index 000000000..6fc819091 --- /dev/null +++ b/docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md @@ -0,0 +1,380 @@ +# Doc-Staleness Prose Gaps — Spec + +**Generated:** 2026-08-07 +**Context:** Craft Plugin v4.5.0 — extends `scripts/docs-staleness-check.sh` Phase 7 +**Sources:** [`BRAINSTORM-doc-staleness-prose-gaps-2026-08-07.md`](BRAINSTORM-doc-staleness-prose-gaps-2026-08-07.md) (6 locked decisions, external research) +**Status:** draft — revised post adversarial review (see "Review Outcome" below); amended +2026-08-15 with an ADR requirement, a test-harness design, and two new pieces of live evidence +(see "Amendment" below) + +--- + +## Amendment (2026-08-15) + +Two real staleness bugs surfaced during a `/savant:restore` currency read on `dev` at +`7db7b565c`. Both sharpen this SPEC's design rather than changing its scope. + +### E1 — Phase 7's existing regex misses the singular noun form + +`CLAUDE.md`'s Project Structure table row for the `agents/` directory read +**8 agent definitions** while the real count had been 2 since v4.0.0. Phase 7 reported **GREEN** over it for five minors. Cause: +the scan pattern is `\b[0-9]+ ${ctype}\b` where `ctype ∈ {commands, skills, agents}` — strictly +plural (`scripts/docs-staleness-check.sh:345`). `8 agent definitions` never matched. The 40% +minimum threshold (line 302) was not the cause; 8 clears it. + +**Resolution (revised during build).** The first reading of this finding was "fold the singular +alternation into the shared matcher" — i.e. widen the broad scan at line 345. That reopens +exactly the false-positive surface review finding 3 closed, and adds singular forms of it +("the 4 agent limit") on top. + +The singular form is therefore checked **only inside check 2's line shapes**, and the broad scan +is left plural-only. `CLAUDE.md`'s Project Structure row is itself a structured shape — a table +row whose first cell names a counted directory — so E1 is caught by adding a **fourth line +shape** (`structure-table`), not by widening anything. That row's count is compared only against +the type its own first cell names, so a `commands/` row is never measured against the agent count. + +### E2 — "release date" has two defensible authorities + +`docs/NEWS.md` claims `**Released:** 2026-08-08` for v4.5.0; `.STATUS` has +`release_date: 2026-08-07`. Neither is wrong — the GitHub release published at +`2026-08-08T03:44:25Z`, which is 2026-08-07 21:44 local. Check 1 as written ("must match +`.STATUS`'s `release_date:`") would flag this as RED on its first run against a correct repo. + +Check 1 must therefore pick **one** authority and normalize: compare against the annotated git +tag's local date (`git for-each-ref --format='%(creatordate:short)' refs/tags/vX.Y.Z`), and +accept a ±1-day window against `.STATUS`'s `release_date:` to absorb the UTC boundary. A +same-day-either-side match is not staleness. + +### Severity — resolved: `warning` + +Existing Phase 7 findings are emitted at severity `warning` (`add_finding 7 "warning" ...`, +line 341) while this SPEC's first draft specified checks 1–2 as blocking RED. + +**Resolved to `warning`**, per ADR-007. `main()` exits 1 for warnings and errors alike, so both +already fail `/craft:check --for release` and `pre-release-check.sh` — the divergence is the +RED/YELLOW label, not whether the gate blocks. These are the first checks in this repo to judge +prose rather than structured tokens, and the first build of check 2 produced five false +positives before the 40% floor was added; ADR-003's gentle-ramp precedent applies. `error` is +earned after the checks run clean across a few real releases. + +### Post-build findings (2026-08-15) — four bugs, four different catchers + +Every one of these was in the first build and none was caught by the gate that "should" have. +Recorded because the pattern is the point: no single review mechanism found more than one. + +| # | Bug | Caught by | Fix | +|---|---|---|---| +| B1 | Script ran at 36s against a dev baseline of 8.0s — ~1300 per-file `awk` spawns plus ~21000 `grep` calls over every line of every box block | the **existing suite** — a 30s timeout in `test_pre_release_check_runs` | one `awk` pass over the file list, an in-awk `hascount()` pre-filter, date window computed once. 8.7s | +| B2 | An unparseable authority date left an empty accept-window, and an empty window matches nothing — so **every** release-date claim in the repo was flagged at once | **self-review** of the PR | the check is vacuous unless the authority parsed, same posture as the no-tag case | +| B3 | `win = 4` scanned the version line plus only **3** more, because `win--` runs on the version line itself — one short of the 4 following lines this SPEC and the REFCARD both advertise | **`/code-review`** | `win = 5`; defect fixture placed at exactly the far edge | +| B4 | `pass2_interactive_review`'s `[f]ix` printed `-> Fixed` and incremented `TOTAL_FIXED` without touching the file | **reading adjacent code** while tracing where findings are routed | pass 1's applier extracted to `apply_line_fix`, shared by both passes | + +Two of these are the same failure mode wearing different clothes — **a check that reports a +result it did not actually establish**. B2 flags without evidence; B4 claims a fix it never made. +B4 is also a *recurrence*: pass 1 had this exact bug (BSD `sed -i` exiting 0 on no match) and was +fixed for it; the sibling pass was left behind. Sharing one applier is what closes the class, not +patching the second site. + +### Fix routing, decided here + +Both prose checks emit `uncertain`, so they land in pass 2's interactive review rather than pass +1's auto-apply. The surrounding prose is hand-authored — a human should see the line before the +number changes under it. But the `fix_detail` is a real `s/…/…/` substitution, not a +human-readable note, so confirming one actually edits the file; it swaps the digits only, leaving +`agent definitions` intact. Phase 8's doc-coverage findings carry a `doc-coverage:surface:cmd` +marker instead, and `apply_line_fix` refuses to execute anything that is not a substitution. + +### Build-time finding: the floor applies to shaped lines too + +The first build of check 2 ran without the broad scan's 40%-of-expected floor, on the assumption +that structured shapes are self-limiting. They are not. Five legitimate counts were flagged on +`dev` immediately: category subtotals inside reference boxes (`SMART (4 commands)`, +`Code (12 commands)`), a bolded subset count (`` `--refine` is declared on **9 commands** ``), and +a narrative count about a different plugin (`kept shipping **0 skills**`). Shape membership +narrows *where* to look; the floor is still what separates a total from a subtotal. All five are +now `falsepos/` fixtures. + +### Superseded by the prose-check hardening (2026-08-15) + +This SPEC's check-1 description above (E2, and the "git tag local date" line in the Acceptance +Criteria) describes the **as-shipped-then** design, not the current one. A follow-up review found +9 more defects in the shipped checks (2 HIGH); see +[`SPEC-prose-check-hardening-2026-08-15.md`](SPEC-prose-check-hardening-2026-08-15.md) and its +[`GRILL`](GRILL-prose-check-hardening-2026-08-15.md). The load-bearing change: check 1 no longer +resolves an authority from the git tag at all (D1) — the tag doesn't exist at either point the +check actually runs, so a tag-based check never fired as a real gate. It now compares every +release-date claim for the current version against every other one, with the one-day window +surviving as the agreement tolerance between claims rather than a tolerance against a tag. Left +here rather than rewritten in place, per this repo's own "positive controls are rewritten, not +deleted, when their model goes obsolete" convention (that hardening's D8) — the checkboxes above +are a historical record of what E2 decided at the time, not a live spec. + +--- + +## Review Outcome (adversarial review, 2026-08-07) + +Original draft (checks 1–3 as first written) was adversarially reviewed before build. 3 +blocking findings, all fixed in this revision — see inline `> **Review:**` notes below for +what changed and why: + +1. **Check 3's proxy was falsified by the exact incident it targets.** `bump-version.sh` + touches `docs/REFCARD.md`'s version line in every release commit — so REFCARD.md's + "last git-log touch" always coincides with the bump commit, meaning the whole-file proxy + would report "not stale" on the file that caused this SPEC to exist. **Fix: dropped + check 3 entirely** — a per-line/per-span version would work but adds real complexity for + an advisory-only signal; not worth building until a simpler check proves insufficient + (gentle-ramp, not preemptive). +2. **Check 3's stated target (`docs/index.md`) was already whole-file excluded** from Phase 7 + via `exclusions.txt`'s "curated hub page" entry — moot once check 3 is dropped. +3. **Check 2's false-positive surface was far larger than the 3-file exclusion list.** A grep + of `docs/**/*.md` found 90+ hits for count-shaped prose outside the proposed exclusions, + including legitimate non-count-of-agents phrases (`docs/guide/orch-flag-usage.md`: "2 + agents max", "4 agents" in orchestration mode-limit prose), a tutorial with an + intentionally fictional example plugin, and a troubleshooting page that intentionally + shows a wrong count to illustrate the exact bug it's teaching. **Fix: check 2 rescoped + to structured line shapes only** (TL;DR lines, quick-reference/version boxes, badge + lines) — not a blanket `\d+ agents?` search anywhere in prose. See revised check 2 below. +4. **`exclusions.txt` maintainability concern raised, accepted as a known tradeoff** — not + blocking. It's already craft's established mechanism for this class of exception (used by + nav-completeness today); rescoping check 2 to structured lines (finding 3's fix) also + shrinks how much the exclusion list needs to grow. +5. **Scope (craft-only) and acceptance criteria confirmed fine as-is** — no changes. + +## Problem + +`docs-staleness-check.sh`'s count-consistency phase (Phase 7) anchors on specific structured +patterns — badges (`version-X.Y.Z`), declared count lines (`N commands`) — and reported GREEN +today while two real staleness bugs sat live in `docs/REFCARD.md` and `docs/skills-agents.md`: +a version box with the right number but a wrong release date and a stale highlight line +describing the prior release, and a TL;DR line claiming "8 specialized agents" two lines above +its own correct "2 specialized agents". Both are free prose the gate doesn't watch. + +## User Story + +As a maintainer running `/craft:check --for release` or `docs-staleness-check.sh` directly, I +want the gate to catch the specific prose-staleness shapes that have actually bitten this repo +(stale release dates, stale agent/skill-count mentions, stale version-highlight lines) so a +release doesn't ship with correct version *numbers* sitting next to incorrect surrounding prose. + +## Locked Decisions (from BRAINSTORM) + +| # | Decision | +|---|---| +| D1 | Extend `docs-staleness-check.sh` directly — no new script, no new dependency. | +| D2 | Scope: craft only, not the dev-tools-wide shared tooling. | +| D3 | Targeted prose-pattern regexes, not general NLP/semantic diffing. | +| D4 | No external tool adoption (Vale/drift/LLM-in-CI) — pattern-first shape borrowed, not the tools themselves. | +| D5 | 3 concrete checks (see below), each with a clean-fixture and planted-defect-fixture regression test. | + +## Scope + +### New checks (Phase 7 extension) + +1. **Release-date claims.** Any doc prose matching `released? \d{4}-\d{2}-\d{2}` (or + `\(released ...\)`) tied to the *current* version token must match `.STATUS`'s + `release_date:` field. Flag on mismatch. Blocking (RED). + +2. **Agent/skill count prose — structured lines only.** + > **Review:** rescoped from a blanket `\d+ agents?`/`\d+ skills?` search (which false-positived + > on orchestration mode-limit prose, a fictional-plugin tutorial, and an intentional-bug + > troubleshooting example) to specific known-good line shapes only: + - Lines matching a "TL;DR"-prefixed sentence (e.g. `docs/skills-agents.md`'s pattern). + - Lines inside a fenced quick-reference/version box (`┌─...─┐` ... `└─...─┘` block, e.g. + `docs/REFCARD.md`'s header box). + - Lines matching an explicit badge/count-summary pattern already used elsewhere + (`**N commands** · **N skills** · **N agents**`, as in `docs/QUICK-START.md`/`docs/index.md`). + + Within those line shapes only, `\d+ (specialized )?agents?` / `\d+ skills?` mentions must + match `plugin.json`'s current counts. Historical-log files (`VERSION-HISTORY.md`, + `CHANGELOG.md` past-version sections, `docs/MIGRATION-v4.md`) stay excluded via + `scripts/config/exclusions.txt`, same mechanism as today's nav-completeness exclusions — + a smaller list now that the check is line-shape-scoped, not file-wide. Blocking (RED). + +3. ~~Version-highlight staleness proxy~~ — **dropped.** + > **Review:** the proposed whole-file git-log-touch proxy is falsified by + > `bump-version.sh` itself, which touches `docs/REFCARD.md`'s version line on every + > release — the exact file this check was meant to catch would always read "recently + > touched, not stale." A correct version requires per-line/per-span touch tracking, + > which is real complexity for an advisory-only signal. Not building until checks 1–2 + > prove insufficient on their own (gentle-ramp). + +### Out of scope + +- General semantic/LLM-based staleness detection (D3/D4). +- Porting the pattern to sibling dev-tools repos (D2) — revisit only if this proves out here. +- Any new CI workflow, cron job, or external service. + +### Follow-up (found 2026-08-07, not in this SPEC's scope) + +`homebrew-tap`'s `generator/manifest.json` has the same prose-staleness blind spot, in a +different repo: `command_count` is auto-synced by `homebrew-release.yml` on every version +release, but each formula's `caveats_extra` fixed prose (command names, feature bullets) is +hand-authored and never re-verified — confirmed live when craft's own caveats text still +referenced the removed `/craft:git:unprotect` command (fixed in `homebrew-tap#211`). Neither +`check-drift.sh` (formula-matches-manifest) nor `check-revision-bump.sh` (content-changed-needs- +revision) checks prose *accuracy*, only structural consistency. This SPEC's checks 1–2 don't +reach `homebrew-tap` (out of scope per D2 — craft only). If this pattern proves out here, +consider a parallel prose-accuracy check in `homebrew-tap` itself (different repo, different +owner of that decision) — not assumed, not scheduled. + +## ADR — ADR-007: pattern-scoped prose gating in the existing staleness script + +This SPEC commits craft to a position it has not written down anywhere: that documentation +*prose* is gated by narrow, hand-authored line-shape patterns inside +`scripts/docs-staleness-check.sh`, and explicitly **not** by a prose linter (Vale), a semantic +differ, or an LLM-in-CI pass. That choice constrains every future staleness check, so it gets a +record rather than living implicitly across D1/D3/D4 in a BRAINSTORM. + +**File:** `docs/adr/ADR-007-pattern-scoped-prose-staleness-gating.md`, following the existing +ADR-001…ADR-006 format in that directory. + +Content it must record: + +| Element | Substance | +|---|---| +| **Context** | Phase 7 anchors on structured patterns and reported GREEN over four real bugs: REFCARD.md's stale release date, skills-agents.md's "8 specialized agents", CLAUDE.md's "8 agent definitions" (E1), README.md's v2.36.0 highlight block five minors after v4.5.0. | +| **Decision** | Extend Phase 7 in place with line-shape-scoped regexes. No new script (D1), no external prose tool (D4), no semantic layer (D3). Craft-only (D2). | +| **Consequences — accepted** | Coverage is exactly as good as the enumerated line shapes; every genuinely new prose shape needs a code change, not a config change. `scripts/config/exclusions.txt` grows over time (accepted tradeoff, review finding 4). | +| **Consequences — rejected alternatives** | Vale/`drift`/LLM-in-CI (D4) — rejected for dependency weight and non-determinism in a release gate. Whole-file git-log-touch proxy — rejected as *falsified*, not merely unattractive: `bump-version.sh` touches REFCARD.md every release, so the proxy reads "fresh" on the file that motivated this work. | +| **Severity posture** | Records whether checks 1–2 emit `error` (blocking) or `warning`, and why — the divergence flagged in the Amendment above. | +| **Revisit trigger** | A prose staleness bug that lands despite checks 1–2, whose shape cannot be expressed as a line-shape regex. That, and only that, reopens the D3/D4 rejection. | + +## Test Harness + +The Test Plan below names tiers; this section specifies the rig they run on. Both checks need +the same three-way verdict pattern (clean → GREEN, planted defect → RED, known false-positive +source → GREEN), and the adversarial review's three false-positive sources are currently prose +in an acceptance criterion rather than executable rows. A table-driven harness makes them +first-class. + +### Layout + +```text +tests/fixtures/prose-staleness/ + clean/ + version-box-correct.md # correct date + counts inside a ┌─┐ box + tldr-correct.md # correct "2 specialized agents" TL;DR line + structure-table-correct.md # correct "2 agent definitions" (E1 regression) + release-date-utc-boundary.md # date 1 day off the tag — the UTC case (E2) + defect/ + version-box-stale-date.md # check 1 — date well off the tag + release-date-far-edge.md # check 1 — stale date at the window's last line (B3) + tldr-eight-agents.md # check 2 — "8 specialized agents" in a TL;DR line + structure-table-singular.md # check 2 — "8 agent definitions", singular form (E1) + falsepos/ + category-subtotal-box.md # "(4 commands)" subtotals inside a reference box + subset-bold-count.md # "**9 commands**" subset + "**0 skills**" narrative + mode-limit-prose.md # "2 agents max" / "4 agents" mode-limit prose +``` + +> **Revised during build.** The review's other two named false-positive sources (the +> fictional-plugin tutorial and the intentional-wrong-count troubleshooting page) are covered by +> path-keyed entries in `exclusions.txt`, so a fixture of them would re-prove the exclusion +> loader rather than this SPEC's new code. They were replaced by the three sources the first +> build of check 2 actually flagged, each isolating a distinct guard: the 40% floor +> (`category-subtotal-box`, `subset-bold-count`) and shape scoping plus exclusion inheritance +> (`mode-limit-prose`, written to its real path so the path-keyed exclusion applies). Ten +> fixtures, not nine. + +### Contract + +Each fixture is a **complete minimal markdown file**, not a snippet — the checks operate on +line context (fenced-box membership, TL;DR prefix), so a snippet would not exercise the same +code path. Every fixture carries a one-line HTML comment header stating what it proves, so a +later reader does not have to infer intent from filename alone. + +The runner (`tests/test_docs_staleness_prose.py`, alongside the existing +`tests/test_docs_staleness.py`) is a single parametrized case over a declared table: + +| Fixture | Check | Expected | Proves | +|---|---|---|---| +| `clean/version-box-correct.md` | 1 | GREEN | no false positive on correct content | +| `clean/release-date-utc-boundary.md` | 1 | GREEN | E2: a one-day gap is the UTC boundary | +| `defect/version-box-stale-date.md` | 1 | RED | planted-defect positive control | +| `defect/release-date-far-edge.md` | 1 | RED | B3: the window reaches its documented last line | +| `clean/tldr-correct.md` | 2 | GREEN | correct counts in a TL;DR line | +| `clean/structure-table-correct.md` | 2 | GREEN | E1 stays fixed | +| `defect/tldr-eight-agents.md` | 2 | RED | the original review bug | +| `defect/structure-table-singular.md` | 2 | RED | E1 would be caught, not missed | +| `falsepos/category-subtotal-box.md` | 2 | GREEN | subtotals in a box are not the total | +| `falsepos/subset-bold-count.md` | 2 | GREEN | a bolded count can be a subset | +| `falsepos/mode-limit-prose.md` | 2 | GREEN | free prose is out of shape scope | + +Each case is run against a throwaway repo holding exactly one fixture document, invoked with +`--json` so the assertion reads the structured `findings[]` array rather than parsing colored +terminal output — the same reason `docs-staleness-check.sh` already ships `--json`. The +destination path inside that repo is declared per case, because the exclusion that protects +`mode-limit-prose` is path-keyed. + +Two harness-level tests sit alongside the table: one asserting every check still owns a +`defect/` fixture (so a future check cannot ship happy-path-only), and one running the real +`dev` tree and asserting `count_consistency` has zero findings — the assertion that caught the +missing 40% floor. + +### Harness requirements + +- **The harness must be able to fail.** Adding a check without its `defect/` row is a rejected + change; the positive control is the point (see `e2e-before-pr.md`). +- **No network, no git-history walk, no TTY.** Fixtures are self-contained files; the one git + read check 1 needs (tag date) is injected via `CRAFT_RELEASE_DATE`, and the expected counts + via `CRAFT_EXPECTED_{CMDS,SKILLS,AGENTS}`, so the suite is hermetic and no fixture has to + materialize 48 command files. Both overrides are unset on every production path. +- **Fixture authority values are pinned, not derived.** The harness hardcodes 48/41/2 and the + v4.5.0 tag date rather than reading the live repo, so a future count change cannot silently + turn a planted defect into a non-defect (an "8 agents" defect stops proving anything the day + craft ships 8 agents). +- **Reuse, don't fork.** `scripts/config/exclusions.txt` handling, `is_file_excluded`, and + `is_pattern_excluded` are called as-is — the harness tests the real code path, not a copy. + +## Acceptance Criteria + +- [x] Checks 1 and 2 are implemented in Phase 7 at severity `warning`, with the divergence from + the draft's "blocking RED" recorded as a deliberate choice in ADR-007's Severity section. +- [x] The **singular** noun form (`N agent definitions`) is checked (E1) — inside check 2's line + shapes only, not by widening the broad scan; `clean/structure-table-correct.md` + + `defect/structure-table-singular.md` prove both directions. +- [x] Check 1 resolves the release-date authority per E2 (git tag local date, one-day window) + and does **not** flag the current `docs/NEWS.md` v4.5.0 entry — `clean/release-date-utc-boundary.md` + pins it. +- [x] `docs/adr/ADR-007-pattern-scoped-prose-staleness-gating.md` exists and records every row + of the ADR table above, including the rejected alternatives and the revisit trigger. +- [x] The test harness exists at `tests/fixtures/prose-staleness/` (11 fixtures — see the + revision note in Test Harness) with a table-driven runner asserting against `--json`. +- [x] A broken release-date authority makes the check **vacuous, never universal** (B2), pinned by + `test_unparseable_authority_date_is_vacuous_not_universal`. +- [x] The release-date window reaches the last line it documents (B3), pinned by + `defect/release-date-far-edge.md` at exactly that boundary. +- [x] A reported fix actually edits the file (B4) — `apply_line_fix` is shared by both passes and + pinned by two tests, including that a `false` result leaves the file byte-identical. +- [x] Runtime stays within the existing `test_pre_release_check_runs` budget (B1): 8.7s against a + dev baseline of 8.0s, versus 36s before the fix. +- [x] Check 2's line-shape scoping is tested against real false-positive sources — revised from + the review's 3 named files to the 3 the first build actually flagged, since two of the + originals are covered by path-keyed exclusions rather than by this SPEC's code. +- [x] Regression fixtures restore the exact bugs (stale release date, "8 specialized agents", + "8 agent definitions") and prove the new checks catch them. Positive controls verified by + two planted mutations: removing the 40% floor fails 3 tests, dropping the singular + alternation fails exactly the E1 defect test. +- [x] Running the check against current `dev` HEAD stays GREEN — asserted continuously by + `test_live_repo_stays_green_on_count_consistency`, not just checked once by hand. +- [x] `docs/reference/REFCARD-DOCS-STALENESS.md` documents the new checks, the four line shapes, + and the two test-only env overrides in its existing inventory. +- [x] `CHANGELOG.md` / `docs/CHANGELOG.md` `[Unreleased]` gets an entry. +- [x] No new script, no new external dependency (D1/D4) — the checks live inside + `docs-staleness-check.sh`, using awk and the python3 it already requires. + +## Test Plan + +| Tier | Coverage | +|---|---| +| `unit` | New pattern-matcher functions, tested against clean + planted-defect fixtures for both checks, plus the 3 false-positive-source fixtures the review found for check 2. | +| `dogfood` | `docs-staleness-check.sh` on current `dev` HEAD → GREEN. On a git-stash of the pre-fix REFCARD.md/skills-agents.md → RED (regression proof). | +| `e2e` | `/craft:check --for release` runtime unaffected (checks stay in Phase 7's existing budget). | +| `integration` | N/A — no cross-command data flow. | +| `dependency` | N/A — no new dependency. | + +## Next Step + +`/craft:plan docs/specs/SPEC-doc-staleness-prose-gaps-2026-08-07.md` → routes to +`plan-orchestrator` for implementation (small scope, grill optional — low ambiguity, decisions +already locked in the BRAINSTORM). diff --git a/docs/specs/SPEC-prose-check-hardening-2026-08-15.md b/docs/specs/SPEC-prose-check-hardening-2026-08-15.md new file mode 100644 index 000000000..ce0628478 --- /dev/null +++ b/docs/specs/SPEC-prose-check-hardening-2026-08-15.md @@ -0,0 +1,150 @@ +# Prose-Check Hardening — Spec + +**Generated:** 2026-08-15 +**Context:** Craft v4.5.0 — hardens the Phase 7 prose checks added in PR #334 +**Sources:** high-effort `/code-review` of PR #334 (9 findings), plus +[`SPEC-doc-staleness-prose-gaps-2026-08-07.md`](SPEC-doc-staleness-prose-gaps-2026-08-07.md) +and [ADR-007](../adr/ADR-007-pattern-scoped-prose-staleness-gating.md) +**Status:** grilled 2026-08-15 — 5 branches locked, ready to implement. Decisions live in +[`GRILL-prose-check-hardening-2026-08-15.md`](GRILL-prose-check-hardening-2026-08-15.md); where +this spec's option tables and the ledger differ, **the ledger wins**. + +--- + +## Problem + +PR #334 added two prose checks to `docs-staleness-check.sh` Phase 7. CI is green, the full suite +passes, and the live repo reports GREEN. An adversarial review nonetheless found 9 defects, 7 of +which are invisible to every gate the repo has, because the gates only prove the checks don't fire +on *craft's current docs* — not that they fire correctly on any other input. + +Two of the nine make the feature **worse than not having it**, and one suggests it may never fire +where it was designed to. + +## Verified findings + +Each reproduced against the real script before being written down. F1, F2, F6 reproduced by +constructing input; F5, F7 by reading the consuming code. + +| ID | Sev | Defect | Evidence | +|----|-----|--------|----------| +| F1 | HIGH | The singular matcher's `([^a-z]\|$)` trailer accepts `-`, so hyphenated compounds read as counts | `30 command-line entry points` → `'30 command' (expected 48)`; `3 agent-facing surfaces` → `'3 agent' (expected 2)` | +| F2 | HIGH | Accepting the offered fix **corrupts that prose**: the substitution is built from the bare `N noun` prefix | `[f]ix` rewrites `30 command-line entry points` → `48 command-line entry points` | +| F3 | MED | Release-date window is proximity-only — any line mentioning the version opens it, with no requirement the date belongs to that version | an upgrade guide saying "Upgrading to v4.5.0 is a drop-in change" claims the next `Released:` date within 4 lines | +| F4 | MED | Pass 2 `[e]xclude` is a no-op for every Phase 7 prose finding | `file` is now `path:lineno`, so the entry written is `docs/x.md:3:30 command`; `is_pattern_excluded` splits on the first colon and can never match | +| F5 | MED | ADR-007's stated basis for `warning` over `error` is factually wrong | `pre-release-check.sh:280` is headed "warn-only, does not block release" and swallows the exit with `\|\| true`; `docs-quality.yml` sets `continue-on-error: true`; `skills/release/SKILL.md:143` says "RED findings block; YELLOW findings warn" | +| F6 | MED | The 40%-of-expected floor is **0** for agents | `$((2 * 40 / 100))` == 0, so the guard ADR-007 cites as what makes shaped lines safe is absent for the smallest count type | +| F7 | MED | Check 1 is vacuous in both automated environments it targets | `docs-quality.yml` uses `actions/checkout@v5` with no `fetch-tags`, so no tag exists → silent skip on every CI run; and release writes NEWS/REFCARD dates *before* the tag is created | +| F8 | LOW | An unclosed `┌` puts the rest of the file in `version-box` mode, losing the `structure-table` type restriction | a `commands/` row then gets measured against the agent count | +| F9 | LOW | REFCARD says `tldr` matches "a line containing `TL;DR`"; the regex requires the line to *open* with it | doc contradicts implementation | + +### Root causes, not nine unrelated bugs + +Grouping matters more than the count — patching nine sites individually would leave the classes open. + +- **C1 — "one more character class" reasoning.** F1 and F8 are both boundary conditions on a + regex written for the cases in front of it. F1 assumed word-final; F8 assumed boxes close. +- **C2 — a guard cited but not verified.** F6: ADR-007 and two code comments claim the 40% floor + makes shaped lines safe. Nobody evaluated it for the smallest count. F1's agent half is a + *consequence* of F6, not an independent bug. +- **C3 — reporting a result never established.** F2 and F4, both the same family PR #334 already + fixed once in `[f]`. **Corrected during the grill:** F4 was first written up here as introduced + by this PR's change of `file` to `path:lineno`. It is not — + `git show dev:scripts/docs-staleness-check.sh` already has `add_finding 7 "warning" + "${file}:${lineno}"` at line 341, and the same shape in Phase 9 at 497 and 548. `[e]xclude` has + been a no-op for **all** Phase 7 and Phase 9 findings, predating this PR. +- **C4 — the check was never exercised where it runs.** F3, F5, F7: proximity heuristic never + tested against adversarial prose; severity chosen from an unverified claim about consumers; + authority never checked for availability in CI or at release time. + +## Decisions required + +These are open. Options are listed with the tradeoff, not pre-resolved. + +| # | Decision | Options | +|---|----------|---------| +| D1 | F1 trailer fix | (a) `([^a-z-]\|$)` — minimal; (b) also exclude `'` and Unicode dashes; (c) require the noun be followed by whitespace/punctuation via an explicit allowlist | +| D2 | F6 floor for small counts | (a) `max(2, 40%)`; (b) a fixed absolute floor per type; (c) drop the floor inside shapes and rely on tighter shapes instead | +| D3 | F2 substitution safety | (a) anchor the substitution with the same trailing guard as D1; (b) include the full matched span incl. trailing char; (c) stop offering a fix for prose findings and route to manual edit only | +| D4 | F3 window discipline | (a) require the version token be in a **heading** line; (b) require the date line itself to name the version; (c) keep proximity but shrink to 2 lines; (d) accept the false-positive class and exclude case-by-case | +| D5 | F5 severity, re-decided on correct facts | (a) `error` (RED) — the label *is* the gate per `skills/release/SKILL.md:143`; (b) keep `warning` and correct the ADR's reasoning; (c) `error` for check 1, `warning` for check 2 | +| D6 | F7 CI availability | (a) add `fetch-tags: true` to `docs-quality.yml`; (b) change the authority to `.STATUS`'s `release_date:` with the same one-day window; (c) accept vacuity in CI and document it | +| D7 | F7 release-time ordering | (a) move the check after tag creation; (b) compare against the *version being released* rather than the tag; (c) accept and document | +| D8 | F4 exclusion round-trip | (a) split `file` back into path + lineno in the finding record; (b) have `[e]` strip the `:lineno` suffix before writing | +| D9 | F8 box-close discipline | (a) close the box at the first line with no box character; (b) cap box mode at N lines; (c) require a matching `└` within the same fenced block | + +### Non-negotiable, not up for decision + +Anything shipped must not **report a result it did not establish** (ADR-007). D3 option (c) and +D5 are constrained by that: an offered fix must be applicable and correct, and a severity must be +chosen from verified consumer behavior rather than an assumption about exit codes. + +## Scope + +**In:** `scripts/docs-staleness-check.sh` Phase 7 checks and their fix payloads; the pass-2 +`[e]` branch; `docs-quality.yml` checkout config if D6(a); ADR-007's severity section; the REFCARD +shape table; fixtures + harness rows for every fix. + +**In (added by the grill):** Phase 9's `[e]` findings, which carry the same `path:lineno` shape — +the ledger's D4 fixes this at the record level rather than per-phase, so both are covered by one +change. The git-tag authority and `docs-quality.yml`'s checkout config drop out of scope entirely: +D1 replaces the authority with cross-file consistency, so there is no tag to fetch. + +**Out:** the version-highlight proxy (dropped in the parent SPEC, still dropped); porting to +sibling repos; any new script or dependency (D1/D4 of the parent SPEC still hold); Phase 9's +*severity*, which D2 leaves as-is. + +## Acceptance criteria + +- [x] Every finding F1–F9 has a fixture or test that **fails before the fix and passes after**, + verified by planted mutation — not merely a passing test after the change. Every phase's + fix was reverted and re-tested before restoring (F1/F2/F4/F6/F8 confirmed via source + revert; F3/F7 confirmed by reverting D1/D6 together, since they share one redesign; + D2/D11's severity confirmed by reverting the severity string itself). This pass also + caught the broad, unscoped plural-only count scan sharing F1's hyphen-boundary bug + (`"7 agents-only"` matched as `"7 agents"`) — not separately enumerated as an F-number, + surfaced by this criterion's own hyphenated-compound corpus, fixed alongside F1/F2. +- [x] A hyphenated-compound corpus (`command-line`, `agent-facing`, `skill-authoring`, + `agents-only`) produces zero findings, and the corresponding real counts still do — + `falsepos/hyphenated-compound-tldr.md`. +- [x] `[e]xclude` round-trips: writing the entry actually suppresses the finding on the next run, + asserted end-to-end rather than by inspecting the written line — + `test_exclude_round_trips_end_to_end` drives the real `[e]` keystroke through a pty. +- [x] The severity decision (D2, GRILL numbering — the SPEC's own D5 above referred to the + pre-grill options table) is recorded in ADR-007 with the **verified** consumer behavior, + and the incorrect claim is removed, not softened. +- [x] Check 1 demonstrably fires — proven by running it, not by reasoning that it should. D1 + dropped the environment dependency this criterion originally asked about (no tag, no CI + availability concern left to prove); demonstrated instead by injecting a real mismatch + into `docs/REFCARD.md`'s live date and confirming check 1 caught it + (`release date '2020-01-01' ... disagrees with other claims (majority: 2026-08-07)`) + before reverting the injection. +- [x] `docs-staleness-check.sh` stays under the `test_pre_release_check_runs` 30s budget — ~11s. +- [x] Live repo stays GREEN. + +## Test plan + +| Tier | Coverage | +|------|----------| +| unit | `apply_line_fix` substitution safety for hyphenated spans (D3); floor arithmetic at every count magnitude incl. 1 and 2 (D2) | +| fixture | New `falsepos/` entries for hyphenated compounds and unclosed boxes; new `defect/` entries for each still-must-catch case | +| e2e | `[e]xclude` round-trip through a real `exclusions.txt`; check 1 firing in the D6-selected environment | +| dogfood | live repo GREEN; runtime under budget | +| negative | planted mutation per fix — the point is that each new test can fail | + +## Risks + +- **D4 and D6/D7 may interact.** If the authority moves to `.STATUS` (D6b), the release-ordering + problem (D7) changes shape, because `.STATUS` is written in the same commit as the doc dates. +- **Tightening F1 could re-mask the E1 bug** the parent SPEC exists to catch + (`8 agent definitions` — noun followed by a space, so it should survive; must be pinned). +- **D5 → `error` makes this a release blocker.** Every remaining false positive becomes a blocked + release rather than a warning, which raises the bar on D1/D2/D3 being right. + +## Next step + +Grill this spec before implementing. The decisions most worth attacking: **D5** (severity, given +the corrected facts), **D6/D7** (whether check 1 can fire anywhere that matters), and **D4** +(whether proximity is salvageable at all). + +> Interrogated by grill — see [GRILL-prose-check-hardening-2026-08-15.md](GRILL-prose-check-hardening-2026-08-15.md) diff --git a/docs/specs/GRILL-repo-triage-2026-08-07.md b/docs/specs/_archive/GRILL-repo-triage-2026-08-07.md similarity index 100% rename from docs/specs/GRILL-repo-triage-2026-08-07.md rename to docs/specs/_archive/GRILL-repo-triage-2026-08-07.md diff --git a/docs/specs/SPEC-ecosystem-tool-tutorials-2026-08-07.md b/docs/specs/_archive/SPEC-ecosystem-tool-tutorials-2026-08-07.md similarity index 100% rename from docs/specs/SPEC-ecosystem-tool-tutorials-2026-08-07.md rename to docs/specs/_archive/SPEC-ecosystem-tool-tutorials-2026-08-07.md diff --git a/docs/teaching-config-schema.md b/docs/teaching-config-schema.md deleted file mode 100644 index ddf40f3ce..000000000 --- a/docs/teaching-config-schema.md +++ /dev/null @@ -1,523 +0,0 @@ -# Teaching Configuration Schema - -⏱️ **5 minutes** • 🟢 Beginner • ✓ Complete reference - -> **TL;DR** (30 seconds) -> -> - **What:** YAML schema for teaching project configuration -> - **Why:** Enables automated course workflows, week tracking, and deployment -> - **Where:** `.flow/teach-config.yml` in your teaching project root -> - **Next:** Copy the [full example](#complete-example) and customize for your course - -Teaching projects use `.flow/teach-config.yml` to configure course information, dates, breaks, instructor details, and deployment settings. This enables Craft's teaching commands to provide context-aware automation. - -!!! tip "Quick Win: Auto-Week Calculation" - Set `progress.current_week: auto` and Craft automatically calculates which week you're in based on your semester dates and break schedule - no manual updates needed! - -!!! note "Flow-CLI Compatibility" - If your config uses flow-cli's schema (`semester_info.start_date`, `course.name`, `branches.production`), Craft normalizes it automatically — no changes needed. See [Flow-CLI Config Compatibility](#flow-cli-config-compatibility) below. - -## File Location - -``` -your-teaching-project/ -├── .flow/ -│ └── teach-config.yml # Teaching configuration (this file) -├── syllabus/ -├── lectures/ -└── assignments/ -``` - -## Schema Reference - -### Course Information - -Basic course metadata displayed across teaching workflows. - -| Field | Type | Required | Description | Example | -|-------|------|----------|-------------|---------| -| `course.number` | string | ✓ | Course code or number | `"STAT 545"`, `"CS 101"` | -| `course.title` | string | ✓ | Full course title | `"Regression Analysis"` | -| `course.semester` | string | ✓ | Semester name | `"Spring"`, `"Fall"`, `"Winter"`, `"Summer"` | -| `course.year` | integer | ✓ | Four-digit year | `2026` | - -**Example:** - -```yaml -course: - number: "STAT 545" - title: "Regression Analysis" - semester: "Spring" - year: 2026 -``` - -### Semester Dates - -Define when your semester starts, ends, and breaks occur. - -| Field | Type | Required | Description | Format | -|-------|------|----------|-------------|--------| -| `dates.start` | date | ✓ | First day of semester | `YYYY-MM-DD` | -| `dates.end` | date | ✓ | Last day of semester | `YYYY-MM-DD` | -| `dates.breaks` | array | - | Break periods (optional) | See below | - -**Break Configuration:** - -Each break in `dates.breaks` includes: - -| Field | Type | Required | Description | Example | -|-------|------|----------|-------------|---------| -| `name` | string | ✓ | Break name | `"Spring Break"`, `"MLK Day"` | -| `start` | date | ✓ | First day of break | `2026-03-16` | -| `end` | date | ✓ | Last day of break (can equal start for single-day breaks) | `2026-03-20` | - -Single-day breaks (where `start == end`) are valid for holidays like MLK Day or Veterans Day. - -**Example:** - -```yaml -dates: - start: "2026-01-19" - end: "2026-05-08" - breaks: - - name: "MLK Day" - start: "2026-01-20" - end: "2026-01-20" # Single-day break (start == end) - - name: "Spring Break" - start: "2026-03-16" - end: "2026-03-20" - - name: "Reading Week" - start: "2026-04-13" - end: "2026-04-14" -``` - -### Instructor Information - -Optional instructor contact details for syllabus generation and student communication. - -| Field | Type | Required | Description | Example | -|-------|------|----------|-------------|---------| -| `instructor.name` | string | - | Full name | `"Dr. Jane Smith"` | -| `instructor.email` | string | - | Contact email | `"jsmith@university.edu"` | -| `instructor.office_hours` | string | - | Office hours description | `"Tu/Th 2-3pm, Zoom"` | - -**Example:** - -```yaml -instructor: - name: "Dr. Jane Smith" - email: "jsmith@university.edu" - office_hours: "Tuesday/Thursday 2-3pm, Zoom link on Canvas" -``` - -### Deployment Configuration - -Controls how your course site is built and published. - -| Field | Type | Required | Description | Default | -|-------|------|----------|-------------|---------| -| `deployment.production_branch` | string | - | Branch for published site | `"production"` | -| `deployment.draft_branch` | string | - | Branch for draft content | `"draft"` | -| `deployment.gh_pages_url` | string | - | GitHub Pages URL | - | - -**Example:** - -```yaml -deployment: - production_branch: "production" - draft_branch: "draft" - gh_pages_url: "https://username.github.io/stat-545" -``` - -**How it works:** - -- **Draft branch**: Work-in-progress content, visible to instructors only -- **Production branch**: Published content, visible to students -- **GitHub Pages**: Automatically deploys from production branch - -### Progress Tracking - -Track which week of the semester you're currently in. - -| Field | Type | Required | Description | Options | -|-------|------|----------|-------------|---------| -| `progress.current_week` | string/integer | - | Current week number | `"auto"` or `1-16` | - -**Automatic calculation (`auto`)**: - -- Calculates week based on today's date -- Accounts for semester start/end dates -- Skips weeks with breaks -- Recommended for most courses - -**Manual override (integer)**: - -- Set specific week number (1-16) -- Useful for: - - Testing future weeks - - Non-standard schedules - - Review weeks - -**Example:** - -```yaml -progress: - current_week: auto # Let Craft calculate based on dates - -# Or manual override: -progress: - current_week: 8 # Force week 8 (for testing/review) -``` - -**How automatic calculation works:** - -1. Count weeks from `dates.start` to today -2. Skip any weeks that fall within `dates.breaks` -3. Cap at semester length (weeks between `start` and `end`) - -### Validation Rules - -Optional validation settings for quality control. - -| Field | Type | Required | Description | Default | -|-------|------|----------|-------------|---------| -| `validation.required_sections` | array | - | Syllabus sections that must exist | `["grading", "policies", "objectives", "schedule"]` | -| `validation.strict_mode` | boolean | - | Errors block publishing | `true` | - -**Strict Mode Behavior:** - -| Mode | Missing Sections | Broken Links | Stale Content | Action | -|------|------------------|--------------|---------------|--------| -| `true` | ❌ Block | ❌ Block | ⚠️ Warn | Prevent publish | -| `false` | ⚠️ Warn | ⚠️ Warn | ⚠️ Warn | Allow publish | - -**Example:** - -```yaml -validation: - required_sections: - - grading - - policies - - objectives - - schedule - - textbook - - accommodations - strict_mode: true -``` - -## Complete Example - -Full configuration for a typical Spring semester course: - -```yaml -# .flow/teach-config.yml - STAT 545 Spring 2026 - -# ============================================================================ -# Course Information -# ============================================================================ -course: - number: "STAT 545" - title: "Regression Analysis" - semester: "Spring" - year: 2026 - -# ============================================================================ -# Semester Dates -# ============================================================================ -dates: - # Semester runs January 19 - May 8 - start: "2026-01-19" - end: "2026-05-08" - - # Break periods (Spring Break + Reading Week) - breaks: - - name: "Spring Break" - start: "2026-03-16" - end: "2026-03-20" - - - name: "Reading Week" - start: "2026-04-13" - end: "2026-04-14" - -# ============================================================================ -# Instructor Information (optional) -# ============================================================================ -instructor: - name: "Dr. Jane Smith" - email: "jsmith@university.edu" - office_hours: "Tuesday/Thursday 2-3pm, Zoom link on Canvas" - -# ============================================================================ -# Deployment Configuration (optional) -# ============================================================================ -deployment: - production_branch: "production" # Students see this - draft_branch: "draft" # Instructors see this - gh_pages_url: "https://jsmith.github.io/stat-545" - -# ============================================================================ -# Progress Tracking (optional) -# ============================================================================ -progress: - # Options: - # - "auto": Calculate based on dates and breaks (recommended) - # - 1-16: Manual override for specific week number - current_week: auto - -# ============================================================================ -# Validation Rules (optional) -# ============================================================================ -validation: - # Sections that must exist in syllabus - required_sections: - - grading - - policies - - objectives - - schedule - - # Strict mode: true = errors block publishing, false = warnings only - strict_mode: true -``` - -## Usage Examples - -### Example 1: Spring Semester with Spring Break - -Typical 16-week Spring semester: - -```yaml -course: - number: "CS 240" - title: "Data Structures" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-12" - end: "2026-05-01" - breaks: - - name: "Spring Break" - start: "2026-03-09" - end: "2026-03-13" - -progress: - current_week: auto -``` - -**Timeline:** - -- Weeks 1-8: Jan 12 - Mar 6 -- Spring Break: Mar 9-13 (skipped) -- Weeks 9-16: Mar 16 - May 1 - -### Example 2: Fall Semester with Thanksgiving - -Fall semester with Thanksgiving break: - -```yaml -course: - number: "MATH 301" - title: "Probability Theory" - semester: "Fall" - year: 2025 - -dates: - start: "2025-08-25" - end: "2025-12-12" - breaks: - - name: "Thanksgiving Break" - start: "2025-11-24" - end: "2025-11-28" - -progress: - current_week: auto -``` - -### Example 3: Summer Session (No Breaks) - -Compressed 8-week summer session: - -```yaml -course: - number: "STAT 101" - title: "Intro to Statistics" - semester: "Summer" - year: 2026 - -dates: - start: "2026-06-01" - end: "2026-07-24" - # No breaks in summer session - -progress: - current_week: auto -``` - -### Example 4: Multi-Break Semester - -Complex schedule with multiple breaks: - -```yaml -course: - number: "ECON 405" - title: "Econometrics" - semester: "Fall" - year: 2025 - -dates: - start: "2025-09-02" - end: "2025-12-15" - breaks: - - name: "Fall Break" - start: "2025-10-12" - end: "2025-10-13" - - - name: "Thanksgiving" - start: "2025-11-25" - end: "2025-11-29" - - - name: "Reading Days" - start: "2025-12-08" - end: "2025-12-09" - -progress: - current_week: auto -``` - -## How Commands Use This Config - -Teaching commands read `.flow/teach-config.yml` to provide context-aware automation: - -| Command | Uses | Example | -|---------|------|---------| -| `/craft:teach:week` | `progress.current_week`, `dates` | Show current week info | -| `/craft:teach:status` | All fields | Teaching dashboard | -| `/craft:teach:publish` | `deployment`, `validation` | Publish to production | -| `/craft:teach:syllabus` | `course`, `instructor`, `dates` | Generate syllabus | -| `/craft:teach:schedule` | `dates`, `breaks` | Create semester calendar | - -## Validation - -### Validate Your Configuration - -Use `/craft:teach:config` to validate your configuration: - -```bash -/craft:teach:config validate -``` - -**Checks:** - -- ✓ Required fields present -- ✓ Date formats correct (YYYY-MM-DD) -- ✓ Dates in logical order (start < end) -- ✓ Breaks fall within semester dates -- ✓ Semester/year combination valid -- ✓ Week calculation works correctly - -**Example output:** - -``` -✓ Configuration valid -✓ Semester: Spring 2026 (16 weeks) -✓ Current week: 8 (auto-calculated) -✓ Breaks: 2 configured - - Spring Break (5 days) - - Reading Week (2 days) -``` - -### Common Validation Errors - -| Error | Fix | -|-------|-----| -| Missing required field | Add `course.number`, `course.title`, etc. (or use flow-cli equivalents: `course.name`, `course.full_name`) | -| Invalid date format | Use `YYYY-MM-DD` format | -| Break outside semester | Adjust break dates to fall within `start`/`end` | -| Break start after end | Swap break `start` and `end` dates (single-day breaks where `start == end` are valid) | -| End before start | Swap semester `start` and `end` dates | -| Invalid semester | Use `"Spring"`, `"Fall"`, `"Winter"`, or `"Summer"` (lowercase auto-capitalized) | -| Invalid current_week | Use `"auto"` or integer 1-16 | - -## Tips and Best Practices - -!!! success "Recommended Settings" - - Use `progress.current_week: auto` for automatic tracking - - Set `validation.strict_mode: true` to catch errors early - - Include all breaks (even 1-2 day breaks) for accurate week calculation - - Use descriptive break names (`"Spring Break"` not `"Break 1"`) - -!!! warning "Common Pitfalls" - - Forgetting to update year when copying from previous semester - - Break dates that don't account for weekends - - Missing `deployment.gh_pages_url` prevents publish command from working - - Setting `current_week` manually and forgetting to update it - -!!! tip "Pro Tips" - - **Test future weeks**: Temporarily set `current_week: 15` to preview end-of-semester content - - **Multiple instructors**: Use `instructor.name: "Dr. Smith & Dr. Jones"` for co-taught courses - - **Non-standard schedules**: Manual `current_week` override handles exam weeks, review sessions - - **Version control**: Commit `.flow/teach-config.yml` - it's your course's source of truth - -## Flow-CLI Config Compatibility - -If your project uses [flow-cli](https://github.com/Data-Wise/flow-cli)'s `teach-config.yml` schema, Craft normalizes it automatically via `_normalize_config()` in `teach_config.py`. No migration needed — both schemas work transparently. - -### Field Mappings - -| Flow-CLI Schema | Craft-Native Schema | Behavior | -|-----------------|---------------------|----------| -| `course.name` | `course.number` | Mapped if `number` absent | -| `course.full_name` | `course.title` | Mapped if `title` absent | -| `course.semester: "spring"` | `course.semester: "Spring"` | Auto-capitalized | -| `semester_info.start_date` | `dates.start` | Mapped if `dates.start` absent | -| `semester_info.end_date` | `dates.end` | Mapped if `dates.end` absent | -| `semester_info.breaks` | `dates.breaks` | Mapped if `dates.breaks` absent | -| `branches.production` | `deployment.production_branch` | Mapped if `production_branch` absent | -| `branches.draft` | `deployment.draft_branch` | Mapped if `draft_branch` absent | - -### Design Principles - -- **Non-destructive**: Craft-native keys always win (never overwritten) -- **Silent**: No warnings or migration prompts -- **Idempotent**: Safe to normalize multiple times -- **Gap-filling**: Only adds missing fields, original keys preserved - -### Example: Flow-CLI Config That Works in Craft - -```yaml -# This flow-cli config works in Craft without changes -course: - name: "STAT 545" # -> course.number - full_name: "STAT 545 - ANOVA" # -> course.title - semester: "spring" # -> "Spring" (capitalized) - year: 2026 - -semester_info: - start_date: "2026-01-19" # -> dates.start - end_date: "2026-05-16" # -> dates.end - breaks: - - name: "MLK Day" - start: "2026-01-20" - end: "2026-01-20" # Single-day break OK - - name: "Spring Break" - start: "2026-03-15" - end: "2026-03-22" - -branches: - production: "production" # -> deployment.production_branch - draft: "draft" # -> deployment.draft_branch -``` - -## Schema Changelog - -| Version | Date | Changes | -|---------|------|---------| -| 1.1 | 2026-02-06 | Flow-CLI config normalization, single-day breaks support | -| 1.0 | 2026-01-16 | Initial schema for teaching workflow | - -## See Also - -- **`/folio:site:publish`** (moved to folio) - Publish teaching site with preview workflow -- **`utils/detect_teaching_mode.py`** - Teaching mode detection utility -- **`commands/utils/teaching_validation.py`** - Content validation utility -- **[Configuration Reference](reference/configuration.md)** - General Craft config diff --git a/docs/teaching-migration.md b/docs/teaching-migration.md deleted file mode 100644 index 54d73eede..000000000 --- a/docs/teaching-migration.md +++ /dev/null @@ -1,735 +0,0 @@ -# Migrating to Craft Teaching Workflows - -⏱️ **15 minutes** • 🟡 Intermediate • ✓ From manual to automated workflows - -> **TL;DR** (30 seconds) -> -> - **What:** Migrate from manual teaching site deployment to Craft automation -> - **Why:** Eliminate errors, save time, gain confidence in publishing -> - **How:** Add config file → Test workflows → Retire old scripts -> - **Benefit:** 5-step safety workflow replaces fragile shell scripts - -This guide helps you transition from manual teaching site workflows to Craft's automated teaching mode. - -> `site:publish`, `site:status`, and `site:progress` below moved to the `folio` plugin; `site:validate` remains in craft. - -## Before and After - -### Old Manual Workflow - -```bash -# 1. Remember which branch is production -git branch --show-current # Am I on draft or production? - -# 2. Manually switch branches -git checkout production - -# 3. Hope the merge works -git merge draft # Fingers crossed! - -# 4. Fix conflicts (no preview, just hope) -# ... manual conflict resolution ... - -# 5. Push and pray -git push origin production - -# 6. Wait 5 minutes and manually check site -# Is it live? Did it break? Who knows! - -# 7. Realize you forgot to validate syllabus -# Too late - it's already published! -``` - -**Problems with manual workflow:** - -- No pre-publish validation (catch errors AFTER students see them) -- No preview of changes (surprises after deployment) -- Manual branch management (easy to mess up) -- No rollback on failure (broken site = panic) -- No verification (did it actually deploy?) -- Cognitive load (remember all steps, every time) - -### New Craft Workflow - -```bash -# One command does everything -/folio:site:publish -``` - -**What happens automatically:** - -1. ✓ Validates content (syllabus, schedule, assignments) -2. ✓ Shows preview with categorized changes -3. ✓ Asks for confirmation -4. ✓ Creates backup branch -5. ✓ Safe merge with rollback on failure -6. ✓ Verifies deployment -7. ✓ Returns to original branch - -**Benefits:** - -- Errors caught BEFORE publishing -- Preview changes before students see them -- Automatic branch management -- Automatic rollback on failure -- Deployment verification -- Zero cognitive load (just run the command) - -## Migration Steps - -### Step 1: Add Configuration File (5 min) - -!!! note "Already have a flow-cli config?" - If your project already has `.flow/teach-config.yml` from flow-cli (with `semester_info`, `course.name`, `branches`), **skip this step** — Craft normalizes it automatically. No separate config needed. - -Create `.flow/teach-config.yml` with your course details: - -```yaml -# .flow/teach-config.yml - -course: - number: "STAT 440" - title: "Regression Analysis" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" - breaks: - - name: "Spring Break" - start: "2026-03-16" - end: "2026-03-20" - -deployment: - production_branch: "production" - draft_branch: "draft" - gh_pages_url: "https://yourname.github.io/stat-440" - -validation: - required_sections: - - grading - - policies - - objectives - - schedule - strict_mode: true - -progress: - current_week: auto -``` - -**Customize:** - -- Course number and title -- Semester dates (start, end, breaks) -- GitHub Pages URL -- Required syllabus sections - -**Test configuration:** - -```bash -/folio:site:status -``` - -**Expected output:** - -``` -✓ Teaching mode detected -✓ Configuration valid -``` - -### Step 2: Test Validation (3 min) - -Before using the publish workflow, test content validation: - -```bash -/craft:site:validate -``` - -**First run will likely show errors:** - -``` -🚫 ERRORS (must fix before publishing): - 1. Syllabus missing required sections: policies - 2. Schedule has incomplete weeks: Week 2, 4 -``` - -**Fix these errors:** - -- Add missing syllabus sections -- Complete schedule for all weeks -- Fix any broken assignment references - -**Validate again until clean:** - -``` -✅ ALL CHECKS PASSED -Status: Ready to publish ✅ -``` - -### Step 3: Test Progress Tracking (1 min) - -Verify semester progress calculation: - -```bash -/folio:site:progress -``` - -**Check output:** - -- Current week matches calendar -- Progress bar is accurate -- Break countdown is correct - -**If week is wrong:** - -- Verify `dates.start` in config -- Check break dates -- Use `--week` for manual override - -### Step 4: First Craft Publish (5 min) - -Run your first automated publish: - -```bash -# Make sure you're on draft branch -git checkout draft - -# Run publish workflow -/folio:site:publish -``` - -**What to expect:** - -1. Validation runs automatically -2. Preview shows categorized changes -3. Confirmation prompt (3 options) -4. Safe merge and push -5. Deployment verification - -**Review the preview carefully:** - -``` -CRITICAL CHANGES: -⚠️ syllabus/index.qmd +15 -3 - -CONTENT CHANGES: -✓ lectures/week-01.qmd +120 -0 -``` - -**Confirm publish:** - -- Choose "Yes - Merge and deploy (Recommended)" -- Monitor output for errors -- Verify live site after deployment - -### Step 5: Retire Old Scripts (1 min) - -Once Craft workflows are working, retire your old deployment scripts: - -```bash -# Archive old scripts -mkdir -p archive/ -mv deploy.sh archive/ -mv publish.sh archive/ -mv validate-syllabus.sh archive/ - -# Update team documentation -# Replace manual steps with /folio:site:publish -``` - -**Update `.gitignore` (optional):** - -``` -# Old deployment scripts (archived) -archive/ -``` - -## Common Migration Patterns - -### Pattern 1: Weekly Content Updates - -**Old workflow:** - -```bash -# Edit content (draft branch) -vim lectures/week-05.qmd - -# Commit changes -git add lectures/week-05.qmd -git commit -m "Add week 5 lecture" - -# Switch to production -git checkout production - -# Merge (hope it works) -git merge draft - -# Push (hope it deploys) -git push origin production - -# Wait and manually check -# Open browser, reload, verify -``` - -**New workflow:** - -```bash -# Edit content (draft branch) -vim lectures/week-05.qmd - -# Commit changes -git add lectures/week-05.qmd -git commit -m "Add week 5 lecture" - -# Publish with safety checks -/folio:site:publish -``` - -### Pattern 2: Assignment Releases - -**Old workflow:** - -```bash -# Add assignment file -vim assignments/hw-05.qmd - -# Update schedule manually -vim schedule.qmd -# Add due date, hope you didn't typo - -# Commit -git add assignments/hw-05.qmd schedule.qmd -git commit -m "Add HW 5" - -# Manual publish (cross fingers) -git checkout production -git merge draft -git push origin production - -# Realize later you forgot to link it -# Students can't find assignment -# Emergency fix required -``` - -**New workflow:** - -```bash -# Add assignment file -vim assignments/hw-05.qmd - -# Update schedule -vim schedule.qmd - -# Commit -git add assignments/hw-05.qmd schedule.qmd -git commit -m "Add HW 5" - -# Validate catches missing links -/craft:site:validate -# Shows: "Assignment HW 5 referenced but file missing link" - -# Fix link, then publish -/folio:site:publish -# Preview shows assignment in CRITICAL CHANGES -# Verify before students see it -``` - -### Pattern 3: Schedule Changes - -**Old workflow:** - -```bash -# Change due dates (high risk!) -vim schedule.qmd -# Move HW 3 from Week 5 to Week 6 - -# Commit and publish immediately -git add schedule.qmd -git commit -m "Move HW 3 due date" -git checkout production -git merge draft -git push origin production - -# Students confused (no notification) -# Canvas still shows old date -# Email apology required -``` - -**New workflow:** - -```bash -# Change due dates -vim schedule.qmd - -# Commit -git add schedule.qmd -git commit -m "Move HW 3 due date" - -# Preview shows CRITICAL CHANGE -/folio:site:publish -# Highlights: schedule.qmd in CRITICAL section -# Review diff before confirming - -# After publish, update Canvas manually -# Send announcement to students -``` - -### Pattern 4: Semester Progress Check - -**Old workflow:** - -```bash -# Mentally calculate: "What week is it?" -# Check calendar, count weeks manually -# Account for spring break... or did I? -# Confusion about whether we're on Week 7 or 8 - -# Update syllabus manually -vim syllabus.qmd -# Change "Current week: 7" to "Current week: 8" -# Commit, publish, hope it's right -``` - -**New workflow:** - -```bash -# One command shows everything -/folio:site:progress - -# Output: -# Week 8 of 16 (50% complete) -# Next break: Spring Break in 12 days - -# No manual updates needed -# Auto-calculated, always accurate -``` - -## What to Do with Old Scripts - -### Review and Archive - -Before deleting old scripts, review them for: - -1. **Environment-specific configuration** - - Server URLs - - API keys - - Paths - -2. **Custom validation logic** - - Unique checks for your course - - Can be added to Craft config - -3. **Deployment hooks** - - Pre/post-deploy actions - - May need custom integration - -**Archive, don't delete:** - -```bash -mkdir -p docs/archive/old-workflows/ -mv *.sh docs/archive/old-workflows/ -git add docs/archive/ -git commit -m "Archive old deployment scripts" -``` - -### Update Team Documentation - -Update your teaching team's workflow documentation: - -**Before:** - -```markdown -## Publishing to Production - -1. Switch to production branch: `git checkout production` -2. Merge from draft: `git merge draft` -3. Resolve conflicts if any -4. Push: `git push origin production` -5. Wait 5 minutes -6. Manually verify site is live -``` - -**After:** - -````markdown -## Publishing to Production - -Run the publish command: -```bash -/folio:site:publish -``` - -The command will: - -- Validate content automatically -- Show preview of changes -- Ask for confirmation -- Deploy with rollback on failure -- Verify site is live -```` - -### Notify Your Team - -**Email template:** - -``` - -Subject: New Teaching Site Workflow - Craft Automation - -We've migrated to Craft for automated teaching site deployment. - -What changed: - -- Old: Manual branch switching, merge, push (error-prone) -- New: One command with validation and preview - -New workflow: - -1. Edit content on draft branch -2. Run: /folio:site:publish -3. Review preview and confirm - -Benefits: - -- Catches errors BEFORE students see them -- Preview changes before publishing -- Automatic rollback on failure -- No more manual branch management - -Documentation: - - -Questions? Reply to this email. - -``` - -## Troubleshooting Migration Issues - -### Existing Content Fails Validation - -**Problem:** Old content doesn't meet Craft's validation rules - -**Solution:** - -```bash -# See what's wrong -/craft:site:validate - -# Fix issues one by one -# OR adjust validation rules - -# Edit config to relax rules temporarily -vim .flow/teach-config.yml -``` - -```yaml -validation: - required_sections: - - grading # Only require grading for now - strict_mode: false # Warnings only -``` - -**Gradually tighten rules:** - -- Start with relaxed validation -- Fix content over time -- Add sections to `required_sections` -- Enable `strict_mode` when ready - -### Branch Names Don't Match - -**Problem:** You use `main` and `dev` instead of `production` and `draft` - -**Solution:** Update config to match your branches - -```yaml -deployment: - production_branch: "main" # Your production branch - draft_branch: "dev" # Your draft branch -``` - -### Merge Conflicts During Migration - -**Problem:** Production and draft have diverged significantly - -**Solution:** - -```bash -# Option 1: Force sync (if production is outdated) -git checkout production -git reset --hard draft -git push --force origin production - -# Option 2: Merge manually first -git checkout production -git merge draft -# Resolve conflicts -git commit -# Then use Craft for future publishes -``` - -### GitHub Pages Not Deploying - -**Problem:** Site doesn't update after publish - -**Solution:** - -1. Check GitHub Actions: `gh run list --limit 5` -2. Verify branch in Settings → Pages -3. Check `gh_pages_url` in config matches actual URL -4. Wait up to 10 minutes for first deploy - -### Team Members Don't Have Craft - -**Problem:** Only you have Craft installed - -**Solution:** - -```bash -# Share installation instructions -# Homebrew (recommended) -brew tap data-wise/tap -brew install craft - -# Or curl -curl -fsSL https://raw.githubusercontent.com/Data-Wise/craft/main/install.sh | bash -``` - -**Document in team README:** - -````markdown -## Setup - -Install Craft: -```bash -brew install craft -``` - -Then use teaching workflows: - -```bash -/folio:site:publish -/folio:site:progress -/craft:site:validate -``` -```` - -## Migration Checklist - -Use this checklist to track migration progress: - -**Pre-migration:** - -- [ ] Review old deployment scripts -- [ ] Document current workflow -- [ ] Identify custom validation logic -- [ ] List all team members who deploy - -**Configuration:** - -- [ ] Create `.flow/teach-config.yml` -- [ ] Set course info (number, title, semester) -- [ ] Add semester dates (start, end, breaks) -- [ ] Configure deployment (branches, URL) -- [ ] Set validation rules -- [ ] Test config with `/folio:site:status` - -**Validation:** - -- [ ] Run `/craft:site:validate` -- [ ] Fix all validation errors -- [ ] Test with incomplete content (intentionally break validation) -- [ ] Verify error messages are clear - -**Publishing:** - -- [ ] Test `/folio:site:publish` on draft branch -- [ ] Review preview output carefully -- [ ] Confirm publish and monitor deployment -- [ ] Verify live site is correct -- [ ] Test rollback (create a failure scenario) - -**Progress Tracking:** - -- [ ] Run `/folio:site:progress` -- [ ] Verify current week calculation -- [ ] Check break countdown accuracy -- [ ] Test `--week` manual override - -**Team Migration:** - -- [ ] Install Craft for all team members -- [ ] Update team workflow documentation -- [ ] Send migration announcement email -- [ ] Hold training session (optional) -- [ ] Archive old deployment scripts - -**Post-migration:** - -- [ ] Monitor first 3 publishes for issues -- [ ] Collect team feedback -- [ ] Adjust validation rules if needed -- [ ] Update issue templates to reference new workflow - -## Success Metrics - -Track these to measure migration success: - -**Before Craft (baseline):** - -- Time to publish: ~15 minutes (manual steps) -- Publishing errors: ~20% of publishes have issues -- Broken link incidents: ~2 per semester -- Rollback required: ~10% of publishes - -**After Craft (target):** - -- Time to publish: ~3 minutes (one command) -- Publishing errors: <5% (caught by validation) -- Broken link incidents: 0 (validation catches them) -- Rollback required: 0 (automatic on failure) - -**Measure at 4 weeks, 8 weeks, and end of semester.** - -## Next Steps - -After successful migration: - -1. **Explore advanced features:** - - Custom validation rules - - JSON output for scripting - - CI/CD integration - -2. **Share experience:** - - Write blog post about migration - - Share config with other instructors - - Contribute validation patterns - -3. **Provide feedback:** - - What worked well? - - What was confusing? - - What features are missing? - -4. **Help others migrate:** - - Share this guide with colleagues - - Offer to help with their migration - - Build a community of practice - -## Resources - -- **[Teaching Mode Setup Tutorial](tutorials/teaching-mode-setup.md)** - First-time setup guide -- **[Config Schema](teaching-config-schema.md)** - Complete YAML reference -- **[Command Reference: `/folio:site:publish`](commands/site.md)** - Publishing workflow with validation -- **[Command Reference: `/folio:site:progress`](commands/site.md)** - Progress dashboard -- **[GitHub Issues](https://github.com/Data-Wise/craft/issues)** - Report problems or suggest features - -## Get Help - -**Questions during migration?** - -- Review troubleshooting section above -- Check [Teaching Config Schema](teaching-config-schema.md) -- Open an issue: [GitHub Issues](https://github.com/Data-Wise/craft/issues) - -**Feedback welcome!** Share your migration experience to help improve this guide. diff --git a/docs/teaching/index.md b/docs/teaching/index.md deleted file mode 100644 index 3a6d17c1e..000000000 --- a/docs/teaching/index.md +++ /dev/null @@ -1,139 +0,0 @@ -# Teaching Workflow - -> **TL;DR**: Three tools, one workflow. Flow-cli dispatches commands at shell speed, Scholar generates AI content, Craft manages your course site safely. - -## Teaching Ecosystem - -The teaching workflow spans three tools. Each has clear ownership: - -
- -- :zap:{ .lg .middle } **Flow-CLI** *(Shell Speed)* - - Config, deployment, semester tracking, shell aliases. - `teach status`, `teach deploy`, `teach config` - -- :brain:{ .lg .middle } **Scholar** *(AI Content)* - - Lectures, exams, quizzes, slides, assignments, rubrics. - `teach lecture`, `teach exam`, `teach quiz` - -- :shield:{ .lg .middle } **Craft** *(Site Safety)* - - Preview-before-publish, validation, progress tracking. - `/folio:site:publish`, `/folio:site:progress` - -
- -## Which tool do I use? - -| I want to... | Use | Command | -|---|---|---| -| Generate a lecture/exam/quiz | Scholar | `teach lecture`, `teach exam` | -| Deploy the course site | flow-cli | `teach deploy` | -| Check if content is ready | All three | `teach check` | -| See what week it is | flow-cli | `teach status` or `tst` | -| Publish with CI safety | Craft | `/folio:site:publish` | -| See all teaching commands | flow-cli | `teach map` | -| Track semester progress | Craft | `/folio:site:progress` | - -## Getting Started - -
- -- :rocket:{ .lg .middle } **First-Time Setup** - - --- - - 25-minute tutorial: create config, test detection, validate, publish. - - [:octicons-arrow-right-24: Setup Tutorial](../tutorials/teaching-mode-setup.md) - -- :gear:{ .lg .middle } **Config Reference** - - --- - - Complete YAML schema for `.flow/teach-config.yml`. Flow-cli configs work too. - - [:octicons-arrow-right-24: Config Schema](../teaching-config-schema.md) - -- :arrows_counterclockwise:{ .lg .middle } **Migration Guide** - - --- - - Moving from manual `git checkout && mkdocs build && git push` to one command. - - [:octicons-arrow-right-24: Migration Guide](../teaching-migration.md) - -- :card_file_box:{ .lg .middle } **Quick Reference** - - --- - - Printable cheat sheet: commands, flags, branch strategy, troubleshooting. - - [:octicons-arrow-right-24: Quick Reference](../reference/REFCARD-TEACHING.md) - -
- -## Site Commands (moved to folio) - -| Command | What it does | -|---------|-------------| -| `/folio:site:publish` | Preview → Validate → Switch branch → Build → Deploy (5-step safety) | -| `/folio:site:progress` | Semester dashboard with week tracking, break countdown, completion % | -| `/folio:site:build` | Build site with teaching-aware context (branch, week, course info) | -| `/folio:site:check` | Content validation (syllabus sections, schedule completeness, assignments) | -| ask "git status" (dev/git skill) | Teaching-aware git status (shows deployment context, what students see) | - -## Config Compatibility - -Flow-cli is the canonical config owner. Craft normalizes flow-cli's schema silently: - -| Flow-CLI writes | Craft reads as | -|---|---| -| `course.name` | `course.number` | -| `course.full_name` | `course.title` | -| `semester_info.start_date` | `dates.start` | -| `semester_info.end_date` | `dates.end` | -| `branches.production` | `deployment.production_branch` | -| `course.semester: "spring"` | `course.semester: "Spring"` | - -No migration needed. Both schemas work transparently. See [Config Schema](../teaching-config-schema.md#flow-cli-config-compatibility) for details. - -## Common Workflows - -### Weekly Content Update - -```bash -# 1. Edit content on draft branch -# 2. Preview changes -/folio:site:build -# 3. Validate and publish -/folio:site:publish -``` - -### Semester Check-In - -```bash -# See where you are -/folio:site:progress - -# Validate everything -/folio:site:publish --dry-run --validate-only -``` - -### Deploy Course Website - -See the full [Deploy Course Website recipe](../cookbook/common/deploy-course-website.md) for step-by-step instructions. - -## Documentation - -| Resource | Purpose | -|----------|---------| -| [Ecosystem Overview](../guide/teaching-workflow.md) | Full guide with workflows, validation, troubleshooting | -| [Setup Tutorial](../tutorials/teaching-mode-setup.md) | First-time 25-minute walkthrough | -| [Config Schema](../teaching-config-schema.md) | YAML reference (flow-cli compatible) | -| [Migration Guide](../teaching-migration.md) | From manual workflows to Craft | -| [Quick Reference](../reference/REFCARD-TEACHING.md) | Printable cheat sheet | -| [Teaching Spec](https://github.com/Data-Wise/craft/blob/main/docs/specs/_archive/SPEC-teaching-workflow-2026-01-16.md) | Original implementation spec | -| [Ecosystem Spec](https://github.com/Data-Wise/craft/blob/main/docs/specs/_archive/SPEC-teaching-ecosystem-coordination-2026-02-06.md) | Cross-tool coordination spec | diff --git a/docs/tutorials/TUTORIAL-codex-plugin.md b/docs/tutorials/TUTORIAL-codex-plugin.md index 5afcf4e88..43eb126d1 100644 --- a/docs/tutorials/TUTORIAL-codex-plugin.md +++ b/docs/tutorials/TUTORIAL-codex-plugin.md @@ -18,6 +18,32 @@ claude plugin install codex@openai-codex No further config needed — the plugin starts a broker process per working directory the first time it's used in that directory. +## When to Use This vs OpenCode + +See the routing table in +[`TUTORIAL-opencode-mcp-plugin.md`](TUTORIAL-opencode-mcp-plugin.md#when-to-use-which-routing-table) +and [`SPEC-codex-opencode-delegation-2026-08-08.md`](../specs/SPEC-codex-opencode-delegation-2026-08-08.md) +for the full rationale — short version: Codex for hard, well-scoped SWE tasks (deep bugs, +tricky refactors); OpenCode for one-shot questions or long background tasks. + +## Always Monitor a Delegation (mandatory, not optional) + +Never fire `codex exec`/`codex review` and silently wait. `--json` emits JSONL events — +filter for the terminal event + token usage only, not raw logs: + +```bash +codex exec review --json --uncommitted 2>&1 | while IFS= read -r line; do + case "$line" in + *'"type":"turn.completed"'*|*'"type":"turn.failed"'*|*'"type":"error"'*|*token*|*usage* ) + echo "[codex] $(echo "$line" | cut -c1-400)" ;; + esac +done +``` + +If the inline `usage` numbers read all-zeros, the real total is in a *child* rollout file +under `~/.codex/sessions/**/rollout-*.jsonl` — find the one whose `parent_thread_id` matches +the run's own `thread_id` and grep for `"type":"token_count"`. + ## Gotchas 1. **One broker per cwd, not global.** Each project directory that uses diff --git a/docs/tutorials/TUTORIAL-opencode-mcp-plugin.md b/docs/tutorials/TUTORIAL-opencode-mcp-plugin.md new file mode 100644 index 000000000..b359c2442 --- /dev/null +++ b/docs/tutorials/TUTORIAL-opencode-mcp-plugin.md @@ -0,0 +1,127 @@ +# OpenCode MCP Bridge Cheat Sheet + +Two separate MCP servers let Claude Code delegate work to OpenCode. Ask +Claude to "hand this to OpenCode" or "delegate this to opencode" once +either is installed. + +## The two bridges + +| Name | Package | Style | +|------|---------|-------| +| `opencode` | `opencode-mcp` (npm) | Synchronous-first — ~80 tools, tiered (`opencode_setup` → `opencode_ask`/`opencode_run` → fine-grained session/message control) | +| `opencode-async` | local build, `~/tools/better-opencode-mcp` | Fire-and-forget — 7 tools (`opencode`, `opencode_sessions`, `opencode_respond`, `opencode_cancel`, `opencode_health`, `ping`, `Help`), built for long-running background tasks | + +They are not duplicates: `opencode` covers everything from a one-shot +question to full session management; `opencode-async` is a thin +task-delegation layer (submit → poll `opencode_sessions` → `opencode_respond` +if it asks for input). + +## Setup + +```bash +# opencode — published npm package +claude mcp add opencode -s user -- npx -y opencode-mcp + +# opencode-async — local build +claude mcp add opencode-async -s user -- node ~/tools/better-opencode-mcp/dist/index.js +``` + +`-s user` registers the server globally (every project, every session). +Drop it (or use `-s project`) to scope a bridge to one repo instead — see +`claude mcp add --help` for the full scope table. + +## When to Use Which (routing table) + +See [`SPEC-codex-opencode-delegation-2026-08-08.md`](../specs/SPEC-codex-opencode-delegation-2026-08-08.md) +for the full rationale. Quick version: + +| Situation | Use | Why | +|---|---|---| +| Faster to just do it yourself | Don't delegate | Round-trip costs more than the task | +| Security / architecture / can't verify the result | Don't delegate | Not verifiable after the fact | +| Hard, well-scoped SWE task | [`codex`](TUTORIAL-codex-plugin.md) | Matches the existing `codex-rescue` trigger | +| One-shot question, need the answer this turn | `opencode` (`opencode_ask`/`opencode_run`) | Sync tier, blocks until done | +| Long task, don't want to block the session | `opencode-async` | Fire-and-forget, poll later | + +**Status vocabularies are different between the two bridges — don't conflate them:** +`opencode-async` reports `working` / `input_required` / `completed` / `failed` / `cancelled`; +`opencode`'s `opencode_check` reports `running` / `completed` / `error`. + +## Always Monitor a Delegation (mandatory, not optional) + +Never fire a delegated task and silently wait. Use a background `Monitor` watching for status +changes, token/cost usage, and errors only — three signals, not raw logs. Copy-paste starting +points: + +```bash +# opencode (sync tier) — wrap opencode_ask/opencode_run in a background call, +# then watch its stdout for the terminal "result" line + any error. +# (Called via the MCP tool directly in a live session — this is the CLI-probe +# equivalent for scripting/testing outside a session.) +``` + +```bash +# opencode-async — poll opencode_sessions for status until terminal: +# working -> completed | failed | cancelled (input_required needs opencode_respond) +``` + +```bash +# codex — codex exec (--json) already emits JSONL events; filter for the +# terminal event and any "token_count"/usage payloads, e.g.: +codex exec review --json --uncommitted 2>&1 | while IFS= read -r line; do + case "$line" in + *'"type":"turn.completed"'*|*'"type":"turn.failed"'*|*'"type":"error"'*|*token*|*usage* ) + echo "[codex] $(echo "$line" | cut -c1-400)" ;; + esac +done +``` + +Real token-usage numbers aren't always in the CLI's own event stream — `codex`'s per-turn +usage can read all-zeros while the real total lives in a *child* rollout session file under +`~/.codex/sessions/**/rollout-*.jsonl` (`"type":"token_count"`, look for +`parent_thread_id` matching the review's own `thread_id`). Check there if the inline events +look empty. + +## Gotchas + +1. **New tool registrations need a session restart.** `claude mcp add` + writes to `~/.claude.json` immediately, but a *running* Claude Code + session doesn't pick up the new MCP server until it restarts — the + tools won't appear via `ToolSearch` mid-session even though `claude mcp + list` already shows them connected. +2. **`opencode` auto-starts an OpenCode SDK server.** First call spawns + `OpenCode server ... on 127.0.0.1:4096` if one isn't already running — + expect a few seconds of latency on cold start. +3. **Always discover providers/models first.** `opencode`'s own + instructions warn against assuming a provider is available — call + `opencode_setup` then `opencode_provider_models` before passing + `providerID`/`modelID` to `opencode_ask`/`opencode_run`, or you may get + empty responses. +4. **`opencode-async` tasks don't block.** `opencode` (the tool, not the + server) returns a `taskId` immediately with status `"working"` — poll + `opencode_sessions` to see completion, and watch for status + `"input_required"`, which needs `opencode_respond` before the task can + finish. +5. **Scope check before assuming a bridge is available.** Both were + originally `local`-scoped to specific project directories (`~/`, + `savant-openai` for `opencode`; `cc-config`, `better-opencode-mcp` for + `opencode-async`) before being promoted to `user` scope — if either + stops showing up, check `claude mcp list` and the project's + `mcpServers` block in `~/.claude.json` before assuming it's broken. + +## Verifying it works + +Both servers were smoke-tested directly over stdio (JSON-RPC +`initialize` + `tools/list`, no live session needed): + +- `opencode` — responds with 80 tools + a tiered usage guide in the + initialize response's top-level `instructions` field (a sibling of + `serverInfo`, not nested inside it); auto-starts its SDK server on port + 4096. +- `opencode-async` — responds with 7 tools (`Help`, `ping`, + `opencode_health`, `opencode`, `opencode_sessions`, `opencode_respond`, + `opencode_cancel`). + +If you want to re-run the same check: spawn the server's command, write +newline-delimited JSON-RPC to stdin (not LSP-style `Content-Length` +framing — these two don't use it), and read the response from stdout. diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 90a332269..9e38f7fea 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -62,7 +62,6 @@ |----------|------|-------------------| | [Dependency Management](dependency-management-getting-started.md) | 10 min | Check, install, and manage tool dependencies | | [Formatting Migration](TUTORIAL-formatting-migration.md) | 15 min | Migrate scripts to the shared formatting library | -| [Teaching Mode Setup](teaching-mode-setup.md) | 15 min | Configure teaching-specific features and workflows | ## What's New diff --git a/docs/tutorials/teaching-mode-setup.md b/docs/tutorials/teaching-mode-setup.md deleted file mode 100644 index 6c08e77ab..000000000 --- a/docs/tutorials/teaching-mode-setup.md +++ /dev/null @@ -1,567 +0,0 @@ - -# Teaching Mode Setup - -⏱️ **25 minutes** • 🟢 Beginner • ✓ Complete first-time setup - -> **TL;DR** (30 seconds) -> -> - **What:** Set up Craft's teaching workflows for your course -> - **Why:** Automated validation, safe publishing, progress tracking -> - **How:** Create config file → Test detection → Validate → Publish -> - **Next:** Use `/folio:site:progress` weekly, `/folio:site:publish` for releases - -Teaching Mode transforms Craft into a comprehensive teaching workflow automation tool. This tutorial walks you through first-time setup for a Quarto-based course website. - -!!! note "Already using flow-cli?" - If your project already has a `.flow/teach-config.yml` from flow-cli (with `semester_info`, `course.name`, `branches`), Craft reads it directly — no separate config needed. See the [Config Schema Reference](../teaching-config-schema.md#flow-cli-config-compatibility) for details. - -!!! tip "Before You Start" - You need: - - - A Quarto project for your teaching content - - Git repository with draft/production branches (or willing to create them) - - GitHub Pages or similar deployment target - - Basic familiarity with YAML and Git - -## What You'll Learn - -By the end of this tutorial, you'll have: - -- ✓ Teaching mode enabled with auto-detection -- ✓ Content validation catching missing syllabus sections -- ✓ Preview-before-publish workflow preventing errors -- ✓ Automatic semester progress tracking -- ✓ Safe deployment to production - -## Step 1: Create Config File (5 min) - -Teaching mode activates when Craft detects `.flow/teach-config.yml` in your project root. - -### 1.1 Create Directory - -```bash -# In your teaching project root -mkdir -p .flow -``` - -### 1.2 Create Configuration - -Create `.flow/teach-config.yml` with your course details: - -```yaml -# .flow/teach-config.yml - STAT 440 Spring 2026 - -# ============================================================================ -# Course Information -# ============================================================================ -course: - number: "STAT 440" - title: "Regression Analysis" - semester: "Spring" - year: 2026 - -# ============================================================================ -# Semester Dates -# ============================================================================ -dates: - # Semester runs January 19 - May 8 - start: "2026-01-19" - end: "2026-05-08" - - # Break periods (holidays + Spring Break) - breaks: - - name: "MLK Day" - start: "2026-01-20" - end: "2026-01-20" # Single-day breaks are supported - - name: "Spring Break" - start: "2026-03-16" - end: "2026-03-20" - -# ============================================================================ -# Deployment Configuration -# ============================================================================ -deployment: - production_branch: "production" # Students see this - draft_branch: "draft" # Instructors see this - gh_pages_url: "https://yourname.github.io/stat-440" - -# ============================================================================ -# Progress Tracking -# ============================================================================ -progress: - # "auto" calculates based on dates and breaks (recommended) - current_week: auto - -# ============================================================================ -# Validation Rules -# ============================================================================ -validation: - # Sections that must exist in syllabus - required_sections: - - grading - - policies - - objectives - - schedule - - # Strict mode: true = errors block publishing - strict_mode: true -``` - -**Customize for your course:** - -- Replace `STAT 440` with your course number -- Update semester dates to match your academic calendar -- Add all break periods (even 1-2 day breaks) -- Set your GitHub Pages URL - -!!! warning "Common Mistake" - Don't forget to update the **year** field. Copy-paste errors from previous semesters are common! - -### 1.3 Validate Configuration - -Test that your config file is valid: - -```bash -# In Claude Code -/folio:site:status -``` - -**Expected output:** - -``` -✓ Teaching mode detected -✓ Configuration valid -✓ Semester: Spring 2026 (16 weeks) -✓ Current week: 1 (auto-calculated) -``` - -**If you see errors**, check: - -- Date format is `YYYY-MM-DD` -- Dates are in logical order (start < end) -- Breaks fall within semester dates -- All required fields present - -## Step 2: Test Detection (2 min) - -Verify that Craft recognizes your project as a teaching project. - -### 2.1 Check Status - -```bash -/folio:site:status -``` - -**What to look for:** - -``` -┌─────────────────────────────────────────────────────────┐ -│ 📚 TEACHING PROJECT STATUS │ -├─────────────────────────────────────────────────────────┤ -│ ✓ Mode: Teaching (detected via .flow/teach-config.yml) │ -│ ✓ Course: STAT 440 - Regression Analysis │ -│ ✓ Semester: Spring 2026 │ -│ ✓ Current Week: 1 of 16 │ -└─────────────────────────────────────────────────────────┘ -``` - -If teaching mode is **NOT** detected, check: - -1. File location: Must be `.flow/teach-config.yml` in project root -2. File permissions: Must be readable -3. YAML syntax: Use a validator if needed - -## Step 3: Validate Content (3 min) - -Run content validation to check for missing or incomplete sections. - -### 3.1 Run Validation - -```bash -/craft:site:validate -``` - -### 3.2 Understand Validation Output - -**Example output with errors:** - -``` -============================================================ -TEACHING CONTENT VALIDATION: ❌ BLOCKED -============================================================ - -🚫 ERRORS (must fix before publishing): - 1. Syllabus missing required sections: policies, objectives - 2. Schedule has incomplete weeks (no content): Week 2, 4 - -⚠️ WARNINGS (recommended to fix): - 1. Missing assignment files: HW 2 - -📋 DETAILED CHECKS: - [✓] Syllabus: grading - [✗] Syllabus: policies - [✗] Syllabus: objectives - [✓] Syllabus: schedule - [✓] Schedule: exists - [✗] Schedule: 2/4 weeks complete - [✗] Assignments: 2/3 found - -============================================================ -Summary: 3/7 checks passed -Status: 2 error(s) blocking publish ❌ -============================================================ -``` - -**Clean output (all checks pass):** - -``` -============================================================ -TEACHING CONTENT VALIDATION: ✅ PASSED -============================================================ - -✅ ALL CHECKS PASSED - -📋 DETAILED CHECKS: - [✓] Syllabus: grading - [✓] Syllabus: policies - [✓] Syllabus: objectives - [✓] Syllabus: schedule - [✓] Schedule: exists - [✓] Schedule: 4/4 weeks complete - [✓] Assignments: 3/3 found - -============================================================ -Summary: 7/7 checks passed -Status: Ready to publish ✅ -============================================================ -``` - -### 3.3 Fix Validation Errors - -**Missing syllabus sections:** - -- Add headings in your `syllabus/index.qmd` or `syllabus.qmd` -- Required sections (default): grading, policies, objectives, schedule -- Can customize in `validation.required_sections` - -**Incomplete schedule:** - -- Check your `schedule.qmd` for missing weeks -- Validation expects content for each week (lectures, readings, or assignments) - -**Missing assignment files:** - -- Validation checks that assignments referenced in schedule exist -- Add missing files or update references - -!!! tip "Pro Tip: Use Warnings" - Warnings don't block publishing - they're helpful reminders. Errors MUST be fixed (unless you use `--skip-validation`). - -## Step 4: First Publish (10 min) - -Publish your content from draft to production with preview and safety checks. - -### 4.1 Ensure You're on Draft Branch - -```bash -# Check current branch -git branch --show-current - -# Switch to draft if needed -git checkout draft -``` - -### 4.2 Run Publish Command - -```bash -/folio:site:publish -``` - -### 4.3 Review Preview - -The command shows a categorized diff: - -``` -┌─────────────────────────────────────────────┐ -│ 📋 PUBLISH PREVIEW │ -├─────────────────────────────────────────────┤ -│ CRITICAL CHANGES: │ -│ ⚠️ syllabus/index.qmd +15 -3 │ -│ ⚠️ schedule.qmd +42 -8 │ -│ │ -│ CONTENT CHANGES: │ -│ ✓ lectures/week-01.qmd +120 -0 │ -│ ✓ lectures/week-02.qmd +95 -0 │ -│ ✓ readings/chapter-1.qmd +58 -2 │ -│ │ -│ OTHER CHANGES: │ -│ _quarto.yml +2 -1 │ -│ .gitignore +3 -0 │ -│ │ -│ Summary: 8 files, +335 lines, -14 lines │ -└─────────────────────────────────────────────┘ -``` - -**File categories:** - -- **CRITICAL**: syllabus*, schedule*, assignments/ -- **CONTENT**: lectures/, readings/, resources/ -- **OTHER**: All other files - -### 4.4 Confirm Publish - -You'll be prompted with 3 options: - -1. **"Yes - Merge and deploy (Recommended)"** - Proceed -2. **"Preview full diff first"** - See detailed `git diff` -3. **"Cancel"** - Abort - -Choose option 1 to proceed. - -### 4.5 Monitor Deployment - -The command will: - -1. Create backup branch (`production-backup-`) -2. Checkout production branch -3. Fast-forward merge from draft -4. Push to remote -5. Verify deployment (if `gh_pages_url` configured) - -**Success output:** - -``` -✅ PUBLISH SUCCESSFUL - -🌐 Live Site: https://yourname.github.io/stat-440/ - -📊 Changes Published: - - 8 files modified - - +335 lines added - - -14 lines removed - -💡 Next Steps: - • Review the live site: https://yourname.github.io/stat-440/ - • Clean up worktree by asking "clean up merged branches" (dev/git skill) - • Continue editing on draft branch - -⏱ Deployment may take 1-2 minutes to fully propagate. -``` - -### 4.6 Verify Live Site - -Wait 1-2 minutes, then visit your GitHub Pages URL: - -``` -https://yourname.github.io/stat-440/ -``` - -**Check:** - -- Syllabus displays correctly -- Schedule shows all weeks -- Assignments are linked properly - -!!! success "Congratulations!" - You've successfully published your first teaching site with Craft's safety workflow! - -## Step 5: Track Progress (2 min) - -Use the progress dashboard to monitor semester status. - -### 5.1 Run Progress Command - -```bash -/folio:site:progress -``` - -**Example output:** - -``` -┌─────────────────────────────────────────────────────────┐ -│ 📚 STAT 440: Regression Analysis │ -│ Spring 2026 · Week 1 of 16 (6% complete) │ -├─────────────────────────────────────────────────────────┤ -│ │ -│ 📅 CURRENT WEEK: Week 1 │ -│ Date Range: Jan 19-25 │ -│ │ -│ 📊 PROGRESS: │ -│ ▓░░░░░░░░░░░░░░░ 6% complete │ -│ │ -│ 📌 UPCOMING MILESTONES: │ -│ • Spring Break: Mar 16-20 (57 days) │ -│ │ -│ ⏰ NEXT BREAK: Spring Break in 57 days │ -│ │ -└─────────────────────────────────────────────────────────┘ -``` - -**Key information:** - -- Current week (auto-calculated from dates and breaks) -- Visual progress bar -- Upcoming break countdown -- Date range for current week - -### 5.2 Weekly Check-In - -Run `/folio:site:progress` at the start of each week to: - -- Confirm current week is correct -- Plan upcoming content -- Track progress toward semester goals - -!!! tip "Manual Override" - Need to test future weeks? Use `--week` flag: - ```bash - /folio:site:progress --week 8 - ``` - -## Common Workflows - -### Weekly Content Updates - -```bash -# 1. Edit content on draft branch -# (edit lectures, assignments, etc.) - -# 2. Validate before publishing -/craft:site:validate - -# 3. Publish to production -/folio:site:publish -``` - -### Assignment Releases - -```bash -# 1. Add assignment files -# assignments/hw-02.qmd - -# 2. Update schedule -# schedule.qmd (add HW 2 due date) - -# 3. Validate -/craft:site:validate - -# 4. Publish -/folio:site:publish -``` - -### Schedule Changes - -```bash -# 1. Edit schedule.qmd -# (move due dates, adjust topics) - -# 2. Validate completeness -/craft:site:validate - -# 3. Preview changes -/folio:site:publish -# (review CRITICAL CHANGES section carefully) - -# 4. Confirm and publish -``` - -## Troubleshooting - -### Teaching Mode Not Detected - -**Symptom:** `/folio:site:status` doesn't show teaching mode - -**Solution:** - -1. Check file location: `.flow/teach-config.yml` in project root -2. Verify YAML syntax with validator -3. Ensure file is readable (`ls -la .flow/`) - -### Validation Errors Block Publishing - -**Symptom:** Errors prevent `/folio:site:publish` from proceeding - -**Solution:** - -1. Read error messages carefully -2. Fix missing sections or incomplete content -3. Run `/craft:site:validate` to verify fixes -4. **Emergency bypass** (not recommended): `--skip-validation` - -### Merge Conflicts During Publish - -**Symptom:** "Cannot fast-forward" error - -**Solution:** - -```bash -# Production branch has diverged from draft -git checkout production -git merge draft -# Resolve conflicts -git commit -/folio:site:publish # Try again -``` - -### Progress Inaccurate - -**Symptom:** Current week calculation is wrong - -**Solution:** - -1. Verify `dates.start` and `dates.end` in config -2. Check break dates are correct -3. Ensure breaks don't overlap -4. Use `--week` flag for manual override - -### Deployment Not Live After 5 Minutes - -**Symptom:** GitHub Pages not updating - -**Solution:** - -1. Check GitHub Actions: `gh run list --limit 5` -2. Verify branch in Settings → Pages (should be "production") -3. Check build logs for errors -4. Wait up to 10 minutes for first deployment - -## Next Steps - -Now that teaching mode is set up: - -**Weekly tasks:** - -- [ ] Run `/folio:site:progress` each Monday -- [ ] Update content on `draft` branch -- [ ] Validate with `/craft:site:validate` -- [ ] Publish with `/folio:site:publish` - -**Semester tasks:** - -- [ ] Add assignment files before due dates -- [ ] Update syllabus when policies change -- [ ] Keep schedule current (adjust for snow days, etc.) - -**Advanced features:** - -- [ ] Explore `--json` output for scripting -- [ ] Set up CI/CD to auto-validate on push -- [ ] Create custom validation rules -- [ ] Integrate with LMS (future enhancement) - -## Additional Resources - -- **[Config Schema Reference](../teaching-config-schema.md)** - Complete YAML specification (includes flow-cli compatibility) -- **[Teaching Workflow Guide](../guide/teaching-workflow.md)** - Ecosystem overview (Craft vs Scholar vs flow-cli) -- **[Migration Guide](../teaching-migration.md)** - From manual to Craft workflows -- **[Site Commands Reference](../commands/site.md)** - All site commands including publish, progress, and validate - -## Help and Support - -**Questions or issues?** - -- Check troubleshooting section above -- Review [Teaching Config Schema](../teaching-config-schema.md) -- Open an issue: [GitHub Issues](https://github.com/Data-Wise/craft/issues) - -**Feedback welcome!** Teaching mode is actively developed. Share your experiences and suggestions. diff --git a/examples/test_parser.py b/examples/test_parser.py deleted file mode 100644 index 83d6f13e2..000000000 --- a/examples/test_parser.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -""" -Example script demonstrating teach_config.py usage. - -Usage: - python3 examples/test_parser.py [path/to/config/directory] - -This will load and validate the teaching config from the specified directory -(or current directory if not specified). -""" - -import sys -import json -from pathlib import Path - -# Add parent directory to path -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from commands.utils.teach_config import load_teach_config - - -def main(): - # Get directory from command line or use current directory - cwd = sys.argv[1] if len(sys.argv) > 1 else "." - - print(f"Loading teaching config from: {Path(cwd).resolve()}") - print("-" * 60) - - try: - config = load_teach_config(cwd) - - if config is None: - print("No teaching config found.") - print("\nSearched for:") - print(" - .flow/teach-config.yml") - print(" - teach-config.yml") - sys.exit(1) - - print("Configuration loaded successfully!\n") - - # Display course info - course = config["course"] - print(f"Course: {course['number']} - {course['title']}") - print(f"Semester: {course['semester']} {course['year']}") - - # Display dates - dates = config["dates"] - print(f"\nDates:") - print(f" Start: {dates['start']}") - print(f" End: {dates['end']}") - - # Display breaks if any - if dates.get("breaks"): - print(f"\nBreaks:") - for i, brk in enumerate(dates["breaks"], 1): - print(f" {i}. {brk['name']}: {brk['start']} to {brk['end']}") - - # Display instructor if present - if "instructor" in config: - instructor = config["instructor"] - print(f"\nInstructor:") - if "name" in instructor: - print(f" Name: {instructor['name']}") - if "email" in instructor: - print(f" Email: {instructor['email']}") - if "office_hours" in instructor: - print(f" Office Hours: {instructor['office_hours']}") - - # Display deployment settings - deployment = config["deployment"] - print(f"\nDeployment:") - print(f" Production Branch: {deployment['production_branch']}") - print(f" Draft Branch: {deployment['draft_branch']}") - if "gh_pages_url" in deployment: - print(f" GitHub Pages: {deployment['gh_pages_url']}") - - # Display progress - progress = config["progress"] - print(f"\nProgress:") - print(f" Current Week: {progress['current_week']}") - - # Display validation settings - validation = config["validation"] - print(f"\nValidation:") - print(f" Strict Mode: {validation['strict_mode']}") - print(f" Required Sections: {', '.join(validation['required_sections'])}") - - # Option to dump full config as JSON - if "--json" in sys.argv: - print("\n" + "=" * 60) - print("Full configuration (JSON):") - print("=" * 60) - print(json.dumps(config, indent=2, default=str)) - - except ValueError as e: - print(f"Validation Error:\n{e}") - sys.exit(1) - except Exception as e: - print(f"Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/mcp/package-lock.json b/mcp/package-lock.json index 4c9c3f22c..94f2a123e 100644 --- a/mcp/package-lock.json +++ b/mcp/package-lock.json @@ -11,15 +11,15 @@ "@modelcontextprotocol/sdk": "^1.30.0" }, "devDependencies": { - "@types/node": "^26.1.2", - "esbuild": "^0.28.1", + "@types/node": "^26.2.0", + "esbuild": "^0.28.2", "typescript": "^7.0.2" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -34,9 +34,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -51,9 +51,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -68,9 +68,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -85,9 +85,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -102,9 +102,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -119,9 +119,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -136,9 +136,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -153,9 +153,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -170,9 +170,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -187,9 +187,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -204,9 +204,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -221,9 +221,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -238,9 +238,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -255,9 +255,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -272,9 +272,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -289,9 +289,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -306,9 +306,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -323,9 +323,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -340,9 +340,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -357,9 +357,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -374,9 +374,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -391,9 +391,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -408,9 +408,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -425,9 +425,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -442,9 +442,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -511,9 +511,9 @@ } }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -1138,9 +1138,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1151,32 +1151,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escape-html": { diff --git a/mcp/package.json b/mcp/package.json index 8ccdcdbb9..bae41123c 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -14,8 +14,8 @@ "@modelcontextprotocol/sdk": "^1.30.0" }, "devDependencies": { - "@types/node": "^26.1.2", - "esbuild": "^0.28.1", + "@types/node": "^26.2.0", + "esbuild": "^0.28.2", "typescript": "^7.0.2" } } diff --git a/mkdocs.yml b/mkdocs.yml index c144b49aa..5e198bf0d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Craft Plugin -site_description: "Full-stack developer toolkit - 48 commands, 2 agents, 41 skills for code, git, site, docs, testing, architecture, CI, distribution, teaching workflows, and planning with ADHD-friendly interfaces. v4.5.0 adds /craft:finish (renamed from /craft:done, ADR-006). See NEWS.md for release-by-release highlights." +site_description: "Full-stack developer toolkit - 48 commands, 2 agents, 41 skills for code, git, site, docs, testing, architecture, CI, distribution, and planning with ADHD-friendly interfaces. v4.6.0 adds /craft:finish (renamed from /craft:done, ADR-006). See NEWS.md for release-by-release highlights." site_url: https://data-wise.github.io/craft/ repo_name: Data-Wise/craft @@ -177,6 +177,7 @@ nav: - "ADR-004: Release Surface Registry": adr/ADR-004-release-surface-registry.md - "ADR-005: Shim Canonical-Path Resolution": adr/ADR-005-shim-canonical-path-resolution.md - "ADR-006: Done Renamed to Finish": adr/ADR-006-done-renamed-to-finish.md + - "ADR-007: Pattern-Scoped Prose Staleness Gating": adr/ADR-007-pattern-scoped-prose-staleness-gating.md - Brainstorm Power User Guide: tutorials/TUTORIAL-brainstorm-power-user.md - CRAFT-001 Emoji Spacing Tutorial: tutorials/TUTORIAL-craft-001-emoji-spacing.md - Your First 10 Minutes: tutorials/TUTORIAL-first-10-minutes.md @@ -223,6 +224,7 @@ nav: - Claude HUD Plugin: tutorials/TUTORIAL-claude-hud-plugin.md - Dropbox Plugin: tutorials/TUTORIAL-dropbox-plugin.md - Agent Skills Plugin: tutorials/TUTORIAL-agent-skills-plugin.md + - OpenCode MCP Bridge: tutorials/TUTORIAL-opencode-mcp-plugin.md - Command Tutorials: - arch:analyze Tutorial: tutorials/TUTORIAL-arch-analyze.md - arch:plan Tutorial: tutorials/TUTORIAL-arch-plan.md @@ -248,18 +250,6 @@ nav: - site:deploy Tutorial: tutorials/TUTORIAL-site-deploy.md - smart-help Tutorial: tutorials/TUTORIAL-smart-help.md - # 3. TEACHING - Dedicated teaching workflow section - - 📚 Teaching: - - Teaching Home: teaching/index.md - - Getting Started: - - Setup Tutorial: tutorials/teaching-mode-setup.md - - Config Schema: teaching-config-schema.md - - Migration Guide: teaching-migration.md - - Ecosystem & Guides: - - Ecosystem Overview: guide/teaching-workflow.md - - Documentation Index: TEACHING-DOCS-INDEX.md - - Quick Reference: reference/REFCARD-TEACHING.md - # 4. COMMANDS & REFERENCE - Command documentation - ⚙️ Commands & Reference: - Command Overview: commands/overview.md @@ -335,6 +325,7 @@ nav: - Run a Coded Workflow: cookbook/recipes/run-a-coded-workflow.md - Fan a Workflow Across Files: cookbook/recipes/fan-a-workflow-across-files.md - Triage a Red PR: cookbook/recipes/triage-a-red-pr.md + - Triage Your Repo: cookbook/recipes/triage-your-repo.md - Set Up New Project: cookbook/common/setup-new-project.md - Deploy Course Website: cookbook/common/deploy-course-website.md - Automate Release Workflow: cookbook/common/automate-release-workflow.md diff --git a/package.json b/package.json index b70fbabc1..afd0cdfd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@data-wise/claude-craft-plugin", - "version": "4.5.0", + "version": "4.6.0", "description": "Full-stack developer toolkit - 48 commands, 2 agents, 41 skills for code, git, site, docs, testing, architecture, CI, distribution, teaching workflows, and planning with ADHD-friendly interfaces", "main": "index.js", "scripts": { diff --git a/pyproject.toml b/pyproject.toml index a74596a86..0bf44ca12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ markers = [ "skills: Skill file validation and behavior", "agents: Agent configuration and hooks", "docs: Documentation link checking, content validation", - "teaching: Teaching workflow and config validation", "claude_md: CLAUDE.md sync, audit, fix, edit operations", "branch_guard: Branch protection hook behavior", "orchestrator: Orchestrator v2.1 features and workflows", diff --git a/scripts/config/exclusions.txt b/scripts/config/exclusions.txt index b75932712..33bc32d94 100644 --- a/scripts/config/exclusions.txt +++ b/scripts/config/exclusions.txt @@ -42,7 +42,6 @@ docs/architecture/HUB-V2-ARCHITECTURE.md:97 commands docs/tutorials/TUTORIAL-post-merge-pipeline.md:97 commands docs/demos/hub-v2.tape:97 commands docs/api/DISCOVERY-API.md:97 commands -docs/API-REFERENCE-COMMANDS.md:97 commands docs/reference/ERROR-SCENARIOS.md:97 commands docs/dev/CONTRIBUTING-HUB-V2.md:97 commands docs/REFCARD.md:99 commands @@ -73,7 +72,6 @@ docs/guide/orch-flag-usage.md:4 agents docs/orchestrator.md:4 agents docs/orchestrator.md:6 agents docs/commands.md:4 agents -docs/commands/orchestrate.md:4 agents docs/commands/hub.md:4 agents docs/commands/smart.md:4 agents docs/workflows/index.md:4 agents @@ -89,9 +87,6 @@ docs/tutorials/TUTORIAL-claude-code-2.1-enhancements.md:4 agents # Tutorial/example counts (historical snapshots, not current totals) # --------------------------------------------------------------------------- # "Stale CLAUDE.md" troubleshooting example (intentionally stale) -docs/cookbook/troubleshooting/claude-md-out-of-sync.md:94 commands -docs/cookbook/troubleshooting/claude-md-out-of-sync.md:18 skills -docs/cookbook/troubleshooting/claude-md-out-of-sync.md:6 agents # Tutorial showing a hypothetical plugin (different plugin, not craft) docs/tutorials/claude-md-workflows.md:51 commands @@ -107,7 +102,6 @@ docs/tutorials/interactive-docs-update-tutorial.md:99 commands # Docs-update interactive example (shows option preview 99→101) docs/examples/docs-update-interactive-example.md:101 commands -docs/examples/docs-update-interactive-example.md:99 commands # REFCARD-DOCS-UPDATE (describes a check type, not actual count) docs/reference/REFCARD-DOCS-UPDATE.md:60 commands diff --git a/scripts/docs-staleness-check.sh b/scripts/docs-staleness-check.sh index a6ac9b152..49fe6f7c8 100755 --- a/scripts/docs-staleness-check.sh +++ b/scripts/docs-staleness-check.sh @@ -147,13 +147,257 @@ EXPECTED_SKILLS="" EXPECTED_AGENTS="" CURRENT_VERSION="" +# The CRAFT_EXPECTED_* overrides exist so the prose-staleness fixtures can declare +# their expected counts directly instead of materializing 48 command files and 41 +# skill dirs per fixture case. Unset in every production path, so the derived +# values are what actually runs. load_counts() { - EXPECTED_CMDS=$(find commands -name "*.md" ! -name "index.md" ! -name "README.md" 2>/dev/null | wc -l | tr -d ' ') - EXPECTED_SKILLS=$(find skills -name "SKILL.md" 2>/dev/null | wc -l | tr -d ' ') - EXPECTED_AGENTS=$(find agents -name "*.md" 2>/dev/null | wc -l | tr -d ' ') + EXPECTED_CMDS="${CRAFT_EXPECTED_CMDS:-$(find commands -name "*.md" ! -name "index.md" ! -name "README.md" 2>/dev/null | wc -l | tr -d ' ')}" + EXPECTED_SKILLS="${CRAFT_EXPECTED_SKILLS:-$(find skills -name "SKILL.md" 2>/dev/null | wc -l | tr -d ' ')}" + EXPECTED_AGENTS="${CRAFT_EXPECTED_AGENTS:-$(find agents -name "*.md" 2>/dev/null | wc -l | tr -d ' ')}" CURRENT_VERSION=$(python3 -c "import json; print(json.load(open('.claude-plugin/plugin.json'))['version'])") } +# --------------------------------------------------------------------------- +# Prose staleness helpers (SPEC-doc-staleness-prose-gaps-2026-08-07) +# --------------------------------------------------------------------------- +# The boundary after a matched noun ("command", "agents", ...): whitespace, +# closing punctuation, or end of line. NOT `[^a-z]`, which also accepts `-` — +# that let "command-line" and "agent-facing" read as counts (F1), and let +# `[f]ix` then rewrite "30 command-line entry points" into +# "48 command-line entry points" (F2). One constant, shared by the awk +# pre-filter and the bash matcher below (and the strip in phase7 that trims it +# back off a matched span) — tuning them independently caused a 4x perf +# regression once already (see emit_shaped_lines' header note). +PROSE_COUNT_TRAILER='([])} .,;:!?*]|$)' + +# emit_shaped_lines prints "file:lineno:shape:content" for lines sitting in one +# of four structured shapes. Free prose is deliberately NOT emitted: an +# adversarial review of a blanket "N agents" search found 90+ false positives +# across docs/ — orchestration mode-limit prose ("2 agents max"), a tutorial +# using an intentionally fictional plugin, and a troubleshooting page that +# prints a wrong count on purpose to teach the bug. Scoping to line shapes is +# what makes checking the singular noun form safe here. +# +# Takes the whole file list at once and makes ONE awk pass over it. The first +# build called awk per file: ~640 markdown files x 2 scans is ~1300 process +# spawns, which took this script from 8s to 36s and blew the 30s timeout in +# test_pre_release_check_runs. FILENAME/FNR give the same per-file output from a +# single invocation. +emit_shaped_lines() { + [[ $# -eq 0 ]] && return 0 + awk -v trailer="$PROSE_COUNT_TRAILER" ' + # Only lines that actually carry a count are worth emitting. Without this + # filter every line of every box block is emitted (~3500 across docs/), + # and the bash loop downstream spends ~6 grep subprocesses on each — the + # other half of the 4x slowdown, alongside per-file awk spawns. + function hascount(s) { + return s ~ ("[0-9]+ (specialized )?(commands?|skills?|agents?)" trailer) + } + + FNR == 1 { inbox = 0 } + + # "version-box": a quick-reference/version box drawn with box characters. + # An unclosed box (no matching bottom border, e.g. truncated by a + # missing fence) used to leak `inbox` mode into the rest of the file, + # so a later structure-table row got measured as a version-box line + # instead and lost its own type restriction (F8). Closed on the FIRST + # line carrying no box-drawing character at all, not only on an + # explicit `└` -- that line falls through to the shape checks below + # instead of being consumed as part of the box. + /┌/ { inbox = 1 } + inbox { + if ($0 !~ /[┌┐└┘│├┤─]/) { + inbox = 0 + } else { + if (hascount($0)) print FILENAME ":" FNR ":version-box:" $0 + if ($0 ~ /└/) inbox = 0 + next + } + } + + # "tldr": a line that OPENS with TL;DR (after optional blockquote or + # emphasis markers), not merely one that mentions it. A doc describing a + # TL;DR bug is not itself making a TL;DR claim: the context table in + # ADR-007 quotes the "8 specialized agents" line the ADR exists to + # explain, and a contains-match flagged it the moment it was written. + # (No apostrophes in this awk program -- it is single-quoted in bash.) + /^[[:space:]]*[>*_[:space:]]*TL;DR/ { + if (hascount($0)) print FILENAME ":" FNR ":tldr:" $0 + next + } + + # "count-summary": the bolded badge line, e.g. **48 commands** | **41 skills**. + /\*\*[0-9]+ (commands|skills|agents)\*\*/ { print FILENAME ":" FNR ":count-summary:" $0; next } + + # "structure-table": a table row whose first cell names a counted + # directory. Added 2026-08-15 after CLAUDE.md claimed "8 agent + # definitions" (singular) for five minors while the plural-only scan + # below read GREEN over it. + /^\|[[:space:]]*`(commands|skills|agents)\/`[[:space:]]*\|/ { + if (hascount($0)) print FILENAME ":" FNR ":structure-table:" $0 + next + } + ' "$@" 2>/dev/null || true +} + +# emit_release_date_claims prints "file:lineno:YYYY-MM-DD" for release-date +# claims within 4 lines of a mention of the current version token — e.g. +# NEWS.md's "## v4.5.0 ..." heading followed by "**Released:** 2026-08-08". +# Windowed so historical entries further down the same file are not compared +# against the current version's date. +# +# The window opens ONLY when the version token sits in a markdown heading or a +# version-box content line (D6/F3). Proximity alone is not enough: an upgrade +# guide saying "Upgrading to v4.5.0 is a drop-in change" is prose, not a claim +# site, and used to open the window anyway, collecting whatever date happened +# to sit within 4 lines of it as though it were this version's release date. +# +# Single awk pass over the whole file list, for the same reason as above. +emit_release_date_claims() { + [[ $# -eq 0 ]] && return 0 + awk -v ver="v${CURRENT_VERSION}" ' + FNR == 1 { win = 0 } + function is_heading(s) { return s ~ /^#+[[:space:]]/ } + function is_box_line(s) { return s ~ /^[[:space:]]*│/ } + # 5, not 4: the win-- below runs on the version line itself, so win=4 + # scanned that line plus only 3 more -- one short of the 4 following + # lines this check documents, silently missing a layout as ordinary as + # heading / blank / Type / blank / Released. + (is_heading($0) || is_box_line($0)) && index($0, ver) { win = 5 } + win > 0 { + if (match($0, /[Rr]eleased[^0-9]{0,12}[0-9]{4}-[0-9]{2}-[0-9]{2}/)) { + s = substr($0, RSTART, RLENGTH) + if (match(s, /[0-9]{4}-[0-9]{2}-[0-9]{2}/)) + print FILENAME ":" FNR ":" substr(s, RSTART, RLENGTH) + } + win-- + } + ' "$@" 2>/dev/null || true +} + +# D1: check 1 has no external authority. The git tag doesn't exist at either +# point this check actually runs -- the release pipeline writes NEWS/REFCARD +# dates in Step 3b, *before* Step 8 creates the tag, and CI's checkout has no +# fetch-tags -- so a tag-based check only ever fired on a developer machine +# that had already pulled it, which is not a gate (F7). Instead, every +# release-date claim for the current version is compared against every other +# one; the one-day tolerance survives as the agreement window between claims, +# absorbing the UTC-boundary case that motivated it (v4.5.0: NEWS.md says +# 2026-08-08, REFCARD.md says 2026-08-07 -- both are correct). +# +# Given N date strings, returns the one with the most occurrences. Ties break +# to the LATER date, not the earlier one: a stale-release-date bug is a +# forgotten update, so the wrong claim is normally older than the correct one, +# never newer. (With exactly two claims -- the common case, one NEWS.md entry +# and one REFCARD.md box -- every tie is 1-1, so this tie-break is what +# decides which claim is "authority" and which is "stale".) Plain nested +# loop, not an associative array -- this script supports bash 3.2. +majority_date() { + local best="" best_count=0 d1 d2 count + for d1 in "$@"; do + count=0 + for d2 in "$@"; do + [[ "$d2" == "$d1" ]] && count=$((count + 1)) + done + if (( count > best_count )) || { (( count == best_count )) && [[ -n "$best" ]] && [[ "$d1" > "$best" ]]; }; then + best="$d1" + best_count="$count" + fi + done + echo "$best" +} + +# Applies one `s/PATTERN/REPLACEMENT/flags` substitution to one line of one file. +# Echoes "true" only when the file actually changed, "false" otherwise — +# including when fix_detail is not a substitution at all (Phase 8's doc-coverage +# findings carry a `doc-coverage:surface:cmd` marker instead). +# +# Uses python3 rather than `sed -i`: the patterns built in count_consistency use +# `\b` word boundaries, which GNU sed honors but BSD sed (macOS default) treats +# as a literal `b`. sed also exits 0 when nothing matched, so the script once +# reported "Fixed: N items" while no file was modified — a silent no-op on every +# macOS run. Shared by pass 1 and pass 2 so the two cannot drift apart again. +apply_line_fix() { + python3 - "$1" "$2" "$3" <<'PYEOF' 2>/dev/null || echo "false" +import re, sys + +file_path, lineno_str, sed_cmd = sys.argv[1], sys.argv[2], sys.argv[3] +# sed_cmd looks like: s/PATTERN/REPLACEMENT/g +# The delimiter is `/` — we never construct anything else. +if not sed_cmd.startswith("s/"): + print("false") + sys.exit(0) +parts = sed_cmd[2:].rsplit("/", 2) +if len(parts) != 3: + print("false") + sys.exit(0) +pattern, replacement, flags = parts +try: + with open(file_path, encoding="utf-8") as f: + lines = f.readlines() +except OSError: + print("false") + sys.exit(0) + +idx = int(lineno_str) - 1 +if idx < 0 or idx >= len(lines): + print("false") + sys.exit(0) +old = lines[idx] +try: + new = re.sub(pattern, replacement, old) +except re.error: + print("false") + sys.exit(0) +if new == old: + print("false") + sys.exit(0) +lines[idx] = new +with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) +print("true") +PYEOF +} + +# The acceptable window is three literal dates (authority-1, authority, +# authority+1), computed ONCE with a single python3 call rather than a python3 +# spawn per claim. Callers then do a plain string comparison. python3 is +# already a hard dependency here (load_counts reads plugin.json with it) — no +# new dependency. +ACCEPTED_RELEASE_DATES="" +compute_release_date_window() { + local authority_date="$1" + ACCEPTED_RELEASE_DATES=$(python3 -c " +import sys, datetime +try: + d = datetime.date.fromisoformat(sys.argv[1]) +except ValueError: + sys.exit(0) +print(' '.join(str(d + datetime.timedelta(days=n)) for n in (-1, 0, 1))) +" "$authority_date" 2>/dev/null) +} +release_date_accepted() { + case " ${ACCEPTED_RELEASE_DATES} " in + *" $1 "*) return 0 ;; + esac + return 1 +} + +# Dedup ledger shared by the broad count scan and the line-shape scan, so a +# stale count sitting inside a structured shape is reported once, not twice. +# A plain string, not an associative array — this script supports bash 3.2. +REPORTED_COUNT_KEYS="" +count_key_reported() { + case "$REPORTED_COUNT_KEYS" in + *"|$1|"*) return 0 ;; + esac + return 1 +} +mark_count_key() { + REPORTED_COUNT_KEYS="${REPORTED_COUNT_KEYS}|$1|" +} + # --------------------------------------------------------------------------- # Result tracking # --------------------------------------------------------------------------- @@ -171,15 +415,39 @@ TOTAL_WARNINGS=0 TOTAL_ERRORS=0 TOTAL_FIXED=0 +# Renders a finding's location the way every consumer has always seen it — +# `path:lineno`, or the bare path when there is no locator. The split above is +# internal; this keeps the JSON contract unchanged. +finding_location() { + local file="$1" locator="$2" + if [[ -n "$locator" ]]; then + echo "${file}:${locator}" + else + echo "$file" + fi +} + +# The finding record carries the file path and the location *within* it as two +# fields, never one glued string. They were glued (`path:lineno`) until +# 2026-08-15, which made pass 2's `[e]xclude` a silent no-op for every Phase 7 +# and Phase 9 finding: `is_pattern_excluded` splits the exclusion entry on its +# first colon, so an entry of `docs/x.md:3:30 command` could never match. It +# printed "Excluded" and the finding returned on the next run. +# +# `locator` is a location, not necessarily a line number: Phase 9's +# site_description finding carries `site_description`, and coverage findings +# carry none at all. Nothing may assume it is numeric — pass 1 gets its line +# number from fix_detail, which is a separate payload. add_finding() { local phase="$1" local severity="$2" # error | warning local file="$3" - local message="$4" - local fixable="${5:-false}" # true if auto-fixable - local fix_detail="${6:-}" # sed command or description + local locator="$4" # line number, named locator, or "" — never glued into file + local message="$5" + local fixable="${6:-false}" # true if auto-fixable + local fix_detail="${7:-}" # sed command or description - local entry="${severity}|${file}|${message}|${fixable}|${fix_detail}" + local entry="${severity}|${file}|${locator}|${message}|${fixable}|${fix_detail}" case "$phase" in 6) PHASE6_FINDINGS+=("$entry") ;; @@ -265,7 +533,7 @@ phase6_nav_completeness() { # Check if this file appears in nav if ! echo "$nav_files" | grep -qF "$rel_path"; then - add_finding 6 "warning" "$file" "Not in mkdocs.yml nav" + add_finding 6 "warning" "$file" "" "Not in mkdocs.yml nav" issues=$((issues + 1)) fi done < <(find docs -name "*.md" -not -path "*/\.*" 2>/dev/null | sort) @@ -273,7 +541,7 @@ phase6_nav_completeness() { # Check for nav entries pointing to missing files while IFS= read -r nav_entry; do if [[ ! -f "docs/$nav_entry" ]]; then - add_finding 6 "error" "mkdocs.yml" "Nav entry 'docs/$nav_entry' — file missing" + add_finding 6 "error" "mkdocs.yml" "" "Nav entry 'docs/$nav_entry' — file missing" issues=$((issues + 1)) fi done < <(echo "$nav_files") @@ -296,10 +564,16 @@ phase7_count_consistency() { # (small numbers like "7 commands" in prose are not total counts) local count_types=("commands" "skills" "agents") local expected_values=("$EXPECTED_CMDS" "$EXPECTED_SKILLS" "$EXPECTED_AGENTS") - # Minimum count to consider: ~40% of expected value (catches old totals, skips prose) + # Minimum count to consider: ~40% of expected value (catches old totals, + # skips prose), floored at 2. Agents' 40% is `2 * 40 / 100 == 0`, so + # without the floor the guard this comment and ADR-007 describe does not + # exist for the smallest count type — every "N agent(s)" mention, however + # small, read as a stale-total candidate (F6). local min_thresholds=() for exp in "${expected_values[@]}"; do - min_thresholds+=("$((exp * 40 / 100))") + local floor=$((exp * 40 / 100)) + (( floor < 2 )) && floor=2 + min_thresholds+=("$floor") done for i in "${!count_types[@]}"; do @@ -307,7 +581,15 @@ phase7_count_consistency() { local expected="${expected_values[$i]}" local min_count="${min_thresholds[$i]}" - # grep for "N commands/skills/agents" patterns in docs + # grep for "N commands/skills/agents" patterns in docs. One regex for + # detection AND the fix anchor -- same reasoning as check 2's span- + # anchored fix (D3): the trailing `\b` this scan used before accepted + # `-` as a boundary the same way check 2's did, so "7 agents-only" + # read as "7 agents" (expected 2). This scan's findings are + # auto-fixable, unlike check 2's, so a boundary-unsafe substitution + # here is worse than F2 -- it can silently corrupt a doc under + # `--fix --non-interactive` with no human in the loop. PROSE_COUNT_TRAILER + # is the same boundary class the shape-scoped check uses. while IFS= read -r match; do [[ -z "$match" ]] && continue local file="${match%%:*}" @@ -318,10 +600,12 @@ phase7_count_consistency() { # Skip excluded files is_file_excluded "$file" && continue - # Extract the number + local full noun + full=$(echo "$content" \ + | grep -oE "[0-9]+ ${ctype}${PROSE_COUNT_TRAILER}" | head -1) + [[ -z "$full" ]] && continue local found_count - found_count=$(echo "$content" | grep -oE "[0-9]+ ${ctype}" | head -1 | grep -oE '[0-9]+') - [[ -z "$found_count" ]] && continue + found_count=$(echo "$full" | grep -oE '^[0-9]+') # Skip if count matches [[ "$found_count" == "$expected" ]] && continue @@ -334,17 +618,174 @@ phase7_count_consistency() { continue fi + # Already reported by the line-shape scan below? Report once. + count_key_reported "${file}:${lineno}:${ctype}" && continue + + # Trim the trailing boundary char the trailer group consumed, so + # the fix substitutes the exact matched text ("48 commands"), not + # a re-derived \b-bounded pattern that could match elsewhere. + noun=$(echo "$full" | sed -E 's/[])} .,;:!?*]$//') + # Determine if auto-fixable (simple count swap) local fixable="true" - local fix_detail="${file}:${lineno}:s/\b${found_count} ${ctype}\b/${expected} ${ctype}/g" + local fix_detail="${file}:${lineno}:s/${noun}/${expected} ${ctype}/" - add_finding 7 "warning" "${file}:${lineno}" \ + add_finding 7 "warning" "$file" "$lineno" \ "'${found_count} ${ctype}' (expected ${expected})" \ "$fixable" "$fix_detail" + mark_count_key "${file}:${lineno}:${ctype}" issues=$((issues + 1)) - done < <(grep -rnE "\b[0-9]+ ${ctype}\b" docs/ CLAUDE.md README.md --include="*.md" 2>/dev/null || true) + done < <(grep -rnE "\b[0-9]+ ${ctype}${PROSE_COUNT_TRAILER}" docs/ CLAUDE.md README.md --include="*.md" 2>/dev/null || true) done + # ----------------------------------------------------------------------- + # Check 2 — count prose inside structured line shapes, singular included. + # The scan above is plural-only and unscoped; this one is shape-scoped and + # therefore safe to run against the singular form too. Ordering matters: + # the broad scan marks its keys first, so a line both scans can see is + # reported by whichever reaches it first and skipped by the other. + # ----------------------------------------------------------------------- + # Build the scannable file list once, then hand it to each scan in a single + # awk invocation (see emit_shaped_lines' header note on why per-file awk + # calls were a 4x slowdown). + local prose_file shaped sfile srest slineno shape scontent singular found j + local -a prose_files=() + while IFS= read -r prose_file; do + [[ -z "$prose_file" ]] && continue + [[ -f "$prose_file" ]] || continue + is_file_excluded "$prose_file" && continue + prose_files+=("$prose_file") + done < <(printf '%s\n' CLAUDE.md README.md; find docs -name '*.md' 2>/dev/null || true) + + while IFS= read -r shaped; do + [[ -z "$shaped" ]] && continue + sfile="${shaped%%:*}"; srest="${shaped#*:}" + slineno="${srest%%:*}"; srest="${srest#*:}" + shape="${srest%%:*}"; scontent="${srest#*:}" + + for j in "${!count_types[@]}"; do + local ctype2="${count_types[$j]}" + local expected2="${expected_values[$j]}" + local min2="${min_thresholds[$j]}" + singular="${ctype2%s}" + + # A structure-table row names its own type in the first cell, so + # only compare against that type — a `commands/` row must not be + # measured against the agent count. + if [[ "$shape" == "structure-table" ]]; then + echo "$scontent" | grep -qE "^\|[[:space:]]*\`${ctype2}/\`" || continue + fi + + # One regex for detection AND the fix anchor. F2 was a second, + # looser regex used only to build `noun` (no trailer at all) — + # it re-matched "command" inside "command-line" even after the + # detection regex's own trailer would have rejected the line, + # so [f]ix rewrote "30 command-line entry points" into + # "48 command-line entry points". Capturing one span and + # deriving both the report and the substitution from it makes + # that drift impossible. + local full + full=$(echo "$scontent" \ + | grep -oE "[0-9]+ (specialized )?(${ctype2}|${singular})${PROSE_COUNT_TRAILER}" \ + | head -1) + [[ -z "$full" ]] && continue + found=$(echo "$full" | grep -oE '^[0-9]+') + [[ "$found" == "$expected2" ]] && continue + + # Same 40%-of-expected floor the broad scan applies. Structured + # shapes still carry non-total counts: category subtotals inside + # a reference box ("SMART (4 commands)"), a bolded subset count + # ("`--refine` is declared on **9 commands**"), and narrative + # counts about other plugins ("kept shipping **0 skills**"). + # Without this floor all three read as stale totals. + [[ "$found" -lt "$min2" ]] && continue + + count_key_reported "${sfile}:${slineno}:${ctype2}" && continue + is_pattern_excluded "$sfile" "${found} ${ctype2}" && continue + is_pattern_excluded "$sfile" "${found} ${singular}" && continue + + # Trim the single trailing boundary char the trailer group + # consumed (space/closing punctuation) back off, so the + # reported/substituted noun is "8 agent", not "8 agent ". `$` + # in the trailer consumes nothing, so an end-of-line match is + # already bare and this is a no-op for it. + local noun + noun=$(echo "$full" | sed -E 's/[])} .,;:!?*]$//') + + # Not auto-fixable: the surrounding prose ("8 agent definitions") + # is hand-authored, so a blind count swap can produce grammatical + # nonsense. Routed to the interactive pass instead. + # + # Routed to pass 2 (uncertain), not pass 1: the surrounding + # prose is hand-authored, so a human should see the line before + # the number changes under it. The fix_detail is still a real + # substitution so that confirming it actually edits the file — + # it swaps only the digits, leaving "agent definitions" intact. + add_finding 7 "warning" "$sfile" "$slineno" \ + "prose[${shape}]: '${noun}' (expected ${expected2})" \ + "uncertain" "${sfile}:${slineno}:s/${noun}/${expected2}${noun#"$found"}/" + mark_count_key "${sfile}:${slineno}:${ctype2}" + issues=$((issues + 1)) + done + done < <(emit_shaped_lines "${prose_files[@]+"${prose_files[@]}"}") + + # ----------------------------------------------------------------------- + # Check 1 — release-date claims agree with each other for the current + # version. No external authority (D1): the tag doesn't exist at either + # point this check runs (see the comment on majority_date). Vacuous + # (skipped, not failed) with 0 or 1 claims -- there is nothing to compare + # a lone claim against, which is the normal state on a feature branch + # before release, or when only one doc in the repo names a release date at + # all. Accepted cost: a date that is uniformly wrong in every file agrees + # with itself and is never caught. + # ----------------------------------------------------------------------- + local claim cfile crest clineno cdate + local -a claim_files=() claim_lines=() claim_dates=() + while IFS= read -r claim; do + [[ -z "$claim" ]] && continue + cfile="${claim%%:*}"; crest="${claim#*:}" + clineno="${crest%%:*}"; cdate="${crest#*:}" + is_file_excluded "$cfile" && continue + claim_files+=("$cfile") + claim_lines+=("$clineno") + claim_dates+=("$cdate") + done < <(emit_release_date_claims "${prose_files[@]+"${prose_files[@]}"}") + + if [[ "${#claim_dates[@]}" -ge 2 ]]; then + local authority + authority="$(majority_date "${claim_dates[@]}")" + compute_release_date_window "$authority" + # Empty window means the majority date itself failed to parse -- + # cannot happen in practice (it came from the [0-9]{4}-[0-9]{2}-[0-9]{2} + # regex that fed emit_release_date_claims), but a broken authority must + # make this vacuous, never universal, same posture as the old + # missing-tag case: an empty accept-window would otherwise fail every + # claim in the repo at once. + if [[ -n "$ACCEPTED_RELEASE_DATES" ]]; then + for i in "${!claim_dates[@]}"; do + cdate="${claim_dates[$i]}" + release_date_accepted "$cdate" && continue + cfile="${claim_files[$i]}"; clineno="${claim_lines[$i]}" + is_pattern_excluded "$cfile" "$cdate" && continue + + # error (D2, promoted per D11): shipped as "warning" while + # the redesign (D1, D6) itself was unproven -- a passing + # unit suite written by the same author in the same sitting + # isn't evidence. Promoted once a live-repo run came back + # clean (0 findings) across every tracked doc AND both real + # claim sites (docs/NEWS.md, docs/REFCARD.md), plus a + # transcript of the check actually firing: injecting + # 2020-01-01 into docs/REFCARD.md:7 produced "release date + # '2020-01-01' for v4.5.0 disagrees with other claims + # (majority: 2026-08-07)", reverted after confirming. + add_finding 7 "error" "$cfile" "$clineno" \ + "release date '${cdate}' for v${CURRENT_VERSION} disagrees with other claims (majority: ${authority})" \ + "uncertain" "${cfile}:${clineno}:s/${cdate}/${authority}/" + issues=$((issues + 1)) + done + fi + fi + print_phase_status "$issues" } @@ -393,7 +834,7 @@ phase8_skill_agent_coverage() { fi if ! $documented; then - add_finding 8 "warning" "$cmd_file" \ + add_finding 8 "warning" "$cmd_file" "" \ "Command '${cmd_name}' not in docs (commands.md, docs/commands/)" \ "uncertain" "command:${cmd_file}" issues=$((issues + 1)) @@ -415,7 +856,7 @@ phase8_skill_agent_coverage() { if [[ -f "$skill_file" ]]; then desc=$(sed -n '/^---$/,/^---$/{ /^description:/s/^description:[[:space:]]*//p; }' "$skill_file" 2>/dev/null | head -1) fi - add_finding 8 "warning" "$skill_file" \ + add_finding 8 "warning" "$skill_file" "" \ "Not documented in $skills_doc" \ "uncertain" "skill:${skill_file}:${desc}" issues=$((issues + 1)) @@ -430,7 +871,7 @@ phase8_skill_agent_coverage() { local agent_name agent_name=$(basename "$(dirname "$agent_file")")/$(basename "$agent_file" .md) if ! grep -q "$agent_file\|$agent_name" "$skills_doc" 2>/dev/null; then - add_finding 8 "warning" "$agent_file" \ + add_finding 8 "warning" "$agent_file" "" \ "Not documented in $skills_doc" \ "uncertain" "agent:${agent_file}" issues=$((issues + 1)) @@ -455,7 +896,7 @@ phase8_skill_agent_coverage() { # Map block→error, warn→warning for staleness check convention local sev="warning" [[ "$severity_raw" == "block" ]] && sev="error" - add_finding 8 "$sev" "commands/${cmd//:///}.md" \ + add_finding 8 "$sev" "commands/${cmd//:///}.md" "" \ "$message" "uncertain" "doc-coverage:${surface}:${cmd}" issues=$((issues + 1)) done < <(echo "$cov_json" | grep '"cmd"' || true) @@ -494,7 +935,7 @@ phase9_cross_doc_freshness() { if [[ "$found_ver" != "$CURRENT_VERSION" ]]; then local fixable="true" local fix_detail="${file}:${lineno}:s/${found_ver}/${CURRENT_VERSION}/g" - add_finding 9 "warning" "${file}:${lineno}" \ + add_finding 9 "warning" "$file" "$lineno" \ "Version '${found_ver}' (current: ${CURRENT_VERSION})" \ "$fixable" "$fix_detail" issues=$((issues + 1)) @@ -512,7 +953,7 @@ phase9_cross_doc_freshness() { if [[ -n "$site_ver" ]]; then site_ver="${site_ver#v}" if [[ "$site_ver" != "$CURRENT_VERSION" ]]; then - add_finding 9 "warning" "mkdocs.yml:site_description" \ + add_finding 9 "warning" "mkdocs.yml" "site_description" \ "References v${site_ver} (current: v${CURRENT_VERSION})" \ "uncertain" "" issues=$((issues + 1)) @@ -545,7 +986,7 @@ phase9_cross_doc_freshness() { if $stale; then is_pattern_excluded "$file" "$content" && continue - add_finding 9 "warning" "${file}:${lineno}" \ + add_finding 9 "warning" "$file" "$lineno" \ "Stale counts in summary (skills: ${see_skills:-?}/${EXPECTED_SKILLS}, agents: ${see_agents:-?}/${EXPECTED_AGENTS})" \ "uncertain" "" issues=$((issues + 1)) @@ -565,7 +1006,7 @@ phase9_cross_doc_freshness() { [[ -z "$found_count" ]] && continue [[ "$found_count" == "$EXPECTED_CMDS" ]] && continue is_pattern_excluded "$file" "${found_count} commands" && continue - add_finding 9 "warning" "${file}:${lineno}" \ + add_finding 9 "warning" "$file" "$lineno" \ "Architecture doc: '${found_count} commands' (current: ${EXPECTED_CMDS})" \ "uncertain" "" issues=$((issues + 1)) @@ -593,7 +1034,9 @@ pass1_auto_fix() { fi for item in "${FIXABLE_ITEMS[@]}"; do - # Parse: phase|severity|file|message|fixable|fix_detail + # Parse: phase|severity|file|locator|message|fixable|fix_detail + # fix_detail is a separate payload (path:lineno:sed_cmd), not derived + # from the record's own file/locator fields. local fix_detail="${item##*|}" local file="${fix_detail%%:*}" local rest="${fix_detail#*:}" @@ -601,54 +1044,8 @@ pass1_auto_fix() { local sed_cmd="${rest#*:}" if [[ -f "$file" ]]; then - # Apply the fix via python3 instead of `sed -i`. - # Why: the sed patterns built in count_consistency use `\b` word - # boundaries, which GNU sed honors but BSD sed (macOS default) - # treats as literal `b`. The sed call previously returned exit 0 - # even when nothing matched, so the script reported "Fixed: N - # items" while no file was actually modified — silent no-op on - # every macOS run. Python's re.sub gets the contract right on - # every platform and lets us also detect "nothing actually - # changed" via a proper before/after comparison. local fixed_one=false - fixed_one="$(python3 - "$file" "$lineno" "$sed_cmd" <<'PYEOF' 2>/dev/null || echo "false" -import re, sys - -file_path, lineno_str, sed_cmd = sys.argv[1], sys.argv[2], sys.argv[3] -# sed_cmd looks like: s/PATTERN/REPLACEMENT/g -# Parse it. The delimiter is `/` (we never use anything else when constructing). -if not sed_cmd.startswith("s/"): - print("false") - sys.exit(0) -parts = sed_cmd[2:].rsplit("/", 2) -if len(parts) != 3: - print("false") - sys.exit(0) -pattern, replacement, flags = parts -# Translate `\b` (GNU sed word boundary) into Python regex's equivalent. -# Python's re already supports \b, so the pattern is mostly portable as-is. -try: - with open(file_path, encoding="utf-8") as f: - lines = f.readlines() -except OSError: - print("false") - sys.exit(0) - -idx = int(lineno_str) - 1 -if idx < 0 or idx >= len(lines): - print("false") - sys.exit(0) -old = lines[idx] -new = re.sub(pattern, replacement, old) -if new == old: - print("false") - sys.exit(0) -lines[idx] = new -with open(file_path, "w", encoding="utf-8") as f: - f.writelines(lines) -print("true") -PYEOF -)" + fixed_one="$(apply_line_fix "$file" "$lineno" "$sed_cmd")" if [[ "$fixed_one" == "true" ]]; then TOTAL_FIXED=$((TOTAL_FIXED + 1)) if [[ "$JSON_MODE" != "true" ]]; then @@ -695,10 +1092,12 @@ pass2_interactive_review() { local idx=0 for item in "${UNCERTAIN_ITEMS[@]}"; do idx=$((idx + 1)) - # Parse: phase|severity|file|message|fixable|fix_detail - IFS='|' read -r phase severity file message fixable fix_detail <<< "$item" + # Parse: phase|severity|file|locator|message|fixable|fix_detail + IFS='|' read -r phase severity file locator message fixable fix_detail <<< "$item" + local location + location="$(finding_location "$file" "$locator")" - echo -e "[${idx}/${#UNCERTAIN_ITEMS[@]}] ${YELLOW}${file}${NC}" + echo -e "[${idx}/${#UNCERTAIN_ITEMS[@]}] ${YELLOW}${location}${NC}" echo " ${message}" local response="" @@ -709,8 +1108,20 @@ pass2_interactive_review() { case "$response" in f) - if [[ -n "$fix_detail" ]]; then - echo -e " -> ${GREEN}Fixed${NC}" + # Actually apply it. This branch used to print "Fixed" and bump + # TOTAL_FIXED without touching the file — the same + # reports-success-changes-nothing bug pass 1 was fixed for + # earlier, left behind in pass 2. Both now share apply_line_fix. + local target_file="${fix_detail%%:*}" + local fix_rest="${fix_detail#*:}" + local target_line="${fix_rest%%:*}" + local fix_cmd="${fix_rest#*:}" + local applied="false" + if [[ -n "$fix_detail" && -f "$target_file" ]]; then + applied="$(apply_line_fix "$target_file" "$target_line" "$fix_cmd")" + fi + if [[ "$applied" == "true" ]]; then + echo -e " -> ${GREEN}Fixed${NC} ${target_file}:${target_line}" TOTAL_FIXED=$((TOTAL_FIXED + 1)) else echo " -> Cannot auto-fix (manual edit needed)" @@ -720,7 +1131,10 @@ pass2_interactive_review() { echo " -> Skipped" ;; e) - # Add to exclusions file + # Add to exclusions file. $file is the bare path now that the + # locator is a separate field (D4) — the written entry is + # path:pattern, matching what is_pattern_excluded parses, not + # path:lineno:pattern which could never match. local excl_entry="$file" # For pattern items, extract the pattern portion if [[ "$message" == *"'"*"'"* ]]; then @@ -938,13 +1352,18 @@ findings_to_json() { echo -n "[" for (( _i=0; _i<_count; _i++ )); do eval "entry=\${${_arr_name}[$_i]}" - IFS='|' read -r severity file message fixable fix_detail <<< "$entry" + IFS='|' read -r severity file locator message fixable fix_detail <<< "$entry" + # Render the location the way every consumer has always seen it — + # path:locator, or the bare path — so the JSON contract is unchanged + # even though the record now carries the two as separate fields. + local location + location="$(finding_location "$file" "$locator")" # Escape JSON strings - file=$(echo "$file" | sed 's/\\/\\\\/g;s/"/\\"/g') + location=$(echo "$location" | sed 's/\\/\\\\/g;s/"/\\"/g') message=$(echo "$message" | sed 's/\\/\\\\/g;s/"/\\"/g') if ! $first; then echo -n ","; fi first=false - echo -n "{\"severity\":\"${severity}\",\"file\":\"${file}\",\"message\":\"${message}\"}" + echo -n "{\"severity\":\"${severity}\",\"file\":\"${location}\",\"message\":\"${message}\"}" done echo -n "]" } diff --git a/skills/release/references/homebrew.md b/skills/release/references/homebrew.md index f01d66ed6..02f9b3f2a 100644 --- a/skills/release/references/homebrew.md +++ b/skills/release/references/homebrew.md @@ -132,6 +132,12 @@ fi Skip if no local tap exists — the GitHub Actions workflow (`homebrew-release.yml`) handles tap updates automatically on release trigger. +> **Editing tap formula content outside a version release?** (e.g. fixing caveat text in +> `generator/manifest.json` directly, not via a `/release` version bump) — that change needs a +> `revision` bump in the formula's manifest entry, or `homebrew-tap`'s Formula Drift Guard CI +> check will block the PR (and `brew upgrade` would silently never detect the change on +> already-installed machines). See `/craft:dist:homebrew formula`'s docs for the full rule. + #### Verify Homebrew Release Workflow After the GitHub release is created, verify the `homebrew-release` workflow succeeded: diff --git a/skills/release/references/pipeline-steps.md b/skills/release/references/pipeline-steps.md index d27d18b74..5058f3384 100644 --- a/skills/release/references/pipeline-steps.md +++ b/skills/release/references/pipeline-steps.md @@ -122,11 +122,20 @@ After `bump-version.sh` handles mechanical version substitution, update these fi | `VERSION-HISTORY.md` | Insert new version section with highlights | | `README.md` | Update release title line (if present) | | `docs/index.md` | Update `!!! info` box title and description text | -| `docs/REFCARD.md` | Update summary line ~11 title text (after the version) | +| `docs/REFCARD.md` | Update summary line ~11 title text (after the version); update the version box's release date (line ~7) | +| `docs/NEWS.md` | Insert new `## vX.Y.Z` entry with a `**Released:** YYYY-MM-DD` date — not template-generated, added by hand | | `mkdocs.yml` | Update `site_description` tagline after "adds" to describe new release | | `commands/hub.md` | Update version in banner template, test count, skill count | | `docs/commands/hub.md` | Same updates as `commands/hub.md` (published copy) | +**Release-date convention (D9, `GRILL-prose-check-hardening-2026-08-15.md`):** `docs/NEWS.md`'s +`**Released:**` date and `docs/REFCARD.md`'s box date must be the **same date** — the git tag's +local date, not the GitHub API's UTC release timestamp (the two differ whenever a release +publishes across the UTC boundary, e.g. v4.5.0: tag `2026-08-07` local, GitHub release +`2026-08-08T03:44Z`). `docs-staleness-check.sh`'s check 1 (`ADR-007`) compares these two claims +against each other and tolerates a one-day gap for the UTC-boundary case — using the same date at +both sites keeps that tolerance as headroom instead of load-bearing. + **Key distinction:** Step 3 handles mechanical `X.Y.Z` substitution. Step 3b handles semantic content that requires the release title and human judgment. For **other project types**, update manually: diff --git a/tests/MANUAL-TESTING-CHECKLIST.md b/tests/MANUAL-TESTING-CHECKLIST.md deleted file mode 100644 index b6cb90a13..000000000 --- a/tests/MANUAL-TESTING-CHECKLIST.md +++ /dev/null @@ -1,362 +0,0 @@ -# Teaching Workflow Manual Testing Checklist - -## Overview - -This checklist covers manual testing scenarios that complement automated tests. -Use with a real teaching project (e.g., STAT 545) when available. - -## Pre-Testing Setup - -- [ ] Identify test teaching project path -- [ ] Backup project before testing -- [ ] Verify git status is clean -- [ ] Note current git branch - -## 1. Teaching Mode Detection - -### Test Scenarios - -- [ ] **Config Detection**: Project with teach-config.yml - - Expected: Detects as teaching via "config" method - - Command: Run detection on project with config - -- [ ] **Structure Detection**: Project with syllabus.qmd + schedule.qmd - - Expected: Detects as teaching via "structure" method - - Command: Remove config temporarily, test detection - -- [ ] **Non-Teaching Project**: Random code project - - Expected: Not detected as teaching - - Command: Test detection on ~/projects/dev-tools/flow-cli - -### Verification - -```bash -# From test project directory -python3 -c " -import sys -from pathlib import Path -sys.path.insert(0, '/path/to/craft') -from utils.detect_teaching_mode import detect_teaching_mode -is_teaching, method = detect_teaching_mode('.') -print(f'Teaching: {is_teaching}, Method: {method}') -" -``` - -## 2. Configuration Loading & Validation - -### Test Scenarios - -- [ ] **Valid Minimal Config**: Only required fields - - Expected: Loads successfully, no errors - - Fixture: `tests/fixtures/teaching/minimal` - -- [ ] **Valid Full Config**: All optional fields - - Expected: Loads successfully, parses all sections - - Fixture: `tests/fixtures/teaching/stat-545` - -- [ ] **Invalid YAML**: Syntax errors - - Expected: Returns None or error message - - Create: Temporarily break YAML syntax - -- [ ] **Missing Required Fields**: Incomplete config - - Expected: Validation errors reported - - Create: Remove required fields temporarily - -- [ ] **Invalid Dates**: End before start - - Expected: Validation error - - Create: Swap start/end dates - -- [ ] **Overlapping Breaks**: Two breaks overlap - - Expected: Validation error - - Create: Configure overlapping break dates - -### Verification - -```bash -python3 tests/test_teach_config.py -``` - -## 3. Progress Calculation - -### Test Scenarios - -- [ ] **During Semester**: Current date within semester - - Expected: Week 1-16, percentage 0-100 - - Test: Use real course during semester - -- [ ] **Before Semester**: Current date before start - - Expected: Week ≤ 0 or "not started" status - - Create: Future course config - -- [ ] **After Semester**: Current date after end - - Expected: 100% complete or "ended" status - - Create: Past course config - -- [ ] **During Break**: Currently on break - - Expected: Break status indicated - - Test: Configure break around current date - -- [ ] **No Breaks**: Summer session - - Expected: Progress calculated without breaks - - Fixture: `tests/fixtures/teaching/summer` - -### Verification - -```bash -python3 tests/test_semester_progress.py -``` - -## 4. Content Validation - -### Test Scenarios - -- [ ] **Complete Syllabus**: All recommended sections - - Expected: No errors, all checkmarks - - Test: Well-formed syllabus - -- [ ] **Missing Syllabus Sections**: Incomplete - - Expected: Warnings for missing sections - - Create: Remove grading section - -- [ ] **Complete Schedule**: Proper week structure - - Expected: No errors - - Test: Well-formed schedule - -- [ ] **Malformed Schedule**: Missing week headers - - Expected: Warnings reported - - Create: Remove ## Week headers - -- [ ] **Missing Assignments**: No homework files - - Expected: Info message (not error) - - Test: Project without assignments/ - -### Verification - -```bash -python3 tests/test_teaching_validation.py -``` - -## 5. Publishing Workflow - -### Test Scenarios - -**WARNING**: These tests modify git state. Use on test branch only! - -- [ ] **Clean Publish**: No uncommitted changes - - Expected: Builds, pushes, deploys successfully - - Setup: Clean git status - -- [ ] **Dirty Publish**: Uncommitted changes - - Expected: Prompts to commit or stash - - Setup: Make uncommitted edits - -- [ ] **Build Failure**: Quarto errors - - Expected: Rollback, clear error message - - Create: Break quarto syntax - -- [ ] **Deployment Verification**: Check GitHub Pages - - Expected: Site accessible, content updated - - Verify: Visit - -- [ ] **Rollback on Failure**: Git restored on error - - Expected: Returns to pre-publish state - - Test: Simulate failure (break build) - -### Verification - -```bash -# Dry run first -python3 commands/teaching/publish.py --dry-run - -# Real publish (test branch only!) -python3 commands/teaching/publish.py -``` - -## 6. Dashboard & Progress Display - -### Test Scenarios - -- [ ] **Progress Bar**: Visual indicator - - Expected: ASCII progress bar, percentage - - Test: View dashboard during semester - -- [ ] **Week Display**: Current week highlighted - - Expected: Week X of Y format - - Test: View at different semester points - -- [ ] **Break Indicator**: Currently on break - - Expected: Break name shown - - Test: Configure break around today - -- [ ] **Upcoming Milestones**: Next 3 assignments - - Expected: Due dates listed - - Test: Course with upcoming deadlines - -- [ ] **Dashboard Layout**: ADHD-friendly format - - Expected: Short paragraphs, visual structure - - Test: Read dashboard output - -### Verification - -```bash -# View semester progress -python3 commands/teaching/semester-progress.py - -# View full dashboard (when implemented) -python3 commands/teaching/dashboard.py -``` - -## 7. Cross-Component Integration - -### Test Scenarios - -- [ ] **Detection → Config → Validation**: Full chain - - Expected: Smooth flow, no errors - - Test: Run all three in sequence - -- [ ] **Config → Progress → Dashboard**: Data flow - - Expected: Consistent data across tools - - Test: Verify week numbers match - -- [ ] **Validation → Publish**: Blocking errors - - Expected: Publish blocked if validation fails - - Test: Try publishing with errors - -### Verification - -Run complete workflow manually: - -```bash -# 1. Detect -python3 utils/detect_teaching_mode.py - -# 2. Validate -python3 commands/teaching/validate.py - -# 3. Progress -python3 commands/teaching/semester-progress.py - -# 4. Publish (if clean) -python3 commands/teaching/publish.py --dry-run -``` - -## 8. Performance Checks - -### Test Scenarios - -- [ ] **Detection Speed**: < 100ms - - Method: Time detection command - - Target: Instant response - -- [ ] **Config Parsing**: < 200ms - - Method: Time config load - - Target: Very fast - -- [ ] **Full Validation**: < 5s - - Method: Time validation command - - Target: Complete in seconds - -- [ ] **Progress Calculation**: < 100ms - - Method: Time progress calculation - - Target: Instant - -### Verification - -```bash -# Time each component -time python3 -c "from utils.detect_teaching_mode import detect_teaching_mode; detect_teaching_mode('.')" -time python3 -c "from commands.utils.teach_config import load_teach_config; load_teach_config('.')" -time python3 commands/teaching/validate.py -time python3 commands/teaching/semester-progress.py -``` - -## 9. Error Handling & Messages - -### Test Scenarios - -- [ ] **Clear Error Messages**: Actionable guidance - - Expected: Specific error + suggestion - - Test: Trigger various errors - -- [ ] **Graceful Degradation**: Partial failures - - Expected: Continues with warnings - - Test: Missing optional content - -- [ ] **Help Text**: Useful documentation - - Expected: Clear usage instructions - - Test: Run with --help - -### Verification - -Trigger errors intentionally and verify messages are helpful. - -## 10. Edge Cases - -### Test Scenarios - -- [ ] **Empty Config**: Valid YAML but no content - - Expected: Validation errors for missing fields - -- [ ] **Very Long Semester**: 20+ weeks - - Expected: Handles correctly - -- [ ] **Very Short Semester**: 4 weeks (summer) - - Expected: Handles correctly - - Fixture: `tests/fixtures/teaching/summer` - -- [ ] **Many Breaks**: 5+ break periods - - Expected: All counted correctly - -- [ ] **Break Spanning Weeks**: Multi-week break - - Expected: All weeks excluded - -- [ ] **Unicode Content**: Non-ASCII characters - - Expected: Handles correctly - -### Verification - -Create edge case configs and test each component. - -## Success Criteria - -### Must Pass - -- [x] All automated tests pass (139+ tests) -- [ ] All detection scenarios work -- [ ] All validation checks accurate -- [ ] Progress calculation correct -- [ ] Publish workflow safe (no data loss) - -### Quality Metrics - -- [ ] Performance targets met -- [ ] Error messages clear and actionable -- [ ] ADHD-friendly output maintained -- [ ] No false positives (warnings OK) - -## Testing Notes - -### Date - -### Tester - -### Project Used - -### Issues Found - -1. - -### Suggestions - -1. - -## Sign-Off - -- [ ] All critical scenarios tested -- [ ] No blocking bugs found -- [ ] Documentation accurate -- [ ] Ready for merge - -**Tester Signature**: ___________________________ - -**Date**: ___________________________ diff --git a/tests/demo_teaching_validation.py b/tests/demo_teaching_validation.py deleted file mode 100644 index 16b866fdf..000000000 --- a/tests/demo_teaching_validation.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 -"""Demo script for teaching content validation. - -Shows validation output for different course scenarios: -1. Complete valid course -2. Course with missing syllabus sections -3. Course with incomplete schedule -4. Course with missing assignments -""" - -import sys -import tempfile -from pathlib import Path - -import pytest - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from commands.utils.teaching_validation import validate_teaching_content - -pytestmark = [pytest.mark.e2e, pytest.mark.teaching] - - -def create_demo_course(scenario: str) -> Path: - """Create a temporary course directory for demo.""" - course_dir = Path(tempfile.mkdtemp()) - - if scenario == "complete": - # Complete valid course - (course_dir / 'syllabus.qmd').write_text(""" -# Course Syllabus - -## Learning Objectives -Students will master statistical methods and apply them to real-world problems. - -## Course Policies -Attendance is required. Late work accepted with 24-hour notice. - -## Grading -- Homework: 40% -- Exams: 60% - -## Schedule -See the full schedule page for weekly topics and assignments. - """) - - (course_dir / 'schedule.qmd').write_text(""" -# Course Schedule - -## Week 1 -Introduction to statistics. Read Chapter 1. -Complete HW 1 by Friday. - -## Week 2 -Descriptive statistics. Read Chapter 2. -Complete HW 2 by Friday. - -## Week 3 -Probability theory. Read Chapter 3. -Midterm exam on Friday. - """) - - assignments_dir = course_dir / 'assignments' - assignments_dir.mkdir() - (assignments_dir / 'hw-1.qmd').write_text("# Homework 1") - (assignments_dir / 'hw-2.qmd').write_text("# Homework 2") - - elif scenario == "missing_syllabus": - # Missing syllabus sections - (course_dir / 'syllabus.qmd').write_text(""" -# Course Syllabus - -## Grading -Homework and exams. - """) - - (course_dir / 'schedule.qmd').write_text(""" -## Week 1 -Introduction with plenty of content here. - """) - - elif scenario == "incomplete_schedule": - # Incomplete schedule - (course_dir / 'syllabus.qmd').write_text(""" -# Syllabus - -## Objectives -Learn statistics. - -## Policies -Be nice. - -## Grading -Homework 50%, Exams 50%. - -## Schedule -See schedule page. - """) - - (course_dir / 'schedule.qmd').write_text(""" -## Week 1 -Introduction. Lots of content here. - -## Week 2 - -## Week 3 -More content here. - """) - - elif scenario == "missing_assignments": - # Missing assignment files - (course_dir / 'syllabus.qmd').write_text(""" -# Syllabus - -## Objectives -Learn statistics. - -## Policies -Be respectful. - -## Grading -Homework counts. - -## Schedule -Weekly topics. - """) - - (course_dir / 'schedule.qmd').write_text(""" -## Week 1 -Introduction. HW 1 due Friday. Good content here. - -## Week 2 -Descriptive stats. HW 2 due Friday. More content. - -## Week 3 -Probability. HW 3 due Friday. Even more content. - """) - - # Only create HW 1, missing HW 2 and HW 3 - assignments_dir = course_dir / 'assignments' - assignments_dir.mkdir() - (assignments_dir / 'hw-1.qmd').write_text("# Homework 1") - - return course_dir - - -def run_demo(): - """Run validation demos for all scenarios.""" - scenarios = [ - ("complete", "✅ Complete Valid Course"), - ("missing_syllabus", "❌ Missing Syllabus Sections"), - ("incomplete_schedule", "❌ Incomplete Schedule"), - ("missing_assignments", "⚠️ Missing Assignments"), - ] - - for scenario_id, scenario_name in scenarios: - print(f"\n{'=' * 70}") - print(f"SCENARIO: {scenario_name}") - print('=' * 70) - - # Create demo course - course_dir = create_demo_course(scenario_id) - - # Run validation - result = validate_teaching_content(str(course_dir)) - - # Print report - print(result.format_report()) - - # Cleanup - import shutil - shutil.rmtree(course_dir) - - -if __name__ == '__main__': - print("Teaching Content Validation Demo") - print("=" * 70) - print("This demo shows validation output for different course scenarios.") - run_demo() - print("\n" + "=" * 70) - print("Demo complete!") - print("=" * 70) diff --git a/tests/fixtures/prose-staleness/clean/release-date-companion.md b/tests/fixtures/prose-staleness/clean/release-date-companion.md new file mode 100644 index 000000000..5d9c4505a --- /dev/null +++ b/tests/fixtures/prose-staleness/clean/release-date-companion.md @@ -0,0 +1,11 @@ + + +# News + +## v4.5.0 — repo-triage skill + ecosystem-tool tutorials + +**Released:** 2026-08-07 · **Type:** Minor diff --git a/tests/fixtures/prose-staleness/clean/release-date-utc-boundary.md b/tests/fixtures/prose-staleness/clean/release-date-utc-boundary.md new file mode 100644 index 000000000..aa6deb7ea --- /dev/null +++ b/tests/fixtures/prose-staleness/clean/release-date-utc-boundary.md @@ -0,0 +1,10 @@ + + + +# News + +## v4.5.0 — repo-triage skill + ecosystem-tool tutorials + +**Released:** 2026-08-08 · **Type:** Minor + +Without the one-day window this check would flag a correct repo on its first run. diff --git a/tests/fixtures/prose-staleness/clean/structure-table-correct.md b/tests/fixtures/prose-staleness/clean/structure-table-correct.md new file mode 100644 index 000000000..4aacb5b09 --- /dev/null +++ b/tests/fixtures/prose-staleness/clean/structure-table-correct.md @@ -0,0 +1,10 @@ + + +# Project Structure + +| Directory | Purpose | +|-----------|---------| +| `commands/` | 48 commands (auto-discovered, one `.md` per command) | +| `skills/` | 41 skills (`SKILL.md` pattern) | +| `agents/` | 2 agent definitions | +| `tests/` | Unit, e2e, dogfood, and integration test suites | diff --git a/tests/fixtures/prose-staleness/clean/tldr-correct.md b/tests/fixtures/prose-staleness/clean/tldr-correct.md new file mode 100644 index 000000000..61546e794 --- /dev/null +++ b/tests/fixtures/prose-staleness/clean/tldr-correct.md @@ -0,0 +1,7 @@ + + +# Skills and Agents + +**TL;DR** — craft ships 41 skills and 2 specialized agents. + +The rest of this page enumerates them. diff --git a/tests/fixtures/prose-staleness/clean/version-box-correct.md b/tests/fixtures/prose-staleness/clean/version-box-correct.md new file mode 100644 index 000000000..63d31316f --- /dev/null +++ b/tests/fixtures/prose-staleness/clean/version-box-correct.md @@ -0,0 +1,13 @@ + + +# Quick Reference + +```text +┌─────────────────────────────────────────────┐ +│ craft v4.5.0 │ +│ Released: 2026-08-07 │ +│ 48 commands · 41 skills · 2 agents │ +└─────────────────────────────────────────────┘ +``` + +Nothing on this page contradicts the plugin manifest. diff --git a/tests/fixtures/prose-staleness/defect/release-date-far-edge.md b/tests/fixtures/prose-staleness/defect/release-date-far-edge.md new file mode 100644 index 000000000..648dfadba --- /dev/null +++ b/tests/fixtures/prose-staleness/defect/release-date-far-edge.md @@ -0,0 +1,12 @@ + + + + + +# News + +## v4.5.0 — repo-triage skill + ecosystem-tool tutorials + +**Type:** Minor + +**Released:** 2026-07-19 diff --git a/tests/fixtures/prose-staleness/defect/structure-table-singular.md b/tests/fixtures/prose-staleness/defect/structure-table-singular.md new file mode 100644 index 000000000..c38bce96d --- /dev/null +++ b/tests/fixtures/prose-staleness/defect/structure-table-singular.md @@ -0,0 +1,9 @@ + + + +# Project Structure + +| Directory | Purpose | +|-----------|---------| +| `commands/` | 48 commands (auto-discovered, one `.md` per command) | +| `agents/` | 8 agent definitions | diff --git a/tests/fixtures/prose-staleness/defect/tldr-eight-agents.md b/tests/fixtures/prose-staleness/defect/tldr-eight-agents.md new file mode 100644 index 000000000..c7449647e --- /dev/null +++ b/tests/fixtures/prose-staleness/defect/tldr-eight-agents.md @@ -0,0 +1,7 @@ + + +# Skills and Agents + +**TL;DR** — craft ships 41 skills and 8 specialized agents. + +Two lines below, the page's own table correctly lists 2. diff --git a/tests/fixtures/prose-staleness/defect/version-box-stale-date.md b/tests/fixtures/prose-staleness/defect/version-box-stale-date.md new file mode 100644 index 000000000..ca8c92bab --- /dev/null +++ b/tests/fixtures/prose-staleness/defect/version-box-stale-date.md @@ -0,0 +1,12 @@ + + + +# Quick Reference + +```text +┌─────────────────────────────────────────────┐ +│ craft v4.5.0 │ +│ Released: 2026-07-19 │ +│ 48 commands · 41 skills · 2 agents │ +└─────────────────────────────────────────────┘ +``` diff --git a/tests/fixtures/prose-staleness/falsepos/category-subtotal-box.md b/tests/fixtures/prose-staleness/falsepos/category-subtotal-box.md new file mode 100644 index 000000000..2f24dacce --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/category-subtotal-box.md @@ -0,0 +1,13 @@ + + + + +# Playground + +```text +┌─────────────────────────────────────────────────────────────┐ +│ 🎯 SMART (4 commands) │ +│ 📚 DOCUMENTATION (13 commands) │ +│ 🔧 Code (12 commands) ............................ /craft:code:* +└─────────────────────────────────────────────────────────────┘ +``` diff --git a/tests/fixtures/prose-staleness/falsepos/hyphenated-compound-tldr.md b/tests/fixtures/prose-staleness/falsepos/hyphenated-compound-tldr.md new file mode 100644 index 000000000..90fc4bc2b --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/hyphenated-compound-tldr.md @@ -0,0 +1,11 @@ + + +# Architecture + +**TL;DR** — craft exposes 30 command-line entry points across 3 agent-facing surfaces, documented via 5 skill-authoring guides and 7 agents-only workflows. diff --git a/tests/fixtures/prose-staleness/falsepos/mode-limit-prose.md b/tests/fixtures/prose-staleness/falsepos/mode-limit-prose.md new file mode 100644 index 000000000..81a41af2f --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/mode-limit-prose.md @@ -0,0 +1,10 @@ + + + + + + +# Orchestration flag usage + +Default mode dispatches 2 agents max. Raising the cap to 4 agents is supported but rarely +useful; beyond that the coordination overhead dominates. diff --git a/tests/fixtures/prose-staleness/falsepos/release-date-prose-mention.md b/tests/fixtures/prose-staleness/falsepos/release-date-prose-mention.md new file mode 100644 index 000000000..97de1343e --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/release-date-prose-mention.md @@ -0,0 +1,12 @@ + + +# Upgrade Guide + +Upgrading to v4.5.0 is a drop-in change. + +**Released:** 2020-01-01 diff --git a/tests/fixtures/prose-staleness/falsepos/subset-bold-count.md b/tests/fixtures/prose-staleness/falsepos/subset-bold-count.md new file mode 100644 index 000000000..83e0a8b5b --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/subset-bold-count.md @@ -0,0 +1,10 @@ + + + + + +# The --refine flag + +`--refine` is declared on **9 commands** in two modes: default-on and opt-in. + +A stale install of another plugin once shipped **0 skills** because its tarball predated them. diff --git a/tests/fixtures/prose-staleness/falsepos/tldr-agent-subset-count.md b/tests/fixtures/prose-staleness/falsepos/tldr-agent-subset-count.md new file mode 100644 index 000000000..2b8141dd6 --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/tldr-agent-subset-count.md @@ -0,0 +1,10 @@ + + +# Migration Notes + +**TL;DR** — this patch only touches 1 agent config file; the other agent is untouched. diff --git a/tests/fixtures/prose-staleness/falsepos/tldr-mentioned-not-claimed.md b/tests/fixtures/prose-staleness/falsepos/tldr-mentioned-not-claimed.md new file mode 100644 index 000000000..33ac2930b --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/tldr-mentioned-not-claimed.md @@ -0,0 +1,13 @@ + + + + +# ADR-00N: some decision + +## Context + +| Bug | Why it was missed | +|---|---| +| `docs/skills-agents.md` — TL;DR claiming "8 specialized agents" above its own correct "2" | the intervening word broke the pattern | + +Prose about the bug, quoting 8 specialized agents again for good measure. diff --git a/tests/fixtures/prose-staleness/falsepos/unclosed-box-leaks-into-table.md b/tests/fixtures/prose-staleness/falsepos/unclosed-box-leaks-into-table.md new file mode 100644 index 000000000..d10302083 --- /dev/null +++ b/tests/fixtures/prose-staleness/falsepos/unclosed-box-leaks-into-table.md @@ -0,0 +1,18 @@ + + +# Notes + +```text +┌ untouched legacy snapshot, never finished +``` + +| Directory | Purpose | +|-----------|---------| +| `commands/` | 48 commands, once listed 3 agent placeholders here too | diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index c490c62ef..ea3a43932 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -3,7 +3,6 @@ Comprehensive tests to achieve 90%+ coverage for utils modules. This test file specifically targets coverage gaps identified in: -- utils/detect_teaching_mode.py (65% → 90%+) - utils/linkcheck_ignore_parser.py (71% → 90%+) Tests focus on: @@ -25,148 +24,6 @@ pytestmark = [pytest.mark.unit, pytest.mark.structure] -# ============================================================================ -# Test: detect_teaching_mode.py Coverage Gaps -# ============================================================================ - -class TestDetectTeachingModeCoverageGaps: - """Tests for detect_teaching_mode.py coverage gaps (targeting 90%+).""" - - def test_yaml_import_error_fallback(self, tmp_path): - """ - Test fallback behavior when YAML import fails. - - Coverage: Lines 33-34 (ImportError except block) - Scenario: YAML library not available, should fall back to text search - """ - from utils import detect_teaching_mode - - # Mock YAML_AVAILABLE = False - with patch('utils.detect_teaching_mode.YAML_AVAILABLE', False): - # Create _quarto.yml with teaching: true - quarto_file = tmp_path / "_quarto.yml" - quarto_file.write_text("teaching: true\nproject:\n type: website") - - is_teaching, method = detect_teaching_mode.detect_teaching_mode(str(tmp_path)) - - # Should still detect teaching mode via text search fallback - assert is_teaching is True - assert "metadata" in method.lower() - - def test_yaml_unavailable_text_search_fallback(self, tmp_path): - """ - Test text search fallback when YAML parsing is unavailable. - - Coverage: Lines 107-112 (fallback text search logic) - Scenario: YAML library missing, use simple text search - """ - from utils import detect_teaching_mode - - # Create _quarto.yml with teaching: true - quarto_file = tmp_path / "_quarto.yml" - quarto_file.write_text(""" -project: - type: website - teaching: true - title: "Course Website" -""") - - with patch('utils.detect_teaching_mode.YAML_AVAILABLE', False): - is_teaching, method = detect_teaching_mode.detect_teaching_mode(str(tmp_path)) - - assert is_teaching is True - assert method == "metadata" - - def test_yaml_unavailable_false_positive_prevention(self, tmp_path): - """ - Test that text search doesn't create false positives. - - Coverage: Lines 107-112 (fallback validation) - Scenario: File contains "teaching" but not "teaching: true" - """ - from utils import detect_teaching_mode - - # Create _quarto.yml mentioning teaching but not enabled - quarto_file = tmp_path / "_quarto.yml" - quarto_file.write_text(""" -# This is a teaching resource reference -# teaching: false -project: - type: website -""") - - with patch('utils.detect_teaching_mode.YAML_AVAILABLE', False): - is_teaching, method = detect_teaching_mode.detect_teaching_mode(str(tmp_path)) - - # Should not detect as teaching mode (teaching: false or missing) - assert is_teaching is False - - def test_yaml_fallback_exception_handling(self, tmp_path): - """ - Test exception handling in text search fallback. - - Coverage: Lines 111-112 (except Exception in fallback) - Scenario: File read error during fallback - """ - from utils import detect_teaching_mode - - quarto_file = tmp_path / "_quarto.yml" - quarto_file.write_text("teaching: true") - - with patch('utils.detect_teaching_mode.YAML_AVAILABLE', False): - # Mock file read to raise exception - with patch('pathlib.Path.read_text', side_effect=PermissionError("Access denied")): - # Should handle exception gracefully - # Fallback will fail, but shouldn't crash - is_teaching, method = detect_teaching_mode.detect_teaching_mode(str(tmp_path)) - - # Should fall through to False (no detection) - assert is_teaching is False - - def test_main_execution_current_directory(self, tmp_path, capsys, monkeypatch): - """ - Test __main__ block execution for current directory. - - Coverage: Lines 153-159 (main block, current directory test) - Scenario: Running module directly with no arguments - """ - from utils import detect_teaching_mode - - # Change to temp directory - monkeypatch.chdir(tmp_path) - - # Create teaching mode indicators - (tmp_path / "_quarto.yml").write_text("teaching: true") - - # Execute main block - with patch.object(sys, 'argv', ['detect_teaching_mode.py']): - detect_teaching_mode.main() if hasattr(detect_teaching_mode, 'main') else None - - captured = capsys.readouterr() - # Should print current directory results - # Note: main() may not exist, check exists first - - def test_main_execution_with_argument(self, tmp_path, capsys): - """ - Test __main__ block execution with command line argument. - - Coverage: Lines 162-167 (main block, test directory argument) - Scenario: Running module with path argument - """ - from utils import detect_teaching_mode - - # Create test directory with teaching mode - test_dir = tmp_path / "test_course" - test_dir.mkdir() - (test_dir / "_quarto.yml").write_text("teaching: true") - - # Execute with argument - with patch.object(sys, 'argv', ['detect_teaching_mode.py', str(test_dir)]): - # Only test if __name__ == "__main__" block can be executed - # In practice, this is hard to test directly without running the module - pass - - # ============================================================================ # Test: linkcheck_ignore_parser.py Coverage Gaps # ============================================================================ @@ -450,76 +307,6 @@ def test_main_execution_examples(self, capsys): assert "No changes" in preview3 or "reads" in preview3 -# ============================================================================ -# Integration: Cross-Module Coverage Tests -# ============================================================================ - -class TestCrossModuleCoverage: - """Integration tests to ensure all modules work together at 90%+ coverage.""" - - def test_teaching_mode_with_linkcheck_integration(self, tmp_path): - """ - Test teaching mode detection in combination with link checking. - - Scenario: Teaching mode project with expected broken links - """ - from utils.detect_teaching_mode import detect_teaching_mode - from utils.linkcheck_ignore_parser import parse_linkcheck_ignore - - # Create teaching mode project - (tmp_path / "_quarto.yml").write_text("teaching: true") - (tmp_path / "syllabus.qmd").write_text("# Syllabus") - (tmp_path / "schedule.qmd").write_text("# Schedule") - - # Create .linkcheck-ignore - ignore_file = tmp_path / ".linkcheck-ignore" - ignore_file.write_text(""" -### Teaching Files -File: syllabus.qmd -Target: ../resources/*.pdf -""") - - # Detect teaching mode - is_teaching, method = detect_teaching_mode(str(tmp_path)) - assert is_teaching is True - - # Parse ignore rules - rules = parse_linkcheck_ignore(str(ignore_file)) - should_ignore, category = rules.should_ignore("syllabus.qmd", "../resources/lecture1.pdf") - assert should_ignore is True - - def test_dry_run_with_teaching_mode_detection(self, tmp_path): - """ - Test dry-run output for teaching mode commands. - - Scenario: Preview teaching mode deployment - """ - from utils.dry_run_output import render_dry_run_preview, RiskLevel - from utils.detect_teaching_mode import detect_teaching_mode - - # Create teaching mode project - (tmp_path / "_quarto.yml").write_text("teaching: true") - - is_teaching, _ = detect_teaching_mode(str(tmp_path)) - assert is_teaching is True - - # Generate dry-run preview for teaching deployment - preview = render_dry_run_preview( - command_name="Deploy Teaching Site", - actions=[ - "✓ Build course website", - "✓ Validate syllabus dates", - "✓ Push to gh-pages branch" - ], - warnings=["Teaching mode detected: extra validations applied"], - summary="Safe to deploy (teaching mode)", - risk_level=RiskLevel.LOW - ) - - assert "Deploy Teaching Site" in preview - assert "Teaching mode" in preview or "teaching" in preview.lower() - - # ============================================================================ # Run Coverage Report # ============================================================================ diff --git a/tests/test_docs_staleness_prose.py b/tests/test_docs_staleness_prose.py new file mode 100644 index 000000000..b0e0b553d --- /dev/null +++ b/tests/test_docs_staleness_prose.py @@ -0,0 +1,564 @@ +"""Table-driven harness for the prose-staleness checks in docs-staleness-check.sh. + +Implements SPEC-doc-staleness-prose-gaps-2026-08-07's "Test Harness" section. + +Each row runs the real script against a throwaway repo containing exactly one +fixture document, and asserts on the structured ``count_consistency`` findings +from ``--json`` rather than parsing colored terminal output. + +Three fixture classes, and the harness is only meaningful with all three: + +* ``clean/`` — correct content must stay GREEN (no false positive) +* ``defect/`` — planted defect must go RED (positive control; a check shipped + without one of these is a rejected change) +* ``falsepos/``— content that LOOKS like a stale count but is not, drawn from + real pages an earlier build of these checks wrongly flagged + +Hermetic by construction: no network, no git-history walk, no TTY. Expected +counts are injected via ``CRAFT_EXPECTED_*``, so the fixtures do not have to +materialize 48 command files apiece. Check 1 (release-date consistency) has no +external authority (D1) -- it compares every release-date claim in the repo +against every other one -- so its fixtures need a peer claim to agree or +disagree with; see ``build_repo_multi`` and the ``release date`` tests below. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPT = REPO_ROOT / "scripts" / "docs-staleness-check.sh" +FIXTURES = Path(__file__).resolve().parent / "fixtures" / "prose-staleness" + +# The authority values every fixture is written against. Kept here, not derived +# from the live repo, so a future count change cannot silently invalidate a +# planted defect (e.g. an "8 agents" defect stops being a defect the day craft +# actually ships 8 agents). +FIXTURE_VERSION = "4.5.0" +FIXTURE_TAG_DATE = "2026-08-07" +FIXTURE_COUNTS = {"CMDS": "48", "SKILLS": "41", "AGENTS": "2"} + +# (fixture, destination path inside the throwaway repo, expect_finding, proves) +# +# Destination matters: `mode-limit-prose` must land on its real path because the +# exclusion that saves it in production is path-keyed. +CASES = [ + pytest.param( + "clean/version-box-correct.md", "docs/refcard.md", False, + "correct date + counts in a version box", + id="clean-version-box", + ), + pytest.param( + "clean/tldr-correct.md", "docs/skills-agents.md", False, + "correct counts in a TL;DR line", + id="clean-tldr", + ), + pytest.param( + "clean/structure-table-correct.md", "docs/structure.md", False, + "E1 stays fixed: correct singular count in a structure-table row", + id="clean-structure-table", + ), + pytest.param( + "clean/release-date-companion.md", "docs/news.md", False, + "D1 accepted cost: a lone release-date claim has no peer to compare " + "against, so it is vacuous, not verified", + id="clean-release-date-single-claim", + ), + pytest.param( + "defect/tldr-eight-agents.md", "docs/skills-agents.md", True, + "check 2 catches the original review bug", + id="defect-tldr-eight-agents", + ), + pytest.param( + "defect/structure-table-singular.md", "docs/structure.md", True, + "E1 would now be caught: singular noun form in a structure-table row", + id="defect-structure-table-singular", + ), + pytest.param( + "falsepos/category-subtotal-box.md", "docs/playground.md", False, + "category subtotals inside a box are not the grand total", + id="falsepos-category-subtotal", + ), + pytest.param( + "falsepos/subset-bold-count.md", "docs/help/refine-flag.md", False, + "a bolded count can describe a subset or another plugin", + id="falsepos-subset-bold-count", + ), + pytest.param( + "falsepos/mode-limit-prose.md", "docs/guide/orch-flag-usage.md", False, + "mode-limit prose is free prose and stays exclusion-covered", + id="falsepos-mode-limit-prose", + ), + pytest.param( + "falsepos/tldr-mentioned-not-claimed.md", "docs/adr/ADR-00N-example.md", False, + "a doc describing a TL;DR bug is not itself making a TL;DR claim", + id="falsepos-tldr-mentioned", + ), + pytest.param( + "falsepos/hyphenated-compound-tldr.md", "docs/architecture.md", False, + "F1/F2: a hyphenated compound (command-line, agent-facing) must never read as a count", + id="falsepos-hyphenated-compound", + ), + pytest.param( + "falsepos/tldr-agent-subset-count.md", "docs/migration.md", False, + "F6: a legitimate small subset count for the smallest count type must clear the floor", + id="falsepos-agent-subset-count", + ), + pytest.param( + "falsepos/unclosed-box-leaks-into-table.md", "docs/notes.md", False, + "F8: an unclosed version-box must not leak into a later structure-table row's own-type restriction", + id="falsepos-unclosed-box", + ), +] + + +def build_repo(tmp_path: Path, fixture: str, dest: str) -> Path: + """Materialize a throwaway repo holding exactly one fixture document.""" + repo = tmp_path / "repo" + (repo / ".claude-plugin").mkdir(parents=True) + (repo / ".claude-plugin" / "plugin.json").write_text( + json.dumps({"name": "craft", "version": FIXTURE_VERSION}) + "\n" + ) + target = repo / dest + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(FIXTURES / fixture, target) + return repo + + +def build_repo_multi(tmp_path: Path, files: list[tuple[str, str]]) -> Path: + """Materialize a throwaway repo holding several fixture documents at once. + + Check 1 (release-date consistency, D1) needs at least two real claims to + exercise at all -- a lone claim is vacuous by design -- so its tests pair + a fixture against `clean/release-date-companion.md` in one repo, unlike + every other check here, which is fully exercised by a single document. + """ + repo = tmp_path / "repo" + (repo / ".claude-plugin").mkdir(parents=True) + (repo / ".claude-plugin" / "plugin.json").write_text( + json.dumps({"name": "craft", "version": FIXTURE_VERSION}) + "\n" + ) + for fixture, dest in files: + target = repo / dest + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(FIXTURES / fixture, target) + return repo + + +def env_for(repo: Path) -> dict: + return { + "PATH": "/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin", + "HOME": str(repo), + "CRAFT_PLUGIN_DIR": str(repo), + "CRAFT_EXPECTED_CMDS": FIXTURE_COUNTS["CMDS"], + "CRAFT_EXPECTED_SKILLS": FIXTURE_COUNTS["SKILLS"], + "CRAFT_EXPECTED_AGENTS": FIXTURE_COUNTS["AGENTS"], + } + + +def run_check(repo: Path) -> dict: + """Run the real script against `repo` and return its parsed JSON report.""" + proc = subprocess.run( + ["bash", str(SCRIPT), "--json"], + env=env_for(repo), capture_output=True, text=True, timeout=120, + ) + assert proc.stdout.strip(), f"no JSON emitted; stderr:\n{proc.stderr}" + return json.loads(proc.stdout) + + +@pytest.mark.parametrize("fixture,dest,expect_finding,proves", CASES) +def test_prose_staleness_fixture(tmp_path, fixture, dest, expect_finding, proves): + report = run_check(build_repo(tmp_path, fixture, dest)) + findings = report["phases"]["count_consistency"]["findings"] + + rendered = "\n".join(f" {f['file']}: {f['message']}" for f in findings) or " (none)" + + if expect_finding: + assert findings, ( + f"planted defect went undetected — {proves}\n" + f"fixture: {fixture} -> {dest}\nfindings:\n{rendered}" + ) + else: + assert not findings, ( + f"false positive on content that is correct — {proves}\n" + f"fixture: {fixture} -> {dest}\nfindings:\n{rendered}" + ) + + +def _call_release_date_helpers(script_body: str) -> str: + """Source majority_date/compute_release_date_window/release_date_accepted + out of the real script and run `script_body` against them.""" + preamble = "\n".join( + f"eval \"$(sed -n '/^{fn}()/,/^}}/p' {SCRIPT})\"" + for fn in ("majority_date", "compute_release_date_window", "release_date_accepted") + ) + proc = subprocess.run( + ["bash", "-c", f"{preamble}\n{script_body}"], + capture_output=True, text=True, timeout=60, + ) + assert proc.returncode == 0, f"helper script failed:\n{proc.stderr}" + return proc.stdout + + +def test_broken_authority_window_is_vacuous_not_universal(): + """A release-date window that failed to compute must reject every claim, + not accept everything (F7-era regression, re-verified after D1). + + `compute_release_date_window` exits silently on an unparseable date, + leaving `ACCEPTED_RELEASE_DATES` empty. Nothing in the current design can + feed it anything but a real `YYYY-MM-DD` (`majority_date` only ever + returns one of the claim strings, all already validated by the regex that + collected them) -- this is defense in depth, not a live path -- but the + guard must still hold: an empty window matches nothing, so a single bad + input must not silently become "everything is accepted" instead. + """ + out = _call_release_date_helpers( + 'compute_release_date_window "not-a-date"\n' + 'release_date_accepted "2026-08-07" && echo ACCEPTED || echo REJECTED\n' + ) + assert out.strip() == "REJECTED", ( + f"a broken window accepted a claim instead of rejecting it: {out!r}" + ) + + +def test_majority_date_ties_break_to_the_later_date(): + """With claims tied 1-1 (the common case: one NEWS.md entry, one REFCARD.md + box), the later date must win -- a stale-release-date bug is a forgotten + update, so the wrong claim is normally older than the correct one, never + newer. Getting this backwards would make the check flag the correct claim + and accept the stale one. + """ + out = _call_release_date_helpers('majority_date "2026-07-19" "2026-08-07"\n') + assert out.strip() == "2026-08-07" + + out = _call_release_date_helpers('majority_date "2026-08-07" "2026-07-19"\n') + assert out.strip() == "2026-08-07", "order of arguments must not change the outcome" + + +def test_majority_date_prefers_the_larger_cluster_over_recency(): + """Two files agreeing beats one newer outlier -- majority by count comes + first; the later-date tie-break only applies when counts are equal.""" + out = _call_release_date_helpers( + 'majority_date "2026-08-07" "2026-08-07" "2026-09-01"\n' + ) + assert out.strip() == "2026-08-07" + + +def _call_apply_line_fix(path: Path, lineno: int, fix_detail: str) -> str: + """Source apply_line_fix out of the real script and call it directly.""" + script = ( + f"eval \"$(sed -n '/^apply_line_fix()/,/^}}/p' {SCRIPT})\"\n" + f"apply_line_fix {path} {lineno} {json.dumps(fix_detail)}\n" + ) + proc = subprocess.run(["bash", "-c", script], capture_output=True, text=True, timeout=60) + return proc.stdout.strip() + + +def test_apply_line_fix_edits_the_file_and_reports_truthfully(tmp_path): + """The shared applier must change the file when it says it did. + + pass 2's [f]ix branch printed "Fixed" and incremented TOTAL_FIXED without + touching anything — the same reports-success-changes-nothing bug pass 1 had + already been fixed for. Both now route through apply_line_fix, so this is + the one place that contract is pinned. + """ + doc = tmp_path / "structure.md" + doc.write_text("intro\n| `agents/` | 8 agent definitions |\noutro\n") + + assert _call_apply_line_fix(doc, 2, "s/8 agent/2 agent/") == "true" + assert doc.read_text().splitlines()[1] == "| `agents/` | 2 agent definitions |" + + +def test_apply_line_fix_returns_false_without_changing_anything(tmp_path): + """False must mean untouched, for every way a fix can fail to apply.""" + doc = tmp_path / "structure.md" + original = "intro\n| `agents/` | 2 agent definitions |\noutro\n" + doc.write_text(original) + + # Phase 8 emits this marker rather than a substitution — must not be run. + assert _call_apply_line_fix(doc, 2, "doc-coverage:refcard:craft:do") == "false" + # Pattern that matches nothing on that line. + assert _call_apply_line_fix(doc, 2, "s/9 agent/2 agent/") == "false" + # Line number past the end of the file. + assert _call_apply_line_fix(doc, 99, "s/2 agent/3 agent/") == "false" + + assert doc.read_text() == original, "a false result still modified the file" + + +def test_every_check_has_a_planted_defect(): + """A check without a defect fixture is a rejected change (SPEC harness rule). + + Guards the harness itself: it is the one assertion that fails if someone + adds a check and only ever tests the happy path. + """ + defects = {p.name for p in (FIXTURES / "defect").glob("*.md")} + assert "version-box-stale-date.md" in defects, "check 1 lost its positive control" + assert {"tldr-eight-agents.md", "structure-table-singular.md"} <= defects, ( + "check 2 lost a positive control" + ) + + +def _build_repo_with_own_scripts(tmp_path: Path, fixture: str, dest: str) -> tuple[Path, Path]: + """Like build_repo, but copies scripts/ alongside so EXCLUSIONS_FILE (which + is resolved relative to the running script's own directory) lands inside + the throwaway tree instead of this repo's real + scripts/config/exclusions.txt. Returns (repo, script_copy). + """ + repo = build_repo(tmp_path, fixture, dest) + scripts_copy = repo / "scripts" + shutil.copytree(REPO_ROOT / "scripts", scripts_copy) + return repo, scripts_copy / "docs-staleness-check.sh" + + +def _run_check(script: Path, repo: Path) -> dict: + env = { + "PATH": "/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin", + "HOME": str(repo), + "CRAFT_PLUGIN_DIR": str(repo), + "CRAFT_EXPECTED_CMDS": FIXTURE_COUNTS["CMDS"], + "CRAFT_EXPECTED_SKILLS": FIXTURE_COUNTS["SKILLS"], + "CRAFT_EXPECTED_AGENTS": FIXTURE_COUNTS["AGENTS"], + } + proc = subprocess.run( + ["bash", str(script), "--json"], env=env, capture_output=True, text=True, timeout=120, + ) + assert proc.stdout.strip(), f"no JSON emitted; stderr:\n{proc.stderr}" + return json.loads(proc.stdout) + + +def test_exclude_round_trips_end_to_end(tmp_path): + """[e]xclude must actually suppress the finding on the next run (F4/D4). + + Before D4, `file` was `path:lineno` glued into one field, so the entry + pass 2 wrote (`path:lineno:pattern`) could never match + `is_pattern_excluded`'s `path:pattern` parse — it printed "Excluded" and + the finding came back on the next run. + + Drives the REAL interactive `[e]` keystroke through a pty (pexpect), + not a reimplementation of pass 2's write logic in Python — a hand-copy + of the same bug pass 2 had would pass either way and prove nothing. + """ + pexpect = pytest.importorskip("pexpect") + + # structure-table-singular, not tldr-eight-agents: its noun ("8 agent") + # matches exactly what the check-time exclusion lookup passes + # (`${found} ${singular}`). tldr's noun ("8 specialized agents") does not + # — a separate, pre-existing exclusion-matching gap unrelated to D4 — and + # would make this test fail for a reason this phase does not fix. + repo, script = _build_repo_with_own_scripts( + tmp_path, "defect/structure-table-singular.md", "docs/structure.md" + ) + + before = _run_check(script, repo) + findings = before["phases"]["count_consistency"]["findings"] + assert findings, "fixture did not produce the finding this test excludes" + + env = { + "PATH": "/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin", + "HOME": str(repo), + "CRAFT_PLUGIN_DIR": str(repo), + "CRAFT_EXPECTED_CMDS": FIXTURE_COUNTS["CMDS"], + "CRAFT_EXPECTED_SKILLS": FIXTURE_COUNTS["SKILLS"], + "CRAFT_EXPECTED_AGENTS": FIXTURE_COUNTS["AGENTS"], + } + child = pexpect.spawn("bash", [str(script), "--fix"], env=env, cwd=str(repo), timeout=30) + try: + child.expect_exact("[f]ix [s]kip [e]xclude permanently:") + child.sendline("e") + child.expect_exact("Excluded") + child.expect(pexpect.EOF) + finally: + child.close() + + exclusions_file = script.parent / "config" / "exclusions.txt" + assert exclusions_file.exists(), "no exclusions.txt was written" + written = exclusions_file.read_text() + assert "docs/structure.md:8 agent" in written, ( + f"exclusions.txt does not carry the expected pattern:\n{written}" + ) + + after = _run_check(script, repo) + after_findings = after["phases"]["count_consistency"]["findings"] + assert not after_findings, ( + f"[e]xclude did not suppress the finding on the next run — " + f"round-trip broken. exclusions.txt:\n{written}\nfindings:\n{after_findings}" + ) + + +def test_fix_preserves_surrounding_prose(tmp_path): + """[f]ix on a real shape-scoped finding must swap only the digits (F2/D3). + + Before D3, the fix payload was built from a second, un-anchored regex + ("N noun" with no trailer) rather than the exact span the detection regex + matched -- on a hyphenated compound that would have rewritten "30 + command-line" into "48 command-line" (the two regexes disagreed on where + the match ended). Drives the real interactive `[f]` keystroke through a + pty and asserts the rest of the line is untouched. + """ + pexpect = pytest.importorskip("pexpect") + + repo, script = _build_repo_with_own_scripts( + tmp_path, "defect/structure-table-singular.md", "docs/structure.md" + ) + target = repo / "docs" / "structure.md" + + env = { + "PATH": "/usr/bin:/bin:/usr/local/bin:/opt/homebrew/bin", + "HOME": str(repo), + "CRAFT_PLUGIN_DIR": str(repo), + "CRAFT_EXPECTED_CMDS": FIXTURE_COUNTS["CMDS"], + "CRAFT_EXPECTED_SKILLS": FIXTURE_COUNTS["SKILLS"], + "CRAFT_EXPECTED_AGENTS": FIXTURE_COUNTS["AGENTS"], + } + child = pexpect.spawn("bash", [str(script), "--fix"], env=env, cwd=str(repo), timeout=30) + try: + child.expect_exact("[f]ix [s]kip [e]xclude permanently:") + child.sendline("f") + child.expect_exact("Fixed") + child.expect(pexpect.EOF) + finally: + child.close() + + lines = target.read_text().splitlines() + assert "| `agents/` | 2 agent definitions |" in lines, ( + f"fix did not land cleanly -- surrounding prose corrupted:\n{target.read_text()}" + ) + + +def test_release_date_agrees_across_files(tmp_path): + """Two independently-correct claims one day apart (the real v4.5.0 case: + NEWS.md says 2026-08-08, REFCARD-style boxes say 2026-08-07) must not + disagree with each other (E2, re-verified after D1's redesign).""" + repo = build_repo_multi(tmp_path, [ + ("clean/release-date-companion.md", "docs/news.md"), + ("clean/release-date-utc-boundary.md", "docs/other-news.md"), + ]) + report = run_check(repo) + findings = report["phases"]["count_consistency"]["findings"] + assert not findings, f"agreeing claims wrongly flagged:\n{findings}" + + +def test_release_date_disagreement_flags_the_stale_claim(tmp_path): + """A release date three weeks off must be caught once it has a peer to + disagree with -- this is the original REFCARD.md bug the parent SPEC + exists for, re-verified under D1's cross-file design.""" + repo = build_repo_multi(tmp_path, [ + ("clean/release-date-companion.md", "docs/news.md"), + ("defect/version-box-stale-date.md", "docs/refcard.md"), + ]) + report = run_check(repo) + findings = report["phases"]["count_consistency"]["findings"] + assert findings, "planted defect went undetected once it had a peer to disagree with" + assert any(f["file"].startswith("docs/refcard.md") for f in findings), ( + f"finding landed on the wrong file:\n{findings}" + ) + assert not any(f["file"].startswith("docs/news.md") for f in findings), ( + f"the correct companion claim was flagged instead of the stale one:\n{findings}" + ) + + +def test_release_date_far_edge_still_caught_cross_file(tmp_path): + """The window's off-by-one guard (win=5, not 4) must still hold once check + 1 compares claims to each other instead of to a tag.""" + repo = build_repo_multi(tmp_path, [ + ("clean/release-date-companion.md", "docs/news.md"), + ("defect/release-date-far-edge.md", "docs/news-old.md"), + ]) + report = run_check(repo) + findings = report["phases"]["count_consistency"]["findings"] + assert findings, "the far-edge claim was not collected -- window off-by-one regressed" + assert any(f["file"].startswith("docs/news-old.md") for f in findings), ( + f"finding landed on the wrong file:\n{findings}" + ) + + +def test_release_date_prose_mention_does_not_open_window(tmp_path): + """A version mention in running prose must not open the claim window + (D6/F3) -- paired against the companion so a wrongly-collected claim would + have a peer to disagree with and surface as a finding.""" + repo = build_repo_multi(tmp_path, [ + ("clean/release-date-companion.md", "docs/news.md"), + ("falsepos/release-date-prose-mention.md", "docs/upgrade-guide.md"), + ]) + report = run_check(repo) + findings = report["phases"]["count_consistency"]["findings"] + assert not findings, ( + f"prose mention of the version opened the claim window (D6 regressed):\n{findings}" + ) + + +def test_check1_promoted_check2_stays_warning(tmp_path): + """D11: check 1 was *designed* to block (D2) but shipped `warning` until + it earned `error` via the Phase 6 evidence gate -- a clean live-repo run + across every tracked doc and both real claim sites (docs/NEWS.md, + docs/REFCARD.md), transcript quoted, not a passing unit suite alone (see + `docs-staleness-check.sh`'s check-1 comment for the quoted transcript). + That gate is now met, so check 1 emits `error`. Check 2's severity was + never part of D11 and stays `warning`. + """ + repo = build_repo_multi(tmp_path, [ + ("clean/release-date-companion.md", "docs/news.md"), + ("defect/version-box-stale-date.md", "docs/refcard.md"), + ]) + findings = run_check(repo)["phases"]["count_consistency"]["findings"] + date_findings = [f for f in findings if "release date" in f["message"]] + assert date_findings, f"expected a release-date finding:\n{findings}" + assert all(f["severity"] == "error" for f in date_findings), ( + f"check 1 not promoted to error post-D11-gate:\n{date_findings}" + ) + + repo2 = build_repo(tmp_path / "case2", "defect/tldr-eight-agents.md", "docs/skills-agents.md") + findings2 = run_check(repo2)["phases"]["count_consistency"]["findings"] + assert findings2, f"expected a count-prose finding:\n{findings2}" + assert all(f["severity"] == "warning" for f in findings2), ( + f"check 2 finding is not severity warning:\n{findings2}" + ) + + +def test_phase_status_goes_red_on_any_error_finding(): + """print_phase_status / phase_status_label must coexist with mixed + severities inside one phase (Phase 4's own acceptance bullet) -- a phase + with one `error` finding among any number of `warning` findings is RED, + and an all-`warning` phase is YELLOW. Tested directly against + phase_status_label rather than through a live check, so it stays + independent of which checks currently emit `error` (check 1, post-D11) + vs `warning` (check 2) -- this pins the mechanism itself, not today's + severity assignment. + """ + preamble = f"eval \"$(sed -n '/^phase_status_label()/,/^}}/p' {SCRIPT})\"" + + mixed = 'ARR=("error|a.md||msg|false|" "warning|b.md||msg2|false|")' + proc = subprocess.run( + ["bash", "-c", f"{preamble}\n{mixed}\nphase_status_label ARR\n"], + capture_output=True, text=True, timeout=30, + ) + assert proc.stdout.strip() == "RED", f"mixed severity did not go RED: {proc.stdout!r}" + + warnings_only = 'ARR=("warning|a.md||msg|false|" "warning|b.md||msg2|false|")' + proc = subprocess.run( + ["bash", "-c", f"{preamble}\n{warnings_only}\nphase_status_label ARR\n"], + capture_output=True, text=True, timeout=30, + ) + assert proc.stdout.strip() == "YELLOW", f"warning-only phase was not YELLOW: {proc.stdout!r}" + + +def test_live_repo_stays_green_on_count_consistency(): + """The checks must not fire on craft's own already-corrected docs. + + SPEC acceptance criterion: running against current HEAD stays GREEN. This is + the assertion that would have caught the first build of check 2, which + flagged five legitimate subset counts before the 40%-of-expected floor. + """ + proc = subprocess.run( + ["bash", str(SCRIPT), "--json"], + cwd=REPO_ROOT, capture_output=True, text=True, timeout=180, + ) + report = json.loads(proc.stdout) + findings = report["phases"]["count_consistency"]["findings"] + rendered = "\n".join(f" {f['file']}: {f['message']}" for f in findings) + assert not findings, f"count_consistency regressed on the live repo:\n{rendered}" diff --git a/tests/test_integration_teaching_workflow.py b/tests/test_integration_teaching_workflow.py deleted file mode 100644 index 791cd7a98..000000000 --- a/tests/test_integration_teaching_workflow.py +++ /dev/null @@ -1,353 +0,0 @@ -#!/usr/bin/env python3 -""" -Integration Tests: Teaching Workflow -===================================== -Tests the full teaching workflow end-to-end including detection, -validation, and publish cycle. - -Components tested: -- commands/utils/detect_teaching_mode.py -- commands/utils/teaching_validation.py -- commands/utils/teach_config.py -- commands/site/publish.md -- commands/site/progress.md -- commands/site/build.md - -Run with: python tests/test_integration_teaching_workflow.py -""" - -import unittest -import tempfile -import yaml -from pathlib import Path -from datetime import date, timedelta -import sys - -import pytest - -# Add plugin directory to path -plugin_dir = Path(__file__).parent.parent -utils_dir = plugin_dir / "commands" / "utils" -sys.path.insert(0, str(plugin_dir)) -sys.path.insert(0, str(utils_dir)) - -# Try to import teaching modules -try: - from commands.utils.detect_teaching_mode import detect_teaching_mode - DETECT_AVAILABLE = True -except ImportError: - DETECT_AVAILABLE = False - -try: - from commands.utils.teach_config import load_teach_config, _normalize_config - CONFIG_AVAILABLE = True -except ImportError: - CONFIG_AVAILABLE = False - -pytestmark = [pytest.mark.integration, pytest.mark.teaching] - - -class TestTeachingWorkflowIntegration(unittest.TestCase): - """Integration tests for teaching workflow.""" - - def setUp(self): - """Create temporary teaching project.""" - self.temp_dir = tempfile.mkdtemp() - self.project_dir = Path(self.temp_dir) - - def tearDown(self): - """Clean up temporary files.""" - import shutil - shutil.rmtree(self.temp_dir, ignore_errors=True) - - @unittest.skipIf(not DETECT_AVAILABLE, "Teaching detection module not available") - def test_01_detection_with_config(self): - """Test: Detects teaching mode from .flow/teach-config.yml.""" - # Arrange: Create teach-config.yml - config_dir = self.project_dir / ".flow" - config_dir.mkdir(parents=True) - config_file = config_dir / "teach-config.yml" - - config_content = { - "course": { - "number": "STAT 545", - "title": "Regression Analysis", - "semester": "Spring 2026", - "year": 2026 - } - } - config_file.write_text(yaml.dump(config_content)) - - # Act - is_teaching, method = detect_teaching_mode(str(self.project_dir)) - - # Assert - self.assertTrue(is_teaching, "Should detect teaching mode") - self.assertEqual(method, "teach-config.yml", f"Should detect via config file, got: {method}") - - @unittest.skipIf(not DETECT_AVAILABLE, "Teaching detection module not available") - def test_02_detection_without_config(self): - """Test: Returns False when no teaching indicators present.""" - # Act - is_teaching, method = detect_teaching_mode(str(self.project_dir)) - - # Assert - self.assertFalse(is_teaching, "Should not detect teaching mode in empty project") - self.assertIsNone(method, "Method should be None when not detected") - - @unittest.skipIf(not CONFIG_AVAILABLE, "Teaching config module not available") - def test_03_config_parsing(self): - """Test: Parses teach-config.yml correctly.""" - # Arrange - config_dir = self.project_dir / ".flow" - config_dir.mkdir(parents=True) - config_file = config_dir / "teach-config.yml" - - start_date = date.today() - end_date = start_date + timedelta(days=105) # 15 weeks - - config_content = { - "course": {"number": "TEST 101", "title": "Test Course", - "semester": "Spring", "year": 2026}, - "dates": { - "start": start_date.isoformat(), - "end": end_date.isoformat() - } - } - config_file.write_text(yaml.dump(config_content)) - - # Act - config = load_teach_config(str(self.project_dir)) - - # Assert - self.assertIsNotNone(config, "Should successfully parse config") - self.assertEqual(config["course"]["number"], "TEST 101") - self.assertEqual(config["course"]["title"], "Test Course") - - def test_04_teaching_commands_exist(self): - """Test: Teaching-related site logic is preserved somewhere. - - publish.md/progress.md/build.md were KILLED (not moved) in the folio - split (Phase 3, 2026-07-12) — their unique logic was salvage-merged - into folio's site-management skill references per ADR-002, not kept - as standalone command files in either repo. This checks the salvage - landed, rather than asserting command files that no longer exist - anywhere.""" - # Arrange - folio_refs_dir = plugin_dir.parent / "folio" / "skills" / "docs" / "site-management" / "references" - - expected_refs = ["publish.md", "build.md"] - - if not folio_refs_dir.exists(): - self.skipTest("folio repo not present alongside craft") - - for ref in expected_refs: - with self.subTest(reference=ref): - # Assert - ref_path = folio_refs_dir / ref - self.assertTrue(ref_path.exists(), f"Salvaged reference {ref} should exist") - - def test_05_teaching_utilities_exist(self): - """Test: Teaching utility modules exist.""" - # Arrange - utilities are split across two directories - expected_utils = [ - ("utils", "detect_teaching_mode.py"), - ("commands/utils", "teach_config.py"), - ("commands/utils", "teaching_validation.py") - ] - - for directory, util in expected_utils: - with self.subTest(utility=util): - # Assert - util_path = plugin_dir / directory / util - self.assertTrue(util_path.exists(), f"Utility {util} should exist in {directory}") - - def test_06_config_structure_validation(self): - """Test: Config file validates required structure.""" - # Arrange: Create minimal valid config - config_dir = self.project_dir / ".flow" - config_dir.mkdir(parents=True) - config_file = config_dir / "teach-config.yml" - - # Minimal config - config_content = { - "course": { - "number": "TEST 101", - "title": "Test Course", - "semester": "Spring 2026", - "year": 2026 - }, - "dates": { - "start": "2026-01-20", - "end": "2026-05-15" - } - } - config_file.write_text(yaml.dump(config_content)) - - # Act: Parse config - content = yaml.safe_load(config_file.read_text()) - - # Assert: Required fields present - self.assertIn("course", content, "Should have course section") - self.assertIn("dates", content, "Should have dates section") - self.assertIn("number", content["course"], "Course should have number") - self.assertIn("title", content["course"], "Course should have title") - - -class TestConfigNormalization(unittest.TestCase): - """Tests for flow-cli -> craft schema normalization. - - All fixtures are hardcoded (self-contained, no dependency on stat-545 repo). - """ - - def setUp(self): - self.temp_dir = tempfile.mkdtemp() - self.project_dir = Path(self.temp_dir) - - def tearDown(self): - import shutil - shutil.rmtree(self.temp_dir, ignore_errors=True) - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_flowcli_schema_loads(self): - """Flow-cli schema (semester_info) loads through full pipeline.""" - config_dir = self.project_dir / ".flow" - config_dir.mkdir(parents=True) - (config_dir / "teach-config.yml").write_text(yaml.dump({ - "course": {"name": "STAT 545", "full_name": "STAT 545 - ANOVA", - "semester": "spring", "year": 2026}, - "semester_info": { - "start_date": "2026-01-19", "end_date": "2026-05-16", - "breaks": [ - {"name": "MLK Day", "start": "2026-01-20", "end": "2026-01-20"}, - {"name": "Spring Break", "start": "2026-03-15", "end": "2026-03-22"} - ] - } - })) - config = load_teach_config(str(self.project_dir)) - self.assertIsNotNone(config) - self.assertEqual(config["course"]["number"], "STAT 545") - self.assertEqual(config["course"]["title"], "STAT 545 - ANOVA") - self.assertEqual(config["dates"]["start"], "2026-01-19") - self.assertEqual(config["course"]["semester"], "Spring") - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_craft_native_unchanged(self): - """Craft-native schema (dates.*) still works as before.""" - config_dir = self.project_dir / ".flow" - config_dir.mkdir(parents=True) - (config_dir / "teach-config.yml").write_text(yaml.dump({ - "course": {"number": "TEST 101", "title": "Test Course", - "semester": "Spring", "year": 2026}, - "dates": {"start": "2026-01-20", "end": "2026-05-15"} - })) - config = load_teach_config(str(self.project_dir)) - self.assertIsNotNone(config) - self.assertEqual(config["course"]["number"], "TEST 101") - self.assertEqual(config["dates"]["start"], "2026-01-20") - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_mixed_schema_merge(self): - """Both dates and semester_info present -> dates wins, gaps filled.""" - config = { - "course": {"number": "TEST 101", "title": "Test", - "semester": "Spring", "year": 2026}, - "dates": {"start": "2026-01-20"}, # has start, missing end - "semester_info": { - "start_date": "2026-01-19", # should NOT overwrite dates.start - "end_date": "2026-05-16", # should fill dates.end - } - } - result = _normalize_config(config) - self.assertEqual(result["dates"]["start"], "2026-01-20") # craft-native wins - self.assertEqual(result["dates"]["end"], "2026-05-16") # gap filled - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_semester_capitalization(self): - """Lowercase 'spring' -> 'Spring'.""" - config = {"course": {"semester": "spring"}} - result = _normalize_config(config) - self.assertEqual(result["course"]["semester"], "Spring") - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_name_to_number_mapping(self): - """course.name -> course.number when number absent.""" - config = {"course": {"name": "STAT 545"}} - result = _normalize_config(config) - self.assertEqual(result["course"]["number"], "STAT 545") - self.assertIn("name", result["course"]) # original key preserved - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_full_name_to_title(self): - """course.full_name -> course.title when title absent.""" - config = {"course": {"full_name": "Analysis of Variance"}} - result = _normalize_config(config) - self.assertEqual(result["course"]["title"], "Analysis of Variance") - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_single_day_break_valid(self): - """Single-day break (start == end) accepted by validation.""" - config_dir = self.project_dir / ".flow" - config_dir.mkdir(parents=True) - (config_dir / "teach-config.yml").write_text(yaml.dump({ - "course": {"number": "TEST 101", "title": "Test", - "semester": "Spring", "year": 2026}, - "dates": { - "start": "2026-01-19", "end": "2026-05-16", - "breaks": [{"name": "MLK Day", - "start": "2026-01-20", "end": "2026-01-20"}] - } - })) - config = load_teach_config(str(self.project_dir)) - self.assertIsNotNone(config, "Single-day break should not cause validation error") - - @unittest.skipIf(not CONFIG_AVAILABLE, "Config module not available") - def test_branches_to_deployment(self): - """branches.production -> deployment.production_branch.""" - config = {"branches": {"production": "main", "draft": "staging"}} - result = _normalize_config(config) - self.assertEqual(result["deployment"]["production_branch"], "main") - self.assertEqual(result["deployment"]["draft_branch"], "staging") - - -class TestTeachingEdgeCases(unittest.TestCase): - """Edge case tests for teaching workflow.""" - - def test_invalid_yaml_handling(self): - """Test: System handles invalid YAML gracefully.""" - # Arrange - with tempfile.TemporaryDirectory() as tmpdir: - config_dir = Path(tmpdir) / ".flow" - config_dir.mkdir() - config_file = config_dir / "teach-config.yml" - - # Write invalid YAML - config_file.write_text("invalid: yaml: syntax: error:") - - # Act & Assert: Should not crash - try: - content = yaml.safe_load(config_file.read_text()) - except yaml.YAMLError: - pass # Expected behavior - - def test_missing_required_fields(self): - """Test: Detects missing required config fields.""" - # Arrange - with tempfile.TemporaryDirectory() as tmpdir: - config_dir = Path(tmpdir) / ".flow" - config_dir.mkdir() - config_file = config_dir / "teach-config.yml" - - # Incomplete config - config_content = {"course": {"number": "TEST 101"}} # Missing title - config_file.write_text(yaml.dump(config_content)) - - # Act - content = yaml.safe_load(config_file.read_text()) - - # Assert: Can detect missing fields - self.assertNotIn("title", content["course"], "Title should be missing") - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/tests/test_semester_progress.py b/tests/test_semester_progress.py deleted file mode 100644 index 3ba36e5a3..000000000 --- a/tests/test_semester_progress.py +++ /dev/null @@ -1,504 +0,0 @@ -""" -Unit tests for semester progress calculation. - -Tests week calculation, break handling, edge cases, and date formatting. - -Run with: - python3 -m pytest tests/test_semester_progress.py -v - python3 -m pytest tests/test_semester_progress.py::TestSemesterProgress::test_week_calculation_no_breaks -v -""" - -import sys -from pathlib import Path - -import pytest - -# Add project root to path -project_root = Path(__file__).parent.parent -sys.path.insert(0, str(project_root)) - -pytestmark = [pytest.mark.unit, pytest.mark.teaching] - -from commands.utils.semester_progress import ( - calculate_current_week, - count_break_days, - format_date_range, - get_week_boundaries, - is_on_break, -) - - -class TestSemesterProgress: - """Test semester progress calculations.""" - - def test_week_calculation_no_breaks(self): - """Test basic week calculation without breaks.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - } - } - - # Week 1 (start date) - result = calculate_current_week(config, "2026-01-19") - assert result["current_week"] == 1 - assert result["total_weeks"] == 16 - assert result["on_break"] is False - assert result["break_name"] is None - assert result["percent_complete"] < 10 - - # Week 4 (3 weeks after start) - result = calculate_current_week(config, "2026-02-09") - assert result["current_week"] == 4 - assert result["on_break"] is False - - # Week 9 (March 16 = 56 days = 8 full weeks) - result = calculate_current_week(config, "2026-03-16") - assert result["current_week"] == 9 - assert 50 < result["percent_complete"] < 60 - - # Final week - result = calculate_current_week(config, "2026-05-04") - assert result["current_week"] == 16 - assert result["percent_complete"] > 90 - - def test_week_calculation_with_breaks(self): - """Test week calculation with spring break.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - "breaks": [ - { - "name": "Spring Break", - "start": "2026-03-16", - "end": "2026-03-20" - } - ] - } - } - - # Before break (Mar 15 = 55 days = 7 full weeks, so week 8) - result = calculate_current_week(config, "2026-03-15") - assert result["current_week"] == 8 - assert result["on_break"] is False - - # During break (Week 9 - break days don't advance week) - result = calculate_current_week(config, "2026-03-18") - assert result["on_break"] is True - assert result["break_name"] == "Spring Break" - assert result["current_week"] == 9 - - # After break (Week 9 - 63 total days - 5 break days = 58 days = week 9) - result = calculate_current_week(config, "2026-03-23") - assert result["current_week"] == 9 - assert result["on_break"] is False - - def test_multiple_breaks(self): - """Test semester with multiple breaks.""" - config = { - "dates": { - "start": "2025-09-02", - "end": "2025-12-15", - "breaks": [ - { - "name": "Fall Break", - "start": "2025-10-12", - "end": "2025-10-13" - }, - { - "name": "Thanksgiving", - "start": "2025-11-25", - "end": "2025-11-29" - } - ] - } - } - - # Before first break - result = calculate_current_week(config, "2025-10-10") - assert result["on_break"] is False - - # During first break - result = calculate_current_week(config, "2025-10-12") - assert result["on_break"] is True - assert result["break_name"] == "Fall Break" - - # Between breaks - result = calculate_current_week(config, "2025-11-15") - assert result["on_break"] is False - - # During second break - result = calculate_current_week(config, "2025-11-27") - assert result["on_break"] is True - assert result["break_name"] == "Thanksgiving" - - # After all breaks - result = calculate_current_week(config, "2025-12-10") - assert result["on_break"] is False - - def test_on_break_detection(self): - """Test is_on_break function.""" - breaks = [ - {"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"}, - {"name": "Reading Week", "start": "2026-04-13", "end": "2026-04-14"} - ] - - # Not on break - on_break, name = is_on_break("2026-02-10", breaks) - assert on_break is False - assert name is None - - # On Spring Break (first day) - on_break, name = is_on_break("2026-03-16", breaks) - assert on_break is True - assert name == "Spring Break" - - # On Spring Break (middle) - on_break, name = is_on_break("2026-03-18", breaks) - assert on_break is True - assert name == "Spring Break" - - # On Spring Break (last day) - on_break, name = is_on_break("2026-03-20", breaks) - assert on_break is True - assert name == "Spring Break" - - # Between breaks - on_break, name = is_on_break("2026-04-01", breaks) - assert on_break is False - assert name is None - - # On Reading Week - on_break, name = is_on_break("2026-04-13", breaks) - assert on_break is True - assert name == "Reading Week" - - def test_before_semester(self): - """Test date before semester starts.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - } - } - - result = calculate_current_week(config, "2025-12-15") - assert result["current_week"] == 0 - assert result["percent_complete"] == 0.0 - assert result["on_break"] is False - assert result["days_elapsed"] == 0 - assert result["days_remaining"] > 0 - - def test_after_semester(self): - """Test date after semester ends.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - } - } - - result = calculate_current_week(config, "2026-06-01") - assert result["current_week"] == result["total_weeks"] - assert result["percent_complete"] == 100.0 - assert result["on_break"] is False - assert result["days_remaining"] == 0 - - def test_summer_session_no_breaks(self): - """Test compressed summer session without breaks.""" - config = { - "dates": { - "start": "2026-06-01", - "end": "2026-07-24" - } - } - - # Week 1 - result = calculate_current_week(config, "2026-06-01") - assert result["current_week"] == 1 - assert result["total_weeks"] == 8 - - # Week 4 (21 days = 3 full weeks, so week 4) - result = calculate_current_week(config, "2026-06-22") - assert result["current_week"] == 4 - # 21 days out of 53 total = ~40% - assert 38 < result["percent_complete"] < 42 - - # Final week - result = calculate_current_week(config, "2026-07-20") - assert result["current_week"] == 8 - - def test_manual_override(self): - """Test manual week override in config.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - }, - "progress": { - "current_week": 12 # Manual override - } - } - - # Should use manual week regardless of date - result = calculate_current_week(config, "2026-02-01") - assert result["current_week"] == 12 - assert result["total_weeks"] == 16 - # Percent should reflect manual week - assert 70 < result["percent_complete"] < 80 - - def test_week_boundaries(self): - """Test week boundary calculation.""" - # No breaks - start, end = get_week_boundaries(1, "2026-01-19", []) - assert start == "2026-01-19" - assert end == "2026-01-25" - - start, end = get_week_boundaries(2, "2026-01-19", []) - assert start == "2026-01-26" - assert end == "2026-02-01" - - # With break (week 9 should skip over spring break) - breaks = [{"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"}] - start, end = get_week_boundaries(9, "2026-01-19", breaks) - # Week 9 starts after break ends - assert start > "2026-03-20" - - def test_count_break_days(self): - """Test counting break days in a range.""" - breaks = [ - {"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"} - ] - - # Entire semester (5-day break) - days = count_break_days("2026-01-01", "2026-12-31", breaks) - assert days == 5 - - # Range containing break - days = count_break_days("2026-03-10", "2026-03-25", breaks) - assert days == 5 - - # Range before break - days = count_break_days("2026-01-01", "2026-03-01", breaks) - assert days == 0 - - # Range after break - days = count_break_days("2026-04-01", "2026-05-01", breaks) - assert days == 0 - - # Partial overlap (last 2 days of break) - days = count_break_days("2026-03-19", "2026-03-25", breaks) - assert days == 2 - - def test_date_formatting(self): - """Test ADHD-friendly date formatting.""" - # Same month - formatted = format_date_range("2026-01-27", "2026-01-30") - assert formatted == "Jan 27-30" - - # Different months - formatted = format_date_range("2026-01-27", "2026-02-02") - assert formatted == "Jan 27 - Feb 2" - - # Spring break example - formatted = format_date_range("2026-03-16", "2026-03-20") - assert formatted == "Mar 16-20" - - def test_first_week_edge_case(self): - """Test first week of semester.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - } - } - - # First day - result = calculate_current_week(config, "2026-01-19") - assert result["current_week"] == 1 - assert result["week_start"] == "2026-01-19" - assert result["days_elapsed"] == 0 - - # Second day (still week 1) - result = calculate_current_week(config, "2026-01-20") - assert result["current_week"] == 1 - assert result["days_elapsed"] == 1 - - def test_last_week_edge_case(self): - """Test last week of semester.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - } - } - - # Last day - result = calculate_current_week(config, "2026-05-08") - assert result["current_week"] == result["total_weeks"] - assert result["days_remaining"] == 0 - assert result["percent_complete"] == 100.0 - - # Day before last - result = calculate_current_week(config, "2026-05-07") - assert result["current_week"] == result["total_weeks"] - assert result["days_remaining"] == 1 - - def test_days_elapsed_and_remaining(self): - """Test days_elapsed and days_remaining calculations.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-02-09", # 3 weeks - "breaks": [ - {"name": "Break", "start": "2026-01-26", "end": "2026-01-28"} # 3 days - ] - } - } - - # Before break (7 days elapsed) - result = calculate_current_week(config, "2026-01-25") - assert result["days_elapsed"] == 6 - - # During break (7 days elapsed, frozen) - result = calculate_current_week(config, "2026-01-27") - assert result["on_break"] is True - assert result["days_elapsed"] == 7 # Doesn't count break days - - # After break - result = calculate_current_week(config, "2026-01-30") - assert result["days_elapsed"] == 8 # 7 before + 1 after (skipped 3 break days) - - def test_return_structure_completeness(self): - """Test that return dictionary has all required fields.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - } - } - - result = calculate_current_week(config, "2026-02-10") - - # Check all required fields exist - required_fields = { - "current_week", - "total_weeks", - "percent_complete", - "on_break", - "break_name", - "days_elapsed", - "days_remaining", - "semester_start", - "semester_end", - "week_start", - "week_end" - } - - assert set(result.keys()) == required_fields - - # Check types - assert isinstance(result["current_week"], int) - assert isinstance(result["total_weeks"], int) - assert isinstance(result["percent_complete"], (int, float)) - assert isinstance(result["on_break"], bool) - assert result["break_name"] is None or isinstance(result["break_name"], str) - assert isinstance(result["days_elapsed"], int) - assert isinstance(result["days_remaining"], int) - assert isinstance(result["semester_start"], str) - assert isinstance(result["semester_end"], str) - assert isinstance(result["week_start"], str) - assert isinstance(result["week_end"], str) - - def test_performance(self): - """Test calculation performance (< 100ms target).""" - import time - - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - "breaks": [ - {"name": "Spring Break", "start": "2026-03-16", "end": "2026-03-20"}, - {"name": "Reading Week", "start": "2026-04-13", "end": "2026-04-14"} - ] - } - } - - # Run 100 calculations - start_time = time.time() - for _ in range(100): - calculate_current_week(config, "2026-03-10") - elapsed = time.time() - start_time - - # Should complete 100 calculations in < 100ms (avg < 1ms each) - assert elapsed < 0.1, f"Performance regression: {elapsed*10:.2f}ms per call" - - def test_break_on_semester_boundary(self): - """Test break that starts/ends on semester boundaries.""" - config = { - "dates": { - "start": "2026-01-19", - "end": "2026-03-01", - "breaks": [ - # Break starts on semester start - {"name": "Orientation", "start": "2026-01-19", "end": "2026-01-20"} - ] - } - } - - result = calculate_current_week(config, "2026-01-19") - assert result["on_break"] is True - assert result["break_name"] == "Orientation" - - def test_auto_vs_manual_override(self): - """Test that manual override takes precedence over auto calculation.""" - config_auto = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - }, - "progress": { - "current_week": "auto" - } - } - - config_manual = { - "dates": { - "start": "2026-01-19", - "end": "2026-05-08" - }, - "progress": { - "current_week": 15 # Manual override - } - } - - # Auto should calculate based on date - result_auto = calculate_current_week(config_auto, "2026-02-10") - assert result_auto["current_week"] == 4 - - # Manual should use override - result_manual = calculate_current_week(config_manual, "2026-02-10") - assert result_manual["current_week"] == 15 - - def test_default_current_date(self): - """Test using default current date (today).""" - config = { - "dates": { - "start": "2020-01-01", # Past date - "end": "2030-12-31" # Future date - } - } - - # Should use today's date when current_date is None - result = calculate_current_week(config) - assert result["current_week"] > 0 - assert isinstance(result["current_week"], int) - assert isinstance(result["percent_complete"], (int, float)) - - -if __name__ == "__main__": - import pytest - sys.exit(pytest.main([__file__, "-v"])) diff --git a/tests/test_site_publish.py b/tests/test_site_publish.py deleted file mode 100644 index f7e1cff4b..000000000 --- a/tests/test_site_publish.py +++ /dev/null @@ -1,463 +0,0 @@ -#!/usr/bin/env python3 -""" -Unit tests for /craft:site:publish command. - -Tests the 5-step teaching workflow: -1. Validate draft branch -2. Preview changes -3. Confirm publish -4. Execute publish (with rollback) -5. Cleanup - -Mock git operations to avoid actual repository changes. -""" - -import os -import sys -import tempfile -import unittest -from pathlib import Path -from unittest.mock import Mock, patch, MagicMock, call - -import pytest - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from utils.detect_teaching_mode import detect_teaching_mode -from commands.utils.teaching_validation import ( - ValidationResult, - validate_teaching_content, -) - -pytestmark = [pytest.mark.integration, pytest.mark.site] - - -class TestSitePublishValidation(unittest.TestCase): - """Test validation step of publish workflow.""" - - def setUp(self): - """Create temporary teaching project.""" - self.test_dir = tempfile.mkdtemp() - - # Create teaching project structure - flow_dir = Path(self.test_dir) / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text(""" -course: - code: "STAT 440" - name: "Regression Analysis" - -branches: - draft: "draft" - production: "production" - -deployment: - gh_pages_url: "https://example.com/course/" - verify_deployment: true - deployment_timeout: 300 -""") - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_teaching_mode_detected(self): - """Should detect teaching mode from config.""" - is_teaching, method = detect_teaching_mode(self.test_dir) - - self.assertTrue(is_teaching) - self.assertEqual(method, "config") - - def test_validation_called_in_teaching_mode(self): - """Should call validation when teaching mode detected.""" - # Create complete course for validation - syllabus_content = """ -# Syllabus -## Objectives -Learn stats. -## Policies -Be nice. -## Grading -Homework 100%. -## Schedule -See schedule. -""" - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - schedule_content = """ -## Week 1 -Good content here with sufficient length. -""" - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - - self.assertTrue(result.can_publish()) - self.assertEqual(len(result.errors), 0) - - def test_validation_errors_block_publish(self): - """Should block publish when validation errors exist.""" - # Create incomplete course - syllabus_content = """ -# Syllabus -## Grading -Homework only. -""" - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - schedule_content = """ -## Week 1 -Content. - -## Week 2 - -## Week 3 -More content. -""" - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - - self.assertFalse(result.can_publish()) - self.assertGreater(len(result.errors), 0) - - def test_validation_warnings_allow_publish(self): - """Should allow publish when only warnings exist.""" - # Create complete syllabus and schedule - syllabus_content = """ -# Syllabus -## Objectives -Learn. -## Policies -Rules. -## Grading -Grades. -## Schedule -Times. -""" - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - # Schedule references assignments but files don't exist - schedule_content = """ -## Week 1 -Complete HW 1. Sufficient content here. - -## Week 2 -Complete HW 2. More content here. -""" - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - - # Should be publishable (warnings don't block) - self.assertTrue(result.can_publish()) - # But should have warnings about missing assignments - self.assertGreater(len(result.warnings), 0) - - -class TestSitePublishPreview(unittest.TestCase): - """Test preview changes step of publish workflow.""" - - @patch('subprocess.run') - def test_git_diff_stat_called(self, mock_run): - """Should call git diff --stat for preview.""" - mock_run.return_value = Mock( - returncode=0, - stdout=""" - syllabus/index.qmd | 18 +++++++++------- - schedule.qmd | 50 +++++++++++++++++++++++++++++++++++++++++ - lectures/week-01.qmd | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 180 insertions(+), 8 deletions(-) -""", - stderr="" - ) - - # Would call: git diff production..draft --stat - # Verify output can be parsed - result = mock_run.return_value.stdout - - self.assertIn("syllabus/index.qmd", result) - self.assertIn("schedule.qmd", result) - self.assertIn("lectures/week-01.qmd", result) - - def test_categorize_changed_files(self): - """Should categorize files into critical/content/other.""" - files = [ - "syllabus/index.qmd", - "syllabus.qmd", - "schedule.qmd", - "assignments/hw-1.qmd", - "lectures/week-01.qmd", - "readings/chapter-1.qmd", - "resources/datasets.qmd", - "_quarto.yml", - ".gitignore", - ] - - critical = [] - content = [] - other = [] - - for file in files: - if any(pattern in file for pattern in ["syllabus", "schedule", "assignments/"]): - critical.append(file) - elif any(pattern in file for pattern in ["lectures/", "readings/", "resources/"]): - content.append(file) - else: - other.append(file) - - self.assertEqual(len(critical), 4) # syllabus/index, syllabus, schedule, assignments/hw-1 - self.assertEqual(len(content), 3) # lectures, readings, resources - self.assertEqual(len(other), 2) # _quarto.yml, .gitignore - - -class TestSitePublishConfirmation(unittest.TestCase): - """Test confirmation step of publish workflow.""" - - def test_confirmation_options(self): - """Should present 3 options: Yes, Preview diff, Cancel.""" - options = [ - "Yes - Merge and deploy (Recommended)", - "Preview full diff first", - "Cancel" - ] - - self.assertEqual(len(options), 3) - self.assertIn("Yes", options[0]) - self.assertIn("Preview", options[1]) - self.assertIn("Cancel", options[2]) - - -class TestSitePublishExecution(unittest.TestCase): - """Test execution step with git operations.""" - - @patch('subprocess.run') - @patch('datetime.datetime') - def test_backup_branch_created(self, mock_datetime, mock_run): - """Should create backup branch with timestamp.""" - mock_datetime.now.return_value.strftime.return_value = "20260116-143022" - mock_run.return_value = Mock(returncode=0, stdout="", stderr="") - - # Simulate: git branch production-backup-20260116-143022 - backup_name = f"production-backup-{mock_datetime.now().strftime('%Y%m%d-%H%M%S')}" - - self.assertEqual(backup_name, "production-backup-20260116-143022") - - @patch('subprocess.run') - def test_fast_forward_merge(self, mock_run): - """Should attempt fast-forward merge.""" - mock_run.return_value = Mock(returncode=0, stdout="", stderr="") - - # Would call: git merge draft --ff-only - # Verify command structure - expected_args = ["git", "merge", "draft", "--ff-only"] - - self.assertEqual(len(expected_args), 4) - self.assertIn("--ff-only", expected_args) - - @patch('subprocess.run') - def test_merge_conflict_rollback(self, mock_run): - """Should rollback on merge conflict.""" - # Simulate merge failure - mock_run.side_effect = [ - Mock(returncode=0, stdout="", stderr=""), # backup creation - Mock(returncode=0, stdout="", stderr=""), # checkout production - Mock(returncode=1, stdout="", stderr="CONFLICT"), # merge fails - Mock(returncode=0, stdout="", stderr=""), # rollback - ] - - # On conflict, should call: git reset --hard production-backup- - rollback_command = ["git", "reset", "--hard", "production-backup-20260116-143022"] - - self.assertIn("--hard", rollback_command) - self.assertIn("production-backup", rollback_command[3]) - - @patch('subprocess.run') - def test_push_to_remote(self, mock_run): - """Should push production branch to remote.""" - mock_run.return_value = Mock(returncode=0, stdout="", stderr="") - - # Would call: git push origin production - push_command = ["git", "push", "origin", "production"] - - self.assertEqual(len(push_command), 4) - self.assertEqual(push_command[3], "production") - - @patch('subprocess.run') - def test_push_failure_rollback(self, mock_run): - """Should rollback on push failure.""" - # Simulate push failure - side_effects = [ - Mock(returncode=0, stdout="", stderr=""), # backup - Mock(returncode=0, stdout="", stderr=""), # checkout - Mock(returncode=0, stdout="", stderr=""), # merge success - Mock(returncode=1, stdout="", stderr="Permission denied"), # push fails - Mock(returncode=0, stdout="", stderr=""), # rollback - ] - mock_run.side_effect = side_effects - - # Should preserve backup branch and rollback - # Verify rollback logic is triggered - self.assertEqual(len(side_effects), 5) - - @patch('requests.get') - def test_deployment_verification(self, mock_get): - """Should verify deployment by checking URL.""" - mock_get.return_value = Mock(status_code=200) - - # Would call: requests.get("https://example.com/course/") - url = "https://example.com/course/" - response = mock_get(url) - - self.assertEqual(response.status_code, 200) - - @patch('requests.get') - def test_deployment_verification_timeout(self, mock_get): - """Should handle deployment verification timeout gracefully.""" - import requests - mock_get.side_effect = requests.Timeout() - - # Should warn but not rollback - try: - response = mock_get("https://example.com/course/", timeout=300) - self.fail("Should have raised Timeout") - except requests.Timeout: - # Expected - should warn user but continue - pass - - @patch('requests.get') - def test_deployment_verification_404(self, mock_get): - """Should warn on 404 but not rollback.""" - mock_get.return_value = Mock(status_code=404) - - url = "https://example.com/course/" - response = mock_get(url) - - # Should warn but not rollback (may take time to propagate) - self.assertEqual(response.status_code, 404) - - -class TestSitePublishCleanup(unittest.TestCase): - """Test cleanup step of publish workflow.""" - - @patch('subprocess.run') - def test_return_to_original_branch(self, mock_run): - """Should checkout original branch after publish.""" - mock_run.return_value = Mock(returncode=0, stdout="", stderr="") - - # Would call: git checkout draft - checkout_command = ["git", "checkout", "draft"] - - self.assertEqual(len(checkout_command), 3) - self.assertEqual(checkout_command[2], "draft") - - def test_success_message_format(self): - """Should show ADHD-friendly success message.""" - message_parts = [ - "✅ PUBLISH SUCCESSFUL", - "🌐 Live Site:", - "📊 Changes Published:", - "💡 Next Steps:", - ] - - # Verify all key components present - for part in message_parts: - self.assertIsNotNone(part) - - -class TestSitePublishNonTeaching(unittest.TestCase): - """Test publish workflow for non-teaching projects.""" - - def setUp(self): - """Create temporary non-teaching project.""" - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_non_teaching_mode_detected(self): - """Should detect non-teaching mode.""" - is_teaching, method = detect_teaching_mode(self.test_dir) - - self.assertFalse(is_teaching) - self.assertIsNone(method) - - def test_validation_skipped_for_non_teaching(self): - """Should skip validation for non-teaching projects.""" - is_teaching, _ = detect_teaching_mode(self.test_dir) - - if not is_teaching: - # Validation should be skipped - self.assertFalse(is_teaching) - - -class TestSitePublishIntegration(unittest.TestCase): - """Integration tests for full publish workflow.""" - - def setUp(self): - """Create temporary teaching project.""" - self.test_dir = tempfile.mkdtemp() - - # Create complete teaching project - flow_dir = Path(self.test_dir) / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text(""" -course: - code: "STAT 440" -branches: - draft: "draft" - production: "production" -deployment: - gh_pages_url: "https://example.com/course/" -""") - - # Complete syllabus - syllabus_content = """ -# Syllabus -## Objectives -Learn. -## Policies -Rules. -## Grading -Grades. -## Schedule -Times. -""" - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - # Complete schedule - schedule_content = """ -## Week 1 -Content here that is long enough to pass validation checks. -""" - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_full_workflow_success(self): - """Should complete all 5 steps successfully.""" - # Step 1: Detect teaching mode - is_teaching, method = detect_teaching_mode(self.test_dir) - self.assertTrue(is_teaching) - - # Step 2: Validate content - validation = validate_teaching_content(self.test_dir) - self.assertTrue(validation.can_publish()) - - # Step 3-5 would require git operations (mocked in other tests) - # Verify workflow components are present - self.assertTrue(is_teaching) - self.assertTrue(validation.can_publish()) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_teach_config.py b/tests/test_teach_config.py deleted file mode 100644 index be7ae3a29..000000000 --- a/tests/test_teach_config.py +++ /dev/null @@ -1,810 +0,0 @@ -#!/usr/bin/env python3 -""" -Unit tests for teaching configuration parser. - -Tests cover: -- Valid configurations (complete and minimal) -- Invalid YAML syntax -- Missing required fields -- Date validation (format, logical order) -- Break validation (dates, overlaps, semester bounds) -- Config path detection (.flow/ vs root) -- Default value application -- Edge cases and error handling - -Run: - python3 -m pytest tests/test_teach_config.py -v - python3 tests/test_teach_config.py # Direct execution -""" - -import os -import sys -import tempfile -import unittest -from datetime import datetime -from pathlib import Path - -import pytest - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -pytestmark = [pytest.mark.unit, pytest.mark.teaching] - -from commands.utils.teach_config import ( - get_config_path, - validate_date, - parse_date, - validate_breaks, - apply_defaults, - validate_config, - load_teach_config, - DEFAULTS, - VALID_SEMESTERS, -) - - -class TestValidateDate(unittest.TestCase): - """Test date validation function""" - - def test_valid_dates(self): - """Valid YYYY-MM-DD dates should pass""" - valid_dates = [ - "2026-01-19", - "2025-12-31", - "2000-01-01", - "2099-12-31", - ] - for date_str in valid_dates: - with self.subTest(date=date_str): - self.assertTrue(validate_date(date_str)) - - def test_invalid_format(self): - """Invalid date formats should fail""" - invalid_dates = [ - "2026/01/19", # Wrong separator - "01-19-2026", # Wrong order - "2026-1-19", # Missing zero padding - "2026-13-01", # Invalid month - "2026-01-32", # Invalid day - "not-a-date", # Text - "", # Empty - None, # None type - ] - for date_str in invalid_dates: - with self.subTest(date=date_str): - self.assertFalse(validate_date(date_str)) - - -class TestParseDate(unittest.TestCase): - """Test date parsing function""" - - def test_parse_valid_date(self): - """Valid dates should parse to datetime objects""" - result = parse_date("2026-01-19") - self.assertIsInstance(result, datetime) - self.assertEqual(result.year, 2026) - self.assertEqual(result.month, 1) - self.assertEqual(result.day, 19) - - def test_parse_invalid_date(self): - """Invalid dates should return None""" - self.assertIsNone(parse_date("invalid")) - self.assertIsNone(parse_date("2026/01/19")) - self.assertIsNone(parse_date(None)) - - -class TestValidateBreaks(unittest.TestCase): - """Test break validation function""" - - def test_empty_breaks(self): - """Empty breaks list should be valid""" - errors = validate_breaks([], "2026-01-19", "2026-05-08") - self.assertEqual(errors, []) - - def test_valid_break(self): - """Valid break should pass validation""" - breaks = [ - { - "name": "Spring Break", - "start": "2026-03-16", - "end": "2026-03-20", - } - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertEqual(errors, []) - - def test_multiple_valid_breaks(self): - """Multiple non-overlapping breaks should be valid""" - breaks = [ - { - "name": "Spring Break", - "start": "2026-03-16", - "end": "2026-03-20", - }, - { - "name": "Reading Week", - "start": "2026-04-13", - "end": "2026-04-14", - }, - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertEqual(errors, []) - - def test_missing_name(self): - """Break missing name should fail""" - breaks = [{"start": "2026-03-16", "end": "2026-03-20"}] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("missing 'name'" in e for e in errors)) - - def test_missing_start(self): - """Break missing start should fail""" - breaks = [{"name": "Spring Break", "end": "2026-03-20"}] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("missing 'start'" in e for e in errors)) - - def test_missing_end(self): - """Break missing end should fail""" - breaks = [{"name": "Spring Break", "start": "2026-03-16"}] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("missing 'end'" in e for e in errors)) - - def test_invalid_date_format(self): - """Break with invalid date format should fail""" - breaks = [ - { - "name": "Spring Break", - "start": "2026/03/16", - "end": "2026-03-20", - } - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("invalid start date format" in e for e in errors)) - - def test_end_before_start(self): - """Break with end before start should fail""" - breaks = [ - { - "name": "Spring Break", - "start": "2026-03-20", - "end": "2026-03-16", - } - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("start date must be before or equal to end date" in e for e in errors)) - - def test_break_before_semester(self): - """Break starting before semester should fail""" - breaks = [ - { - "name": "Winter Break", - "start": "2026-01-01", - "end": "2026-01-18", - } - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("starts before semester begins" in e for e in errors)) - - def test_break_after_semester(self): - """Break ending after semester should fail""" - breaks = [ - { - "name": "Summer Break", - "start": "2026-05-07", - "end": "2026-05-15", - } - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("ends after semester ends" in e for e in errors)) - - def test_overlapping_breaks(self): - """Overlapping breaks should fail""" - breaks = [ - { - "name": "Break 1", - "start": "2026-03-16", - "end": "2026-03-20", - }, - { - "name": "Break 2", - "start": "2026-03-19", # Overlaps with Break 1 - "end": "2026-03-23", - }, - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("overlap" in e for e in errors)) - - def test_invalid_semester_dates(self): - """Invalid semester dates should fail gracefully""" - breaks = [ - { - "name": "Spring Break", - "start": "2026-03-16", - "end": "2026-03-20", - } - ] - errors = validate_breaks(breaks, "invalid", "2026-05-08") - self.assertTrue(any("invalid semester dates" in e for e in errors)) - - -class TestApplyDefaults(unittest.TestCase): - """Test default value application""" - - def test_apply_all_defaults(self): - """Empty config should get all defaults""" - config = {"course": {}, "dates": {}} - result = apply_defaults(config) - - # Check deployment defaults - self.assertIn("deployment", result) - self.assertEqual(result["deployment"]["production_branch"], "production") - self.assertEqual(result["deployment"]["draft_branch"], "draft") - - # Check progress defaults - self.assertIn("progress", result) - self.assertEqual(result["progress"]["current_week"], "auto") - - # Check validation defaults - self.assertIn("validation", result) - self.assertEqual(result["validation"]["strict_mode"], True) - self.assertIn("grading", result["validation"]["required_sections"]) - - # Check dates defaults - self.assertIn("breaks", result["dates"]) - self.assertEqual(result["dates"]["breaks"], []) - - def test_preserve_existing_values(self): - """Existing values should not be overwritten""" - config = { - "course": {}, - "dates": {"breaks": [{"name": "test"}]}, - "deployment": {"production_branch": "prod"}, - "progress": {"current_week": 5}, - "validation": {"strict_mode": False}, - } - result = apply_defaults(config) - - self.assertEqual(result["deployment"]["production_branch"], "prod") - self.assertEqual(result["progress"]["current_week"], 5) - self.assertEqual(result["validation"]["strict_mode"], False) - self.assertEqual(len(result["dates"]["breaks"]), 1) - - -class TestValidateConfig(unittest.TestCase): - """Test configuration validation""" - - def get_minimal_valid_config(self): - """Return minimal valid configuration""" - return { - "course": { - "number": "STAT 545", - "title": "Regression Analysis", - "semester": "Spring", - "year": 2026, - }, - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - "breaks": [], - }, - } - - def test_valid_minimal_config(self): - """Minimal valid config should pass""" - config = self.get_minimal_valid_config() - errors = validate_config(config) - self.assertEqual(errors, []) - - def test_missing_course_section(self): - """Missing course section should fail""" - config = {"dates": {"start": "2026-01-19", "end": "2026-05-08"}} - errors = validate_config(config) - self.assertTrue(any("Missing required section: 'course'" in e for e in errors)) - - def test_missing_dates_section(self): - """Missing dates section should fail""" - config = { - "course": { - "number": "STAT 545", - "title": "Test", - "semester": "Spring", - "year": 2026, - } - } - errors = validate_config(config) - self.assertTrue(any("Missing required section: 'dates'" in e for e in errors)) - - def test_missing_course_fields(self): - """Missing required course fields should fail""" - config = self.get_minimal_valid_config() - del config["course"]["number"] - errors = validate_config(config) - self.assertTrue(any("course.number" in e for e in errors)) - - def test_invalid_semester(self): - """Invalid semester value should fail""" - config = self.get_minimal_valid_config() - config["course"]["semester"] = "Invalid" - errors = validate_config(config) - self.assertTrue(any("Invalid semester" in e for e in errors)) - - def test_invalid_year_type(self): - """Non-integer year should fail""" - config = self.get_minimal_valid_config() - config["course"]["year"] = "2026" - errors = validate_config(config) - self.assertTrue(any("Invalid year" in e for e in errors)) - - def test_invalid_year_range(self): - """Year out of range should fail""" - config = self.get_minimal_valid_config() - config["course"]["year"] = 1999 - errors = validate_config(config) - self.assertTrue(any("must be between 2000 and 2100" in e for e in errors)) - - def test_invalid_start_date(self): - """Invalid start date format should fail""" - config = self.get_minimal_valid_config() - config["dates"]["start"] = "2026/01/19" - errors = validate_config(config) - self.assertTrue(any("Invalid start date format" in e for e in errors)) - - def test_end_before_start(self): - """End date before start should fail""" - config = self.get_minimal_valid_config() - config["dates"]["start"] = "2026-05-08" - config["dates"]["end"] = "2026-01-19" - errors = validate_config(config) - self.assertTrue(any("end date must be after start date" in e for e in errors)) - - def test_invalid_current_week_type(self): - """Invalid current_week type should fail""" - config = self.get_minimal_valid_config() - config["progress"] = {"current_week": "five"} - errors = validate_config(config) - self.assertTrue(any("Invalid current_week" in e for e in errors)) - - def test_invalid_current_week_range(self): - """current_week out of range should fail""" - config = self.get_minimal_valid_config() - config["progress"] = {"current_week": 100} - errors = validate_config(config) - self.assertTrue(any("must be between 1 and 52" in e for e in errors)) - - def test_invalid_strict_mode_type(self): - """Non-boolean strict_mode should fail""" - config = self.get_minimal_valid_config() - config["validation"] = {"strict_mode": "yes"} - errors = validate_config(config) - self.assertTrue(any("Invalid strict_mode" in e for e in errors)) - - -class TestGetConfigPath(unittest.TestCase): - """Test config file path detection""" - - def test_flow_config_priority(self): - """Should prefer .flow/teach-config.yml over root""" - with tempfile.TemporaryDirectory() as tmpdir: - # Create both files - flow_dir = Path(tmpdir) / ".flow" - flow_dir.mkdir() - flow_config = flow_dir / "teach-config.yml" - flow_config.write_text("test: flow") - - root_config = Path(tmpdir) / "teach-config.yml" - root_config.write_text("test: root") - - # Should return .flow version (resolve both paths for comparison) - result = get_config_path(tmpdir) - self.assertEqual(Path(result).resolve(), flow_config.resolve()) - - def test_root_config_fallback(self): - """Should use root config if .flow doesn't exist""" - with tempfile.TemporaryDirectory() as tmpdir: - root_config = Path(tmpdir) / "teach-config.yml" - root_config.write_text("test: root") - - result = get_config_path(tmpdir) - self.assertEqual(Path(result).resolve(), root_config.resolve()) - - def test_no_config_found(self): - """Should return None if no config exists""" - with tempfile.TemporaryDirectory() as tmpdir: - result = get_config_path(tmpdir) - self.assertIsNone(result) - - -class TestLoadTeachConfig(unittest.TestCase): - """Test full config loading and validation""" - - def test_load_complete_valid_config(self): - """Complete valid config should load successfully""" - config_yaml = """ -course: - number: "STAT 545" - title: "Regression Analysis" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" - breaks: - - name: "Spring Break" - start: "2026-03-16" - end: "2026-03-20" - -instructor: - name: "Dr. Jane Smith" - email: "jsmith@university.edu" - office_hours: "Tu/Th 2-3pm" - -deployment: - production_branch: "production" - draft_branch: "draft" - gh_pages_url: "https://example.com" - -progress: - current_week: auto - -validation: - required_sections: - - grading - - policies - strict_mode: true -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / ".flow" / "teach-config.yml" - config_path.parent.mkdir() - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNotNone(config) - self.assertEqual(config["course"]["number"], "STAT 545") - self.assertEqual(config["dates"]["start"], "2026-01-19") - self.assertEqual(len(config["dates"]["breaks"]), 1) - - def test_load_minimal_config_with_defaults(self): - """Minimal config should load with defaults applied""" - config_yaml = """ -course: - number: "STAT 545" - title: "Regression Analysis" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNotNone(config) - - # Check defaults applied - self.assertEqual(config["deployment"]["production_branch"], "production") - self.assertEqual(config["progress"]["current_week"], "auto") - self.assertTrue(config["validation"]["strict_mode"]) - self.assertEqual(config["dates"]["breaks"], []) - - def test_no_config_returns_none(self): - """Missing config should return None (not an error)""" - with tempfile.TemporaryDirectory() as tmpdir: - config = load_teach_config(tmpdir) - self.assertIsNone(config) - - def test_malformed_yaml_returns_none(self): - """Malformed YAML should return None with warning""" - config_yaml = """ -course: - number: "STAT 545" - - invalid yaml structure -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNone(config) - - def test_invalid_config_raises_error(self): - """Invalid config should raise ValueError""" - config_yaml = """ -course: - number: "STAT 545" - # Missing required fields -dates: - start: "2026-01-19" - end: "2026-05-08" -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - with self.assertRaises(ValueError) as cm: - load_teach_config(tmpdir) - - self.assertIn("validation failed", str(cm.exception)) - - def test_non_dict_config_returns_none(self): - """Non-dictionary YAML should return None""" - config_yaml = "just a string" - - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNone(config) - - def test_invalid_breaks_raises_error(self): - """Config with invalid breaks should raise ValueError""" - config_yaml = """ -course: - number: "STAT 545" - title: "Test" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" - breaks: - - name: "Spring Break" - start: "2026-03-20" - end: "2026-03-16" # End before start -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - with self.assertRaises(ValueError) as cm: - load_teach_config(tmpdir) - - self.assertIn("validation failed", str(cm.exception)) - - -class TestEdgeCases(unittest.TestCase): - """Test edge cases and error conditions""" - - def test_leap_year_date(self): - """Leap year dates should be valid""" - self.assertTrue(validate_date("2024-02-29")) - - def test_non_leap_year_invalid(self): - """Non-leap year Feb 29 should be invalid""" - self.assertFalse(validate_date("2023-02-29")) - - def test_all_valid_semesters(self): - """All valid semester values should pass""" - for semester in VALID_SEMESTERS: - config = { - "course": { - "number": "TEST", - "title": "Test", - "semester": semester, - "year": 2026, - }, - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - }, - } - errors = validate_config(config) - self.assertEqual(errors, []) - - def test_current_week_auto_string(self): - """current_week: auto should be valid""" - config = { - "course": { - "number": "TEST", - "title": "Test", - "semester": "Spring", - "year": 2026, - }, - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - }, - "progress": { - "current_week": "auto", - }, - } - errors = validate_config(config) - self.assertEqual(errors, []) - - def test_current_week_integer(self): - """current_week as integer should be valid""" - config = { - "course": { - "number": "TEST", - "title": "Test", - "semester": "Spring", - "year": 2026, - }, - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - }, - "progress": { - "current_week": 8, - }, - } - errors = validate_config(config) - self.assertEqual(errors, []) - - def test_file_read_error(self): - """File read errors should return None with warning""" - with tempfile.TemporaryDirectory() as tmpdir: - # Create a directory instead of a file - config_path = Path(tmpdir) / "teach-config.yml" - config_path.mkdir() # This will fail to read as YAML - - config = load_teach_config(tmpdir) - self.assertIsNone(config) - - def test_validation_required_sections_optional(self): - """validation.required_sections should be optional""" - config_yaml = """ -course: - number: "STAT 545" - title: "Test" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" - -validation: - strict_mode: false -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNotNone(config) - # Should have default required_sections - self.assertIn("required_sections", config["validation"]) - - def test_instructor_section_optional(self): - """Instructor section is completely optional""" - config = { - "course": { - "number": "TEST", - "title": "Test", - "semester": "Spring", - "year": 2026, - }, - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - }, - } - errors = validate_config(config) - self.assertEqual(errors, []) - - def test_deployment_gh_pages_url_optional(self): - """deployment.gh_pages_url is optional""" - config_yaml = """ -course: - number: "STAT 545" - title: "Test" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" - -deployment: - production_branch: "prod" - draft_branch: "draft" -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNotNone(config) - self.assertEqual(config["deployment"]["production_branch"], "prod") - self.assertNotIn("gh_pages_url", config["deployment"]) - - def test_breaks_validation_with_multiple_errors(self): - """Break validation should collect all errors""" - breaks = [ - { - "name": "Bad Break 1", - "start": "2026-03-20", - "end": "2026-03-16", # End before start - }, - { - "name": "Bad Break 2", - "start": "2026-01-01", # Before semester - "end": "2026-01-05", - }, - ] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - # Should have multiple errors - self.assertGreater(len(errors), 1) - - def test_breaks_not_a_dict(self): - """Break that's not a dictionary should fail""" - breaks = ["not a dict"] - errors = validate_breaks(breaks, "2026-01-19", "2026-05-08") - self.assertTrue(any("must be a dictionary" in e for e in errors)) - - def test_config_without_instructor_section(self): - """Config without instructor section should work""" - config_yaml = """ -course: - number: "STAT 545" - title: "Test" - semester: "Spring" - year: 2026 - -dates: - start: "2026-01-19" - end: "2026-05-08" -""" - with tempfile.TemporaryDirectory() as tmpdir: - config_path = Path(tmpdir) / "teach-config.yml" - config_path.write_text(config_yaml) - - config = load_teach_config(tmpdir) - self.assertIsNotNone(config) - self.assertNotIn("instructor", config) - - def test_validation_section_partial(self): - """Partial validation section should merge with defaults""" - config = { - "course": { - "number": "TEST", - "title": "Test", - "semester": "Spring", - "year": 2026, - }, - "dates": { - "start": "2026-01-19", - "end": "2026-05-08", - }, - "validation": { - "strict_mode": False, - # required_sections should get default - }, - } - result = apply_defaults(config) - self.assertIn("required_sections", result["validation"]) - self.assertEqual(result["validation"]["strict_mode"], False) - - def test_progress_section_partial(self): - """Partial progress section should merge with defaults""" - config = { - "course": {"number": "TEST"}, - "dates": {"start": "2026-01-19"}, - "progress": {}, # Empty progress section - } - result = apply_defaults(config) - self.assertEqual(result["progress"]["current_week"], "auto") - - def test_deployment_section_partial(self): - """Partial deployment section should merge with defaults""" - config = { - "course": {"number": "TEST"}, - "dates": {"start": "2026-01-19"}, - "deployment": { - "production_branch": "custom-prod", - # draft_branch should get default - }, - } - result = apply_defaults(config) - self.assertEqual(result["deployment"]["production_branch"], "custom-prod") - self.assertEqual(result["deployment"]["draft_branch"], "draft") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_teaching_documentation.py b/tests/test_teaching_documentation.py deleted file mode 100644 index 79c7b16f5..000000000 --- a/tests/test_teaching_documentation.py +++ /dev/null @@ -1,288 +0,0 @@ -#!/usr/bin/env python3 -""" -Test suite for teaching workflow documentation. - -Validates that all teaching workflow documentation is complete, accurate, -and properly integrated into the documentation site. -""" - -import os -import re -from pathlib import Path - -import pytest - -pytestmark = [pytest.mark.unit, pytest.mark.teaching] - -# Test configuration -DOCS_DIR = Path(__file__).parent.parent / "docs" -COMMANDS_DIR = Path(__file__).parent.parent / "commands" - - -class TestTeachingDocumentation: - """Test teaching workflow documentation completeness""" - - def test_teaching_guide_exists(self): - """Teaching workflow guide should exist""" - guide_path = DOCS_DIR / "guide" / "teaching-workflow.md" - assert guide_path.exists(), "Teaching workflow guide not found" - - content = guide_path.read_text() - assert len(content) > 1000, "Teaching guide seems incomplete" - assert "Preview-before-publish" in content, "Missing key workflow description" - assert "Content validation" in content, "Missing validation section" - assert "Semester progress" in content, "Missing progress tracking section" - - def test_teaching_refcard_exists(self): - """Teaching quick reference should exist""" - refcard_path = DOCS_DIR / "reference" / "REFCARD-TEACHING.md" - assert refcard_path.exists(), "Teaching refcard not found" - - content = refcard_path.read_text() - # site:publish/progress/build moved to folio in the folio split (Phase 3, 2026-07-12) - assert "/folio:site:publish" in content, "Missing publish command" - assert "/folio:site:progress" in content, "Missing progress command" - assert "/folio:site:build" in content, "Missing build command" - - def test_teaching_demo_exists(self): - """VHS demo tape should exist""" - demo_path = DOCS_DIR / "demos" / "teaching-workflow.tape" - assert demo_path.exists(), "Teaching demo tape not found" - - content = demo_path.read_text() - assert "Output" in content, "Missing output directive" - # site:publish moved to folio in the folio split (Phase 3, 2026-07-12) - assert "/folio:site:publish" in content, "Demo doesn't show publish workflow" - - def test_teaching_tutorial_exists(self): - """Teaching mode setup tutorial should exist (from PR)""" - tutorial_path = DOCS_DIR / "tutorials" / "teaching-mode-setup.md" - assert tutorial_path.exists(), "Teaching setup tutorial not found" - - def test_teaching_config_schema_exists(self): - """Teaching config schema should exist (from PR)""" - schema_path = DOCS_DIR / "teaching-config-schema.md" - assert schema_path.exists(), "Teaching config schema not found" - - def test_teaching_migration_guide_exists(self): - """Teaching migration guide should exist (from PR)""" - migration_path = DOCS_DIR / "teaching-migration.md" - assert migration_path.exists(), "Teaching migration guide not found" - - def test_teaching_commands_documented(self): - """All teaching commands should be documented. - - site/publish.md, site/progress.md, site/build.md moved to the `folio` - plugin in the folio split (Phase 3, 2026-07-12) — folio now owns that - surface. git/status.md (formerly craft's only remaining - teaching-adjacent command) was folded entirely into the dev/git skill - in the v4 consolidation (Phase 3.5, 2026-07) — commands/git/ is now - empty, so no teaching-adjacent commands remain to check here.""" - commands: list[str] = [] - - for cmd in commands: - cmd_path = COMMANDS_DIR / cmd - assert cmd_path.exists(), f"Command file {cmd} not found" - - def test_mkdocs_navigation_includes_teaching(self): - """mkdocs.yml should include teaching documentation""" - mkdocs_path = Path(__file__).parent.parent / "mkdocs.yml" - assert mkdocs_path.exists(), "mkdocs.yml not found" - - content = mkdocs_path.read_text() - assert "teaching-workflow.md" in content, "Teaching guide not in navigation" - assert "REFCARD-TEACHING.md" in content, "Teaching refcard not in help section" - - def test_refcard_includes_teaching_section(self): - """Main refcard should include teaching commands""" - refcard_path = DOCS_DIR / "REFCARD.md" - assert refcard_path.exists(), "Main REFCARD.md not found" - - content = refcard_path.read_text() - # site:publish/progress moved to folio in the folio split (Phase 3, 2026-07-12) - assert "/folio:site:publish" in content, "Publish command not in main refcard" - assert "/folio:site:progress" in content, "Progress command not in main refcard" - assert "Teaching Mode" in content, "Teaching mode section missing" - - def test_changelog_includes_v122(self): - """CHANGELOG should document v1.22.0 teaching release""" - changelog_path = Path(__file__).parent.parent / "CHANGELOG.md" - assert changelog_path.exists(), "CHANGELOG.md not found" - - content = changelog_path.read_text() - assert "## [1.22.0]" in content, "v1.22.0 release not documented" - assert "Teaching Workflow System" in content, "Teaching feature not in changelog" - assert "80% time reduction" in content, "Impact metrics missing" - - def test_teaching_guide_has_complete_sections(self): - """Teaching guide should have all required sections""" - guide_path = DOCS_DIR / "guide" / "teaching-workflow.md" - content = guide_path.read_text() - - required_sections = [ - "## Overview", - "## Quick Start", - "## Core Commands", - "## How It Works", - "## Configuration", - "## Common Workflows", - "## Teaching-Aware Commands", - "## Troubleshooting", - "## Migration Guide", - "## Advanced Features", - "## Impact" - ] - - for section in required_sections: - assert section in content, f"Missing section: {section}" - - def test_teaching_guide_has_mermaid_diagram(self): - """Teaching guide should include workflow diagram""" - guide_path = DOCS_DIR / "guide" / "teaching-workflow.md" - content = guide_path.read_text() - - assert "```mermaid" in content, "Missing Mermaid diagram" - assert ( - "graph TD" in content - or "graph LR" in content - or "flowchart TD" in content - or "flowchart LR" in content - ), "Mermaid diagram incomplete" - - def test_teaching_guide_has_code_examples(self): - """Teaching guide should include practical examples""" - guide_path = DOCS_DIR / "guide" / "teaching-workflow.md" - content = guide_path.read_text() - - # Check for YAML examples - assert "```yaml" in content, "Missing YAML configuration examples" - assert "teaching:" in content, "Missing teaching config example" - - # Check for shell examples - assert "```bash" in content, "Missing shell command examples" - - def test_refcard_has_quick_start(self): - """Refcard should have copy-paste ready quick start""" - refcard_path = DOCS_DIR / "reference" / "REFCARD-TEACHING.md" - content = refcard_path.read_text() - - assert "## Quick Start" in content, "Missing quick start section" - assert "cat >" in content or "teaching:" in content, "Missing quick start config" - - def test_refcard_has_common_workflows(self): - """Refcard should document common workflows""" - refcard_path = DOCS_DIR / "reference" / "REFCARD-TEACHING.md" - content = refcard_path.read_text() - - assert "## Common Workflows" in content, "Missing workflows section" - assert "Weekly Content Update" in content, "Missing weekly update workflow" - - def test_refcard_has_troubleshooting(self): - """Refcard should have troubleshooting quick fixes""" - refcard_path = DOCS_DIR / "reference" / "REFCARD-TEACHING.md" - content = refcard_path.read_text() - - assert "## Troubleshooting" in content, "Missing troubleshooting section" - assert "Not Detecting Teaching Mode" in content, "Missing detection troubleshooting" - - def test_demo_tape_is_valid_vhs(self): - """VHS demo tape should have valid syntax""" - demo_path = DOCS_DIR / "demos" / "teaching-workflow.tape" - content = demo_path.read_text() - - # Check VHS directives - assert "Output" in content, "Missing Output directive" - assert "Set FontSize" in content, "Missing font size setting" - assert "Set Width" in content, "Missing width setting" - assert "Set Height" in content, "Missing height setting" - - # Check demo steps - assert "Type" in content, "Missing Type commands" - assert "Sleep" in content, "Missing Sleep commands" - assert "Enter" in content, "Missing Enter commands" - - def test_teaching_docs_cross_reference(self): - """Teaching docs should properly cross-reference each other""" - guide_path = DOCS_DIR / "guide" / "teaching-workflow.md" - content = guide_path.read_text() - - # Should reference other docs - assert "teaching-config-schema.md" in content, "Missing schema reference" - assert "teaching-migration.md" in content, "Missing migration reference" - assert "teaching-mode-setup.md" in content, "Missing tutorial reference" - - def test_no_broken_internal_links_in_teaching_docs(self): - """Teaching documentation should not have broken internal links""" - teaching_docs = [ - DOCS_DIR / "guide" / "teaching-workflow.md", - DOCS_DIR / "reference" / "REFCARD-TEACHING.md" - ] - - for doc_path in teaching_docs: - if not doc_path.exists(): - continue - - content = doc_path.read_text() - - # Find markdown links - link_pattern = r'\[([^\]]+)\]\(([^)]+)\)' - links = re.findall(link_pattern, content) - - for link_text, link_url in links: - # Skip external links - if link_url.startswith(('http://', 'https://', '#')): - continue - - # Check internal file links - if link_url.endswith('.md'): - # Resolve relative path - target = (doc_path.parent / link_url).resolve() - assert target.exists(), f"Broken link in {doc_path.name}: {link_url}" - - def test_teaching_utilities_have_readme(self): - """readme-teach-config / readme-semester-progress were removed as teaching - residue in the v4 command consolidation (2026-07) — no replacement needed.""" - - def test_teaching_examples_exist(self): - """Teaching config examples should exist""" - examples_dir = Path(__file__).parent.parent / "examples" - - example_path = examples_dir / "teach-config-example.yml" - assert example_path.exists(), "Missing teach-config example" - - content = example_path.read_text() - # Example uses course: root key instead of teaching: - assert "course:" in content or "teaching:" in content, "Example missing config structure" - assert "semester:" in content or "dates:" in content, "Example missing semester info" - - def test_documentation_completeness_score(self): - """Calculate documentation completeness score""" - checks = { - 'guide': (DOCS_DIR / "guide" / "teaching-workflow.md").exists(), - 'refcard': (DOCS_DIR / "reference" / "REFCARD-TEACHING.md").exists(), - 'demo': (DOCS_DIR / "demos" / "teaching-workflow.tape").exists(), - 'tutorial': (DOCS_DIR / "tutorials" / "teaching-mode-setup.md").exists(), - 'schema': (DOCS_DIR / "teaching-config-schema.md").exists(), - 'migration': (DOCS_DIR / "teaching-migration.md").exists(), - 'changelog': "## [1.22.0]" in (Path(__file__).parent.parent / "CHANGELOG.md").read_text(), - 'navigation': "teaching-workflow.md" in (Path(__file__).parent.parent / "mkdocs.yml").read_text() - } - - score = sum(checks.values()) / len(checks) * 100 - print(f"\n📊 Documentation Completeness: {score:.0f}%") - - assert score == 100, f"Documentation incomplete: {score:.0f}%" - - -def run_tests(): - """Run all documentation tests""" - import pytest - import sys - - # Run tests with verbose output - exit_code = pytest.main([__file__, '-v', '--tb=short']) - sys.exit(exit_code) - - -if __name__ == '__main__': - run_tests() diff --git a/tests/test_teaching_integration.py b/tests/test_teaching_integration.py deleted file mode 100644 index 51ed2b334..000000000 --- a/tests/test_teaching_integration.py +++ /dev/null @@ -1,332 +0,0 @@ -#!/usr/bin/env python3 -""" -Teaching Workflow Integration Tests -==================================== -Comprehensive end-to-end tests for the teaching workflow implementation. - -Tests cover: -- End-to-end workflow scenarios -- Error scenarios and edge cases -- Cross-component integration -- Performance benchmarks -- Validation completeness - -Run with: python tests/test_teaching_integration.py -""" - -import json -import os -import shutil -import sys -import tempfile -import time -from datetime import datetime, timedelta -from pathlib import Path -from typing import List, Optional, Dict, Any - -import pytest - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -from commands.utils.teach_config import load_teach_config, validate_config -from commands.utils.teaching_validation import validate_teaching_content -from commands.utils.semester_progress import calculate_current_week as _calculate_current_week -from utils.detect_teaching_mode import detect_teaching_mode - -pytestmark = [pytest.mark.integration, pytest.mark.teaching] - - -# ─── Test Fixtures ──────────────────────────────────────────────────────────── - -def get_fixture_path(name: str) -> Path: - """Get path to a test fixture.""" - return Path(__file__).parent / "fixtures" / "teaching" / name - - -def calculate_semester_progress(cwd: str) -> Optional[Dict[str, any]]: - """ - Wrapper for calculate_current_week that loads config first. - - Args: - cwd: Path to teaching project - - Returns: - Progress dictionary or None if config not found - """ - try: - config = load_teach_config(cwd) - if not config: - return None - return _calculate_current_week(config) - except Exception: - return None - - -# ─── End-to-End Workflow Tests ─────────────────────────────────────────────── - -def test_e2e_minimal_course(): - """Test complete workflow with minimal course configuration.""" - fixture = get_fixture_path("minimal") - - # Step 1: Detection - is_teaching, method = detect_teaching_mode(str(fixture)) - assert is_teaching, "Failed to detect minimal teaching project" - - # Step 2: Load config - config = load_teach_config(str(fixture)) - assert config, "Failed to load config" - assert config['course']['number'] == "TEST 100", f"Wrong course code: {config['course']['number']}" - - # Step 3: Validate - errors = validate_config(config) - assert not errors, f"Validation errors: {errors}" - - # Step 4: Calculate progress - progress = calculate_semester_progress(str(fixture)) - assert progress, "Failed to calculate progress" - - -def test_e2e_full_course(): - """Test complete workflow with full course configuration.""" - fixture = get_fixture_path("stat-545") - - # Step 1: Detection - is_teaching, method = detect_teaching_mode(str(fixture)) - assert is_teaching, "Failed to detect full teaching project" - - # Step 2: Load config - config = load_teach_config(str(fixture)) - assert config, "Failed to load config" - - # Check optional sections - assert 'teaching_assistants' in config and len(config['teaching_assistants']) == 2, \ - f"Expected 2 TAs, got {len(config.get('teaching_assistants', []))}" - - # Step 3: Validate - errors = validate_config(config) - assert not errors, f"Validation errors: {errors}" - - # Step 4: Check break handling - breaks = config['dates'].get('breaks', []) - assert len(breaks) == 2, f"Expected 2 breaks, got {len(breaks)}" - - -def test_e2e_summer_session(): - """Test workflow with summer session (compressed, no breaks).""" - fixture = get_fixture_path("summer") - - # Detection - is_teaching, method = detect_teaching_mode(str(fixture)) - assert is_teaching, "Failed to detect summer teaching project" - - # Load config - config = load_teach_config(str(fixture)) - assert config, "Failed to load config" - - # Verify no breaks - breaks = config['dates'].get('breaks', []) - assert not breaks, "Summer session should have no breaks" - - # Validate - errors = validate_config(config) - assert not errors, f"Validation errors: {errors}" - - -# ─── Error Scenario Tests ───────────────────────────────────────────────────── - -def test_error_missing_config(): - """Test handling of missing teach-config.yml.""" - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - - # Create teaching project without config - (tmp_path / "syllabus.qmd").write_text("# Syllabus") - (tmp_path / "schedule.qmd").write_text("# Schedule") - - # Should detect as teaching - is_teaching, method = detect_teaching_mode(str(tmp_path)) - assert is_teaching, "Should detect teaching project even without config" - - # Config load should return None - config = load_teach_config(str(tmp_path)) - assert config is None, "Should return None for missing config" - - -def test_error_invalid_yaml(): - """Test handling of invalid YAML syntax.""" - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - - # Create config with truly invalid YAML syntax - config_content = """--- -course: - number: "TEST 100" - title: "Test Course - semester: Spring - year: 2026 - invalid_indent: - broken: true -""" - (tmp_path / "teach-config.yml").write_text(config_content) - - # Should fail to load due to malformed YAML - config = load_teach_config(str(tmp_path)) - assert config is None, "Should return None for invalid YAML" - - -def test_error_missing_required_fields(): - """Test validation of missing required fields.""" - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - - # Create config missing required fields - # Has 'course' but missing 'number', 'semester', 'year' - # Has 'dates' but missing 'end' - config_content = """--- -course: - title: "Incomplete Course" - # Missing number, semester, year - -dates: - start: "2026-01-01" - # Missing end -""" - (tmp_path / "teach-config.yml").write_text(config_content) - - try: - config = load_teach_config(str(tmp_path)) - except ValueError: - # load_teach_config raises ValueError on validation failure - return # Test passes - - if not config: - # OK if it fails to load - return # Test passes - - # If it loads, validation should fail - errors = validate_config(config) - assert errors, "Should report errors for missing required fields" - - -# ─── Edge Case Tests ────────────────────────────────────────────────────────── - -def test_edge_before_semester(): - """Test progress calculation before semester starts.""" - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - - # Create future course - future_start = (datetime.now() + timedelta(days=30)).strftime("%Y-%m-%d") - future_end = (datetime.now() + timedelta(days=150)).strftime("%Y-%m-%d") - - config_content = f"""--- -course: - number: "TEST 100" - title: "Future Course" - semester: "Spring" - year: 2027 - -dates: - start: "{future_start}" - end: "{future_end}" -""" - (tmp_path / "teach-config.yml").write_text(config_content) - - progress = calculate_semester_progress(str(tmp_path)) - assert progress, "Failed to calculate progress for future course" - - # Week should be 0 or negative, percentage 0 - assert progress.get('current_week', 1) <= 0, \ - f"Current week should be <=0, got {progress.get('current_week')}" - - -def test_edge_after_semester(): - """Test progress calculation after semester ends.""" - with tempfile.TemporaryDirectory() as tmpdir: - tmp_path = Path(tmpdir) - - # Create past course - past_start = (datetime.now() - timedelta(days=150)).strftime("%Y-%m-%d") - past_end = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") - - config_content = f"""--- -course: - number: "TEST 100" - title: "Past Course" - semester: "Fall" - year: 2025 - -dates: - start: "{past_start}" - end: "{past_end}" -""" - (tmp_path / "teach-config.yml").write_text(config_content) - - progress = calculate_semester_progress(str(tmp_path)) - assert progress, "Failed to calculate progress for past course" - - # Should indicate semester is over via percent_complete == 100 - percent = progress.get('percent_complete', 0) - current_week = progress.get('current_week', 0) - total_weeks = progress.get('total_weeks', 0) - assert percent >= 100.0 and current_week == total_weeks, \ - f"Should be 100% complete, got {percent}% (week {current_week}/{total_weeks})" - - -# ─── Performance Benchmarks ─────────────────────────────────────────────────── - -def test_benchmark_detection(): - """Benchmark: Teaching mode detection should be < 100ms.""" - fixture = get_fixture_path("stat-545") - - start = time.time() - is_teaching, method = detect_teaching_mode(str(fixture)) - duration = (time.time() - start) * 1000 - - assert is_teaching, "Failed to detect teaching project" - - target_ms = 100 - assert duration < target_ms, f"{duration:.2f}ms (target: <{target_ms}ms)" - - -def test_benchmark_config_parsing(): - """Benchmark: Config parsing should be < 200ms.""" - fixture = get_fixture_path("stat-545") - - start = time.time() - config = load_teach_config(str(fixture)) - duration = (time.time() - start) * 1000 - - assert config, "Failed to load config" - - target_ms = 200 - assert duration < target_ms, f"{duration:.2f}ms (target: <{target_ms}ms)" - - -def test_benchmark_validation(): - """Benchmark: Full validation should be < 5s.""" - fixture = get_fixture_path("stat-545") - - start = time.time() - results = validate_teaching_content(str(fixture)) - duration = (time.time() - start) * 1000 - - assert results, "Validation failed to return results" - - target_ms = 5000 - assert duration < target_ms, f"{duration:.2f}ms (target: <{target_ms}ms)" - - -def test_benchmark_progress(): - """Benchmark: Progress calculation should be < 100ms.""" - fixture = get_fixture_path("stat-545") - - start = time.time() - progress = calculate_semester_progress(str(fixture)) - duration = (time.time() - start) * 1000 - - assert progress, "Failed to calculate progress" - - target_ms = 100 - assert duration < target_ms, f"{duration:.2f}ms (target: <{target_ms}ms)" diff --git a/tests/test_teaching_mode.py b/tests/test_teaching_mode.py deleted file mode 100644 index 902bc5900..000000000 --- a/tests/test_teaching_mode.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/env python3 -""" -Unit tests for teaching mode detection utility. - -Tests all three detection strategies with priority ordering: -1. Config file detection -2. Metadata detection -3. Structure detection - -Run with: python3 -m pytest tests/test_teaching_mode.py -v -""" - -import os -import sys -import tempfile -import shutil -from pathlib import Path - -import pytest - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -pytestmark = [pytest.mark.unit, pytest.mark.teaching] - -from utils.detect_teaching_mode import detect_teaching_mode - - -class TestTeachingModeDetection: - """Test suite for teaching mode detection""" - - @pytest.fixture - def temp_project(self): - """Create a temporary project directory""" - temp_dir = tempfile.mkdtemp() - yield Path(temp_dir) - shutil.rmtree(temp_dir) - - def test_config_detection_priority_1(self, temp_project): - """Test Priority 1: .flow/teach-config.yml detection""" - # Create .flow/teach-config.yml - flow_dir = temp_project / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text("course: STAT 440\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is True - assert method == "config" - - def test_metadata_detection_priority_2(self, temp_project): - """Test Priority 2: _quarto.yml teaching: true detection""" - # Create _quarto.yml with teaching: true - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("teaching: true\nproject:\n type: website\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is True - assert method == "metadata" - - def test_structure_detection_priority_3_with_dir(self, temp_project): - """Test Priority 3: Structure detection with syllabus/ directory""" - # Create syllabus/ directory - syllabus_dir = temp_project / "syllabus" - syllabus_dir.mkdir() - - # Create schedule.qmd - schedule_file = temp_project / "schedule.qmd" - schedule_file.write_text("# Course Schedule\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is True - assert method == "structure" - - def test_structure_detection_priority_3_with_file(self, temp_project): - """Test Priority 3: Structure detection with syllabus.qmd file""" - # Create syllabus.qmd file - syllabus_file = temp_project / "syllabus.qmd" - syllabus_file.write_text("# Course Syllabus\n") - - # Create schedule.qmd - schedule_file = temp_project / "schedule.qmd" - schedule_file.write_text("# Course Schedule\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is True - assert method == "structure" - - def test_priority_order_config_over_metadata(self, temp_project): - """Test that config detection takes priority over metadata""" - # Create both config and metadata - flow_dir = temp_project / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text("course: STAT 440\n") - - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("teaching: true\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - # Should detect via config (Priority 1), not metadata - assert is_teaching is True - assert method == "config" - - def test_priority_order_config_over_structure(self, temp_project): - """Test that config detection takes priority over structure""" - # Create config, syllabus, and schedule - flow_dir = temp_project / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text("course: STAT 440\n") - - syllabus_dir = temp_project / "syllabus" - syllabus_dir.mkdir() - - schedule_file = temp_project / "schedule.qmd" - schedule_file.write_text("# Schedule\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - # Should detect via config (Priority 1) - assert is_teaching is True - assert method == "config" - - def test_priority_order_metadata_over_structure(self, temp_project): - """Test that metadata detection takes priority over structure""" - # Create metadata, syllabus, and schedule - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("teaching: true\n") - - syllabus_dir = temp_project / "syllabus" - syllabus_dir.mkdir() - - schedule_file = temp_project / "schedule.qmd" - schedule_file.write_text("# Schedule\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - # Should detect via metadata (Priority 2) - assert is_teaching is True - assert method == "metadata" - - def test_no_teaching_mode_detected(self, temp_project): - """Test negative case: no teaching mode indicators""" - # Create a non-teaching project (e.g., research project) - readme = temp_project / "README.md" - readme.write_text("# Research Project\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is False - assert method is None - - def test_structure_missing_schedule(self, temp_project): - """Test structure detection fails without schedule.qmd""" - # Create only syllabus directory, no schedule - syllabus_dir = temp_project / "syllabus" - syllabus_dir.mkdir() - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is False - assert method is None - - def test_structure_missing_syllabus(self, temp_project): - """Test structure detection fails without syllabus""" - # Create only schedule.qmd, no syllabus - schedule_file = temp_project / "schedule.qmd" - schedule_file.write_text("# Schedule\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is False - assert method is None - - def test_metadata_teaching_false(self, temp_project): - """Test that teaching: false in metadata is not detected""" - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("teaching: false\nproject:\n type: website\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is False - assert method is None - - def test_metadata_teaching_string_true(self, temp_project): - """Test that teaching: 'true' (string) is not detected as true""" - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("teaching: 'true'\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - # YAML should parse 'true' as string, not boolean - # Only boolean true should be detected - assert is_teaching is False - assert method is None - - def test_metadata_no_teaching_field(self, temp_project): - """Test _quarto.yml without teaching field""" - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("project:\n type: website\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is False - assert method is None - - def test_default_cwd(self, temp_project, monkeypatch): - """Test that default cwd='.' uses current directory""" - # Change to temp project directory - monkeypatch.chdir(temp_project) - - # Create config file - flow_dir = temp_project / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text("course: TEST\n") - - # Call without arguments (should use current directory) - is_teaching, method = detect_teaching_mode() - - assert is_teaching is True - assert method == "config" - - def test_invalid_yaml_in_quarto(self, temp_project): - """Test graceful handling of invalid YAML in _quarto.yml""" - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("teaching: true\n invalid: yaml: structure:\n") - - # Should not crash, should return False - is_teaching, method = detect_teaching_mode(str(temp_project)) - - # Result depends on YAML parser behavior - # Either detects or fails gracefully - assert isinstance(is_teaching, bool) - assert method in ["metadata", None] - - def test_empty_quarto_file(self, temp_project): - """Test empty _quarto.yml file""" - quarto_file = temp_project / "_quarto.yml" - quarto_file.write_text("") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is False - assert method is None - - def test_nonexistent_directory(self): - """Test with nonexistent directory path""" - nonexistent = "/tmp/nonexistent-project-xyz123" - - is_teaching, method = detect_teaching_mode(nonexistent) - - # Should handle gracefully - assert is_teaching is False - assert method is None - - -class TestEdgeCases: - """Test edge cases and error handling""" - - @pytest.fixture - def temp_project(self): - """Create a temporary project directory""" - temp_dir = tempfile.mkdtemp() - yield Path(temp_dir) - shutil.rmtree(temp_dir) - - def test_symlink_to_syllabus(self, temp_project): - """Test that symlinks to syllabus directory work""" - # Create actual syllabus directory elsewhere - real_syllabus = temp_project / "real_syllabus" - real_syllabus.mkdir() - - # Create symlink - syllabus_link = temp_project / "syllabus" - syllabus_link.symlink_to(real_syllabus) - - # Create schedule - schedule_file = temp_project / "schedule.qmd" - schedule_file.write_text("# Schedule\n") - - is_teaching, method = detect_teaching_mode(str(temp_project)) - - assert is_teaching is True - assert method == "structure" - - def test_relative_path_input(self, temp_project, monkeypatch): - """Test with relative path input""" - # Change to parent of temp_project - parent = temp_project.parent - monkeypatch.chdir(parent) - - # Create config - flow_dir = temp_project / ".flow" - flow_dir.mkdir() - config_file = flow_dir / "teach-config.yml" - config_file.write_text("course: TEST\n") - - # Use relative path - rel_path = temp_project.name - is_teaching, method = detect_teaching_mode(rel_path) - - assert is_teaching is True - assert method == "config" - - -class TestIntegration: - """Integration tests with realistic project structures""" - - @pytest.fixture - def realistic_teaching_project(self): - """Create a realistic teaching project""" - temp_dir = tempfile.mkdtemp() - project = Path(temp_dir) - - # Create typical teaching project structure - (project / ".flow").mkdir() - (project / ".flow" / "teach-config.yml").write_text("course: STAT 440\n") - - (project / "_quarto.yml").write_text( - "project:\n" - " type: website\n" - "teaching: true\n" - ) - - (project / "syllabus").mkdir() - (project / "syllabus" / "index.qmd").write_text("# Syllabus\n") - - (project / "schedule.qmd").write_text("# Schedule\n") - - (project / "lectures").mkdir() - (project / "assignments").mkdir() - - yield project - shutil.rmtree(temp_dir) - - def test_realistic_teaching_project(self, realistic_teaching_project): - """Test detection on realistic teaching project""" - is_teaching, method = detect_teaching_mode(str(realistic_teaching_project)) - - assert is_teaching is True - # Should detect via config (highest priority) - assert method == "config" - - -if __name__ == "__main__": - # Run tests with pytest - pytest.main([__file__, "-v", "--tb=short"]) diff --git a/tests/test_teaching_validation.py b/tests/test_teaching_validation.py deleted file mode 100644 index e6b615b92..000000000 --- a/tests/test_teaching_validation.py +++ /dev/null @@ -1,666 +0,0 @@ -"""Unit tests for teaching content validation. - -Tests all validation functions with mock course structures: -- Syllabus validation (complete and incomplete) -- Schedule validation (complete and with gaps) -- Assignment validation (all exist and some missing) -- ValidationResult logic and formatting -""" - -import os -import sys -import tempfile -import unittest -from pathlib import Path - -import pytest - -# Add parent directory to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent)) - -pytestmark = [pytest.mark.unit, pytest.mark.teaching] - -from commands.utils.teaching_validation import ( - ValidationResult, - validate_syllabus, - validate_schedule, - validate_assignments, - validate_teaching_content, -) - - -class TestValidationResult(unittest.TestCase): - """Test ValidationResult dataclass.""" - - def test_can_publish_no_errors(self): - """Should allow publish when no errors.""" - result = ValidationResult( - valid=True, - errors=[], - warnings=["Some warning"], - checks={"test": True} - ) - self.assertTrue(result.can_publish()) - - def test_can_publish_with_errors(self): - """Should block publish when errors exist.""" - result = ValidationResult( - valid=False, - errors=["Critical error"], - warnings=[], - checks={"test": False} - ) - self.assertFalse(result.can_publish()) - - def test_format_report_success(self): - """Should format success report correctly.""" - result = ValidationResult( - valid=True, - errors=[], - warnings=["Minor issue"], - checks={ - "Check 1": True, - "Check 2": True, - } - ) - report = result.format_report() - - self.assertIn("READY TO PUBLISH", report) - self.assertIn("2/2 checks passed", report) - self.assertIn("Minor issue", report) - self.assertIn("✓", report) - - def test_format_report_errors(self): - """Should format error report correctly.""" - result = ValidationResult( - valid=False, - errors=["Error 1", "Error 2"], - warnings=["Warning 1"], - checks={ - "Check 1": True, - "Check 2": False, - } - ) - report = result.format_report() - - self.assertIn("BLOCKED", report) - self.assertIn("Error 1", report) - self.assertIn("Error 2", report) - self.assertIn("Warning 1", report) - self.assertIn("2 error(s) blocking", report) - - -class TestValidateSyllabus(unittest.TestCase): - """Test syllabus validation.""" - - def setUp(self): - """Create temporary directory for tests.""" - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_no_syllabus(self): - """Should return all False when no syllabus exists.""" - result = validate_syllabus(self.test_dir) - - self.assertFalse(result['grading']) - self.assertFalse(result['policies']) - self.assertFalse(result['objectives']) - self.assertFalse(result['schedule']) - - def test_complete_syllabus(self): - """Should find all sections in complete syllabus.""" - syllabus_content = """ -# Course Syllabus - -## Learning Objectives -Students will learn statistical methods. - -## Course Policies -Attendance is required. - -## Grading -- Homework: 40% -- Exams: 60% - -## Schedule -Week 1: Introduction - """ - - syllabus_path = Path(self.test_dir) / 'syllabus.qmd' - syllabus_path.write_text(syllabus_content) - - result = validate_syllabus(self.test_dir) - - self.assertTrue(result['grading']) - self.assertTrue(result['policies']) - self.assertTrue(result['objectives']) - self.assertTrue(result['schedule']) - - def test_partial_syllabus(self): - """Should detect missing sections.""" - syllabus_content = """ -# Course Syllabus - -## Grading Policy -Homework counts for everything. - -## Learning Goals -Learn statistics. - """ - - syllabus_path = Path(self.test_dir) / 'syllabus.qmd' - syllabus_path.write_text(syllabus_content) - - result = validate_syllabus(self.test_dir) - - self.assertTrue(result['grading']) - self.assertTrue(result['objectives']) - self.assertFalse(result['policies']) - self.assertFalse(result['schedule']) - - def test_syllabus_subdirectory(self): - """Should find syllabus in subdirectory.""" - syllabus_dir = Path(self.test_dir) / 'syllabus' - syllabus_dir.mkdir() - - syllabus_content = """ -# Syllabus - -## Assessment -Graded on participation. - """ - - (syllabus_dir / 'index.qmd').write_text(syllabus_content) - - result = validate_syllabus(self.test_dir) - - self.assertTrue(result['grading']) - - def test_case_insensitive_matching(self): - """Should match section headers case-insensitively.""" - syllabus_content = """ -# SYLLABUS - -## GRADING POLICY -All caps should work. - -## course POLICIES -Mixed case too. - """ - - syllabus_path = Path(self.test_dir) / 'syllabus.qmd' - syllabus_path.write_text(syllabus_content) - - result = validate_syllabus(self.test_dir) - - self.assertTrue(result['grading']) - self.assertTrue(result['policies']) - - -class TestValidateSchedule(unittest.TestCase): - """Test schedule validation.""" - - def setUp(self): - """Create temporary directory for tests.""" - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_no_schedule(self): - """Should return empty result when no schedule exists.""" - result = validate_schedule(self.test_dir) - - self.assertEqual(result['total_weeks'], 0) - self.assertEqual(result['complete_weeks'], 0) - self.assertEqual(result['gaps'], []) - - def test_complete_schedule(self): - """Should validate complete schedule with no gaps.""" - schedule_content = """ -# Course Schedule - -## Week 1 -Introduction to statistics. Read Chapter 1. -Assignment: HW 1 due Friday. - -## Week 2 -Descriptive statistics. Read Chapter 2. -Assignment: HW 2 due next Friday. - -## Week 3 -Probability basics. Read Chapter 3. -Quiz on Friday covering Weeks 1-2. - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - result = validate_schedule(self.test_dir) - - self.assertEqual(result['total_weeks'], 3) - self.assertEqual(result['complete_weeks'], 3) - self.assertEqual(result['gaps'], []) - - def test_schedule_with_gaps(self): - """Should detect weeks with missing content.""" - schedule_content = """ -# Course Schedule - -## Week 1 -Introduction to statistics. Read Chapter 1. - -## Week 2 - -## Week 3 -Probability basics. Read Chapter 3. - -## Week 4 - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - result = validate_schedule(self.test_dir) - - self.assertEqual(result['total_weeks'], 4) - self.assertEqual(result['complete_weeks'], 2) - self.assertIn(2, result['gaps']) - self.assertIn(4, result['gaps']) - - def test_flexible_week_headers(self): - """Should match various week header formats.""" - schedule_content = """ -# Schedule - -## Week 1 -Content here that is long enough to pass validation. - -### Week 2: -More content that is also long enough to pass. - -#### Week 3 - Topic Name -Even more content with sufficient length to pass validation. - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - result = validate_schedule(self.test_dir) - - self.assertEqual(result['total_weeks'], 3) - self.assertEqual(result['complete_weeks'], 3) - - def test_schedule_subdirectory(self): - """Should find schedule in subdirectory.""" - schedule_dir = Path(self.test_dir) / 'schedule' - schedule_dir.mkdir() - - schedule_content = """ -## Week 1 -Some content here that is long enough to count. - """ - - (schedule_dir / 'index.qmd').write_text(schedule_content) - - result = validate_schedule(self.test_dir) - - self.assertEqual(result['total_weeks'], 1) - self.assertEqual(result['complete_weeks'], 1) - - -class TestValidateAssignments(unittest.TestCase): - """Test assignment validation.""" - - def setUp(self): - """Create temporary directory for tests.""" - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_no_schedule(self): - """Should return empty result when no schedule exists.""" - result = validate_assignments(self.test_dir) - - self.assertEqual(result['referenced'], []) - self.assertEqual(result['missing'], []) - self.assertEqual(result['found'], []) - - def test_all_assignments_exist(self): - """Should find all referenced assignments.""" - schedule_content = """ -# Schedule - -## Week 1 -Complete HW 1 by Friday. - -## Week 2 -Submit Assignment 2 next week. - -## Week 3 -Homework 3 due soon. - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - # Create assignment files - assignments_dir = Path(self.test_dir) / 'assignments' - assignments_dir.mkdir() - - (assignments_dir / 'hw-1.qmd').write_text("# HW 1") - (assignments_dir / 'hw-2.qmd').write_text("# HW 2") - (assignments_dir / 'hw-3.qmd').write_text("# HW 3") - - result = validate_assignments(self.test_dir) - - self.assertEqual(len(result['referenced']), 3) - self.assertIn("HW 1", result['referenced']) - self.assertIn("HW 2", result['referenced']) - self.assertIn("HW 3", result['referenced']) - self.assertEqual(len(result['found']), 3) - self.assertEqual(len(result['missing']), 0) - - def test_some_assignments_missing(self): - """Should detect missing assignment files.""" - schedule_content = """ -# Schedule - -## Week 1 -HW 1 due Friday. - -## Week 2 -HW 2 due Friday. - -## Week 3 -HW 3 due Friday. - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - # Create only HW 1 and HW 3 - assignments_dir = Path(self.test_dir) / 'assignments' - assignments_dir.mkdir() - - (assignments_dir / 'hw-1.qmd').write_text("# HW 1") - (assignments_dir / 'hw-3.qmd').write_text("# HW 3") - - result = validate_assignments(self.test_dir) - - self.assertEqual(len(result['referenced']), 3) - self.assertEqual(len(result['found']), 2) - self.assertIn("HW 2", result['missing']) - - def test_various_assignment_formats(self): - """Should match different assignment reference formats.""" - schedule_content = """ -# Schedule - -Week 1: HW1 due -Week 2: HW 2 due -Week 3: Assignment 3 due -Week 4: Homework 4 due - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - result = validate_assignments(self.test_dir) - - self.assertEqual(len(result['referenced']), 4) - # All should be missing since we didn't create files - self.assertEqual(len(result['missing']), 4) - - def test_assignment_subdirectories(self): - """Should find assignments in subdirectories.""" - schedule_content = """ -HW 1 is available. - """ - - schedule_path = Path(self.test_dir) / 'schedule.qmd' - schedule_path.write_text(schedule_content) - - # Create assignment in subdirectory - assignments_dir = Path(self.test_dir) / 'assignments' / 'hw-1' - assignments_dir.mkdir(parents=True) - (assignments_dir / 'index.qmd').write_text("# HW 1") - - result = validate_assignments(self.test_dir) - - self.assertIn("HW 1", result['found']) - - -class TestValidateTeachingContent(unittest.TestCase): - """Test comprehensive teaching content validation.""" - - def setUp(self): - """Create temporary directory for tests.""" - self.test_dir = tempfile.mkdtemp() - - def tearDown(self): - """Clean up temporary directory.""" - import shutil - shutil.rmtree(self.test_dir) - - def test_complete_valid_course(self): - """Should pass validation for complete course.""" - # Create complete syllabus - syllabus_content = """ -# Syllabus - -## Learning Objectives -Students will master statistics. - -## Course Policies -Be respectful and on time. - -## Grading -Homework 50%, Exams 50%. - -## Schedule -See full schedule page. - """ - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - # Create complete schedule - schedule_content = """ -## Week 1 -Introduction. Read Chapter 1. HW 1 due Friday. - -## Week 2 -More content. Read Chapter 2. HW 2 due Friday. - """ - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - # Create assignments - assignments_dir = Path(self.test_dir) / 'assignments' - assignments_dir.mkdir() - (assignments_dir / 'hw-1.qmd').write_text("# HW 1") - (assignments_dir / 'hw-2.qmd').write_text("# HW 2") - - result = validate_teaching_content(self.test_dir) - - self.assertTrue(result.can_publish()) - self.assertEqual(len(result.errors), 0) - # Warnings allowed (none in this case) - self.assertTrue(result.valid) - - def test_missing_syllabus_sections(self): - """Should error on missing syllabus sections.""" - # Incomplete syllabus - syllabus_content = """ -# Syllabus - -## Grading -Homework counts. - """ - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - # Complete schedule - schedule_content = """ -## Week 1 -Content here that is long enough. - """ - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - - self.assertFalse(result.can_publish()) - self.assertGreater(len(result.errors), 0) - self.assertIn("Syllabus missing required sections", result.errors[0]) - - def test_incomplete_schedule(self): - """Should error on incomplete schedule weeks.""" - # Complete syllabus - syllabus_content = """ -# Syllabus - -## Objectives -Learn stats. - -## Policies -Attendance required. - -## Grading -Homework 100%. - -## Schedule -See schedule page. - """ - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - # Incomplete schedule - schedule_content = """ -## Week 1 -Good content here. - -## Week 2 - -## Week 3 -More good content. - """ - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - - self.assertFalse(result.can_publish()) - self.assertGreater(len(result.errors), 0) - self.assertTrue(any("incomplete weeks" in e for e in result.errors)) - - def test_missing_assignments_warning(self): - """Should warn (not error) on missing assignments.""" - # Complete syllabus - syllabus_content = """ -# Syllabus - -## Objectives -Learn. - -## Policies -Rules. - -## Grading -Grades. - -## Schedule -Times. - """ - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - # Schedule referencing assignments - schedule_content = """ -## Week 1 -Complete HW 1 by Friday. Lots of content here. - -## Week 2 -Complete HW 2 by Friday. More content here. - """ - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - # Don't create assignment files - - result = validate_teaching_content(self.test_dir) - - # Should still be publishable (warnings don't block) - self.assertTrue(result.can_publish()) - # But should have warnings - self.assertGreater(len(result.warnings), 0) - self.assertTrue(any("Missing assignment files" in w for w in result.warnings)) - - def test_checks_populated(self): - """Should populate detailed checks dict.""" - # Minimal valid course - syllabus_content = """ -# Syllabus -## Objectives -Learn. -## Policies -Rules. -## Grading -Grades. -## Schedule -Times. - """ - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - schedule_content = """ -## Week 1 -Content with sufficient length to pass validation. - """ - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - - # Should have checks for syllabus sections - self.assertIn('Syllabus: grading', result.checks) - self.assertIn('Syllabus: policies', result.checks) - self.assertIn('Syllabus: objectives', result.checks) - self.assertIn('Syllabus: schedule', result.checks) - - # Should have schedule checks - self.assertIn('Schedule: exists', result.checks) - - def test_format_report(self): - """Should generate formatted report.""" - # Course with one error and one warning - syllabus_content = """ -# Syllabus -## Grading -Grades. - """ - (Path(self.test_dir) / 'syllabus.qmd').write_text(syllabus_content) - - schedule_content = """ -## Week 1 -HW 1 due Friday. Good content here. - """ - (Path(self.test_dir) / 'schedule.qmd').write_text(schedule_content) - - result = validate_teaching_content(self.test_dir) - report = result.format_report() - - # Should be blocked - self.assertIn("BLOCKED", report) - - # Should show errors - self.assertIn("ERRORS", report) - - # Should show warnings - self.assertIn("WARNINGS", report) - - # Should show checks - self.assertIn("DETAILED CHECKS", report) - - -if __name__ == '__main__': - unittest.main() diff --git a/utils/detect_teaching_mode.py b/utils/detect_teaching_mode.py deleted file mode 100644 index 2dfbd1ee8..000000000 --- a/utils/detect_teaching_mode.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python3 -""" -Teaching Mode Detection Utility for Craft Commands - -Detects whether a project is in teaching mode using multiple strategies with -priority-based fallback. Used by workflow commands to determine context. - -Detection Priority: - 1. Config file (.flow/teach-config.yml) - 2. Metadata (_quarto.yml teaching: true) - 3. Project structure (syllabus/ + schedule.qmd) - -Usage: - from utils.detect_teaching_mode import detect_teaching_mode - - is_teaching, method = detect_teaching_mode() - if is_teaching: - print(f"Teaching mode detected via: {method}") - else: - print("Not in teaching mode") - - # Or with custom directory - is_teaching, method = detect_teaching_mode("/path/to/project") -""" - -import os -from pathlib import Path -from typing import Tuple, Optional - -try: - import yaml - YAML_AVAILABLE = True -except ImportError: - YAML_AVAILABLE = False - - -def detect_teaching_mode(cwd: str = ".") -> Tuple[bool, Optional[str]]: - """ - Detect if current project is in teaching mode. - - Uses a priority-based detection system: - 1. Config file: Check for .flow/teach-config.yml - 2. Metadata: Check _quarto.yml for teaching: true - 3. Structure: Check for syllabus/ (or syllabus.qmd) AND schedule.qmd - - Args: - cwd: Working directory to check (default: current directory) - - Returns: - Tuple of (is_teaching: bool, detection_method: str | None) - - is_teaching: True if teaching mode detected - - detection_method: One of "config", "metadata", "structure", or None - - Examples: - >>> # Project with .flow/teach-config.yml - >>> is_teaching, method = detect_teaching_mode("/path/to/course") - >>> print(is_teaching, method) - True config - - >>> # Project with _quarto.yml teaching: true - >>> is_teaching, method = detect_teaching_mode("/path/to/course") - >>> print(is_teaching, method) - True metadata - - >>> # Project with syllabus/ and schedule.qmd - >>> is_teaching, method = detect_teaching_mode("/path/to/course") - >>> print(is_teaching, method) - True structure - - >>> # Non-teaching project - >>> is_teaching, method = detect_teaching_mode("/path/to/research") - >>> print(is_teaching, method) - False None - """ - project_path = Path(cwd).resolve() - - # Priority 1: Check for .flow/teach-config.yml - config_file = project_path / ".flow" / "teach-config.yml" - if config_file.exists(): - return (True, "config") - - # Priority 2: Check _quarto.yml for teaching: true - quarto_file = project_path / "_quarto.yml" - if quarto_file.exists(): - if _check_quarto_metadata(quarto_file): - return (True, "metadata") - - # Priority 3: Check project structure - if _check_project_structure(project_path): - return (True, "structure") - - return (False, None) - - -def _check_quarto_metadata(quarto_file: Path) -> bool: - """ - Check if _quarto.yml contains teaching: true. - - Args: - quarto_file: Path to _quarto.yml file - - Returns: - True if teaching: true is found, False otherwise - """ - if not YAML_AVAILABLE: - # Fallback: Simple text search if PyYAML not available - try: - content = quarto_file.read_text() - # Look for "teaching: true" (with or without quotes) - return "teaching:" in content and "true" in content.lower() - except Exception: - return False - - try: - with open(quarto_file, 'r') as f: - data = yaml.safe_load(f) - if data and isinstance(data, dict): - return data.get('teaching', False) is True - except Exception: - return False - - return False - - -def _check_project_structure(project_path: Path) -> bool: - """ - Check if project has teaching structure. - - Requires BOTH: - - syllabus/ directory OR syllabus.qmd file - - schedule.qmd file - - Args: - project_path: Path to project directory - - Returns: - True if both conditions are met, False otherwise - """ - # Check for syllabus/ directory or syllabus.qmd file - has_syllabus = ( - (project_path / "syllabus").is_dir() or - (project_path / "syllabus.qmd").is_file() - ) - - # Check for schedule.qmd file - has_schedule = (project_path / "schedule.qmd").is_file() - - return has_syllabus and has_schedule - - -# Example usage and testing -if __name__ == "__main__": - import sys - - # Test current directory - is_teaching, method = detect_teaching_mode() - print(f"Current directory:") - print(f" Teaching mode: {is_teaching}") - print(f" Detection method: {method}") - - # Test with command line argument - if len(sys.argv) > 1: - test_path = sys.argv[1] - is_teaching, method = detect_teaching_mode(test_path) - print(f"\nTest directory: {test_path}") - print(f" Teaching mode: {is_teaching}") - print(f" Detection method: {method}")