From fcb02796fd311c5c370a644d9fc1761fc4b57126 Mon Sep 17 00:00:00 2001 From: Andy Eschbacher Date: Fri, 10 Jul 2026 18:10:32 -0400 Subject: [PATCH] chore: task movements --- ...ose-frontmatter-schema-policy-questions.md | 249 ++++++++++++++++++ ...-audits-into-the-top-level-lint-command.md | 81 ++++++ ...nd-fix-rename-via-a-bounded-alias-table.md | 89 +++++++ 3 files changed, 419 insertions(+) create mode 100644 planning/tasks/6fm8p1cj11qf-fold-audits-into-the-top-level-lint-command.md create mode 100644 planning/tasks/6fmvcgpkyh3e-alias-lint-warn-flag-legacy-field-names-and-fix-rename-via-a-bounded-alias-table.md diff --git a/planning/tasks/6fkkz41cax80-adr-close-frontmatter-schema-policy-questions.md b/planning/tasks/6fkkz41cax80-adr-close-frontmatter-schema-policy-questions.md index 6ecfe58..33561eb 100644 --- a/planning/tasks/6fkkz41cax80-adr-close-frontmatter-schema-policy-questions.md +++ b/planning/tasks/6fkkz41cax80-adr-close-frontmatter-schema-policy-questions.md @@ -22,6 +22,255 @@ truth (replacing today's triplicated, drift-prone split). Output: an ADR (the next number after 0003/0004) recording the decisions, plus this epic's open questions resolved or explicitly deferred. +## Survey — fill this out + +Andy fills this in; the answers become the ADR. For each question: tick a box +with `[x]` (options are mutually exclusive unless it says *pick any*), and use +**Notes** for the "why", caveats, or a different answer. `*(current lean)*` marks +where epic 26's body already leans — override freely. `*(load-bearing)*` marks +the five the ADR must decide (Q1, Q2, Q4, Q6, Q8); the other seven may be +decided *or* explicitly parked with a reason. "Park" = `[x] Park` + a one-line why. + +--- + +### Q1 — Per-status strictness matrix *(load-bearing)* + +How strict should archived/deprecated tasks be? Today active tasks get field +nags; archived get only the universal checks (misfiled / missing-id / missing- +frontmatter). + +- [ ] Per-field classification: **required-always / required-for-active / optional** *(current lean)* +- [ ] Keep today's binary split (active = full nags, archived = universal only) +- [ ] Fully strict — every known field required regardless of status + +Sub-question — is `created` **required-always** (it seeds a stable id) while +`tier`/`effort` stay active-only?   [ ] yes   [ ] no + +> Coupling: epic 24 (status-in-frontmatter) reshapes this axis — coordinate before finalizing. + +**Notes:** + +--- + +### Q2 — Unknown / custom fields *(load-bearing)* + +Preserve-and-ignore (today) vs a closed registry vs a namespaced escape hatch. +Surgical edits currently rely on unknown fields surviving untouched. + +- [ ] Preserve-and-ignore unknown fields (current behavior) +- [ ] Closed registry — flag any unrecognized field as a problem +- [ ] Namespaced escape hatch — unknown allowed only under `x-*`, others flagged +- [ ] Closed registry **plus** `x-*` escape hatch + +Sub-question — is `deprecated_date` (vs recognized `deprecated_at`) an *unknown +field* or a *misspelled known field* (→ handled as an alias, Q3)?   [ ] unknown   [ ] misspelled-known + +**Notes:** *(draft 2026-07-10 — boxes left for you)* `deprecated_date` reads as a +**misspelled known field** → handled as an alias (Q3), not a genuine unknown +field. Leaning Q2 → *closed registry + `x-*` escape hatch*: `x-*` is the +sanctioned preserve-and-ignore zone (forward-compat + human scratch), so a bare +unrecognized key *outside* `x-*` is suspicious-by-default (probably a typo). That +split also gives Q6 its cut: unknown-in-`x-*` → at most `info`; bare unrecognized +key → `warn`; only structural/identity violations → `error`. + +--- + +### Q3 — Aliases & field migration + +How to treat legacy field names under `--fix`. + +- [ ] Leave alone — never rewrite +- [ ] Warn-only (lint flags, `--fix` does not touch) +- [ ] Auto-migrate under `--fix` — the *one* sanctioned semantic rewrite, bounded to a fixed alias table +- [ ] Park + +**Notes (if auto-migrate: what bounds it?):** *(leaning auto-migrate, decided in +chat 2026-07-10 — box left for you)* Bound to a **fixed alias table** (seed: +`deprecated_date → deprecated_at`). This is the *one* sanctioned semantic rewrite; +it lives in `--fix` (and a future `migrate`), never in a general repairer — so +contract C holds. Framing worth recording: entities already have "tag numbers" +(task/audit opaque `id`, epic `NN`) that decouple identity from the renameable +slug, which is why entity renames are safe. Frontmatter **keys have no such tag +layer**, so a key rename is breaking — the alias table *is* the field-level +equivalent of that entity-id decoupling. + +--- + +### Q4 — Where the schema lives & its relation to `schema --json-schema` *(load-bearing)* + +Two sub-decisions. + +**(a) Source of truth:** +- [ ] Go structs + tags (code-declared) *(current lean)* +- [ ] Checked-in YAML/JSON descriptor +- [ ] Go-declared, with a descriptor generated as a build artifact + +**(b) Frontmatter schema vs the existing envelope JSON-schema:** +- [ ] Two clearly-named schemas, kept separate (envelope stays; a *frontmatter* JSON-schema joins it) +- [ ] Unify into one +- [ ] Frontmatter schema only for lint/authoring; do not emit a JSON-schema for it + +**Notes:** + +--- + +### Q5 — Types & value vocabularies + +Which fields get constrained, and how. Fill the ones you have an opinion on. + +| field | constraint | +|---|---| +| `priority` | [ ] closed enum → values: `__________`   [ ] freeform | +| `tier` | [ ] int range `1`–`5`   [ ] other: `______` | +| dates (`created`, `*_at`, …) | [ ] pattern `YYYY-MM-DD` (confirm) | +| `effort` | [ ] freeform, keep as-is   [ ] constrain → how: `__________` | +| `tags` | [ ] freeform   [ ] conventions: `__________` | + +- [ ] Park the whole question (types come with the validator, not this ADR) + +**Notes:** + +--- + +### Q6 — Severity levels *(load-bearing)* + +Does lint grow beyond binary valid/invalid? Adding severities touches the +`--json` contract and exit-code mapping. + +- [ ] Add `error | warn | info` *(current lean — unknown-field / alias / freeform-effort feel like warnings)* +- [ ] Keep binary (valid / invalid) + +If adding severities — which land as **warn** (not hard failure)? *(pick any)* +- [ ] unknown field   [ ] nonstandard alias   [ ] freeform `effort`   [ ] other: `______` + +**Notes (exit-code / `--json` impact):** + +--- + +### Q7 — Cross-field / referential rules + +Are these part of "the schema" or a separate pass? + +- [ ] Part of the declared schema +- [ ] A separate validation pass +- [ ] Park (out of scope for this ADR) + +Which rules are in scope now? *(pick any)* +- [ ] `epic` exists   [ ] pointer validity (`superseded_by` / `parent_task`)   [ ] `related_tasks` resolvable   [ ] `blocks` symmetry   [ ] date ordering (`created ≤ completed_at`) + +**Notes:** + +--- + +### Q8 — Backward-compatibility / rollout *(load-bearing)* + +Turning on strict always-validation lights up real existing debt (we just saw +nine such files in desirelines-planning). + +- [ ] Grandfather by date (files before cutoff exempt) +- [ ] Opt-in `lint --strict`; lenient default +- [ ] One-time `--fix`-then-enforce migration +- [ ] Strict-by-default immediately + +**Notes:** + +--- + +### Q9 — Entity coverage & sharing + +- [ ] One engine, per-entity schemas over a shared core (tasks/epics/audits) *(current lean)* +- [ ] Separate schema per entity + +Build on the existing `schema: 1` field as the version handle?   [ ] yes   [ ] no + +**Notes:** + +--- + +### Q10 — Schema versioning & evolution + +How a repo declares which schema version it targets; how the schema evolves +without breaking older repos. + +- [ ] Reuse the `schema:` frontmatter field as the version handle +- [ ] New dedicated field: `__________` +- [ ] Tied to the `tskflwctl` binary version (no per-repo declaration) +- [ ] Park for a later ADR + +Relation to the envelope `schema_version`: distinct handles — `schema:` versions +the **on-disk file shape**, `schema_version` versions the **`--json` output +contract**; neither drives the other (already true in `layout.go`). + +**Notes:** *(draft 2026-07-10)* **Park the migration framework** — YAGNI at +`schema: 1` (never bumped); the first breaking change is a one-off transform, not +a registry. Keep `schema:` **coarse** (bump only on a true reshape). Read-tolerance +is *already free*: the loader ignores `schema:` today, so old / `schema: 2` / +no-`schema:` files all read now — preserve that (on an unknown-higher version, +degrade + warn, never hard-fail). + +> **Schema change rules (fold into the ADR body):** (1) add fields freely, never +> bump `schema:`; (2) rename = alias-table entry + bounded `--fix`, never in +> place; (3) a type/semantic change of an existing key is a *new* field (or, if +> unavoidable, a `schema:` bump with reader branching); (4) retired field names +> are tombstoned and never reused — the enforced epic-NN duplicate guard already +> *is* this rule, at the entity level. This change-discipline, not a "perfect" +> schema, is the futureproofing. + +--- + +### Q11 — Authoring UX downstream + +- [ ] Generate `schema task` output **and** `task new` scaffolding from the declared schema, so guidance can't drift from enforcement *(current lean)* +- [ ] Keep them hand-maintained for now +- [ ] Park + +Should the loud "missing frontmatter" message point at generated valid-shape +content?   [ ] yes   [ ] no + +**Notes:** + +--- + +### Q12 — External planning repos (epic 23) + +Can a downstream impl/planning repo extend or override the schema? + +- [ ] Schema fixed by `tskflwctl` version — no override +- [ ] Downstream repos may extend (add custom fields / statuses) +- [ ] Park until epic 23 matures + +**Notes:** + +--- + +### C — Contract check (confirm, don't re-open) + +The "**fail loud on invalid frontmatter, describe the valid shape, auto-fix only +safe/unambiguous cases**" contract is preserved; `--fix` does **not** become a +general repairer.   [ ] confirmed + +**Notes (any exception, e.g. the Q3 alias case):** + +--- + +### D — Carveout amendment fold-in + +Fold the settled ADR-0003 entity-carveout amendment (filename-shape classifier, +stray files as `FileProblem`s, the `meta/` folder) into this epic's "valid +entity" formalization as settled input.   [ ] yes   [ ] revisit + +**Notes:** + +--- + +### E — ADR number & logistics + +Next number after 0003 / 0004 → **ADR-0005**.   [ ] confirm   [ ] other: `______` + +**Anything else / global overrides:** + +--- + ## Acceptance criteria - [ ] ADR drafted covering the load-bearing questions: per-status strictness diff --git a/planning/tasks/6fm8p1cj11qf-fold-audits-into-the-top-level-lint-command.md b/planning/tasks/6fm8p1cj11qf-fold-audits-into-the-top-level-lint-command.md new file mode 100644 index 0000000..8c56183 --- /dev/null +++ b/planning/tasks/6fm8p1cj11qf-fold-audits-into-the-top-level-lint-command.md @@ -0,0 +1,81 @@ +--- +schema: 1 +id: 6fm8p1cj11qf +status: ready-to-start +epic: 21-code-quality-architecture-hardening +description: Top-level `tskflwctl lint` checks tasks + epics but not audits (audit checks live only behind `audit lint`); fold audit lint into the lint roster so the hygiene gate covers all three entities. +effort: Unknown +tier: 3 +priority: medium +autonomy_level: 3 +tags: [lint, audit, core] +created: "2026-07-09" +--- +# Fold audits into the top-level `lint` command + +## The gap (researched 2026-07-09) + +`tskflwctl lint` — the canonical hygiene gate ("keep `planning/` lint-clean") — +validates **tasks and epics but not audits**. Its success line even says so: +`✔ all active tasks and epics pass lint`. Audit validation exists, but only +behind the *separate* `audit lint [audit]` subcommand, which a plain `lint` (and +any CI/hygiene run of it) never invokes. + +Two layers to the gap: + +### A. Wiring — audits are absent from the top-level roster + +`core.Service.Lint()` (`internal/core/service.go:216`) lists tasks + epics and +returns their issues; it never touches audits. So audit frontmatter problems +(missing/foreign `bucket`, missing/drifted `id`, and finding-level status-vocab / +bucket↔state violations) — all of which the code *already* detects via +`Service.LintAudits()` (`internal/core/finding.go:196`) — don't surface through +the command everyone actually runs. + +A sharper symptom of the split: `lint --fix` already walks audits and backfills +their ids (`internal/store/fix.go:16-18,88`), yet read-only `lint` never reports +an audit problem. The repairer sees audits; the reporter doesn't. + +### B. Coverage — no field-level `LintAudit` + +Even `LintAudits` only checks findings + id + bucket. There is no domain +`LintAudit(a)` analogous to `LintTask`/`LintEpic` that validates an audit's own +content frontmatter: `area`, `date`, `updated_at` (`internal/domain/audit.go`). +Tasks require `created` be present and `YYYY-MM-DD`; the audit `date` field gets +no presence or format check at all. This is exactly the archived-entity asymmetry +epic 26 flags (the desirelines cleanup found a `deprecated_date`-vs-`deprecated_at` +audit and dateless files that active-only lint let sail through). + +## Proposed change + +Fold the existing audit checks into the top-level `lint` so one `tskflwctl lint` +run reports tasks + epics + audits: + +- Run `LintAudits` (or its per-audit `check`) from `runLint` / `Service.Lint`, + merging audit `LintResult`s and `FileProblem`s into the same render + exit path. +- Update the human footer + success wording ("tasks, epics, and audits") — the + neutral "item" noun for the mixed roster is already in place. +- Keep `audit lint [audit]` as the focused single-audit subcommand; this task is + about the *aggregate* gate covering audits too, not replacing it. + +## Acceptance criteria + +- [ ] `tskflwctl lint` (no subcommand) reports audit issues — bad/missing bucket, + missing/drifted audit id, and finding-level (status vocab, bucket↔state) — + in the same pass as tasks and epics. +- [ ] `lint --json` includes audit results/problems in the envelope. +- [ ] Success message no longer claims only "tasks and epics". +- [ ] `audit lint` behavior is unchanged; no double-reporting when both are run. +- [ ] A test covers an audit with a frontmatter defect being caught by top-level `lint`. + +## Out of scope / deferred + +- **Which audit content fields are *required*** (`area`/`date` presence, `date` + format, `updated_at`) plus a new `LintAudit(a)` field-nag — that's a strictness + decision for epic 26's ADR (per-entity/per-status matrix, Q1/Q9). This task is + the *wiring* fix; add the field nags once the ADR decides them. + +## Related + +- Epic [26-frontmatter-schema-declared-validation-contract](../epics/26-frontmatter-schema-declared-validation-contract.md) — owns the field-strictness policy (Q1 strictness matrix, Q9 entity coverage) this defers to. +- Prior art: `Service.Lint` (`core/service.go:216`), `Service.LintAudits` (`core/finding.go:196`), `runLint` (`cli/lint.go:33`), `FrontmatterBucketIssues` / `MissingIDIssue` / `IDDriftIssue` (`domain/lint.go`), `FixFrontmatter` (`store/fix.go:16`). diff --git a/planning/tasks/6fmvcgpkyh3e-alias-lint-warn-flag-legacy-field-names-and-fix-rename-via-a-bounded-alias-table.md b/planning/tasks/6fmvcgpkyh3e-alias-lint-warn-flag-legacy-field-names-and-fix-rename-via-a-bounded-alias-table.md new file mode 100644 index 0000000..c42bb59 --- /dev/null +++ b/planning/tasks/6fmvcgpkyh3e-alias-lint-warn-flag-legacy-field-names-and-fix-rename-via-a-bounded-alias-table.md @@ -0,0 +1,89 @@ +--- +schema: 1 +id: 6fmvcgpkyh3e +status: ready-to-start +epic: 26-frontmatter-schema-declared-validation-contract +description: Flag legacy/misspelled frontmatter keys (deprecated_date→deprecated_at) in lint, rename under --fix via a frozen alias table; needs a raw-keys→lint path shared with epic 26 field checks. +effort: Unknown +tier: 3 +priority: medium +autonomy_level: 3 +tags: [lint, schema, frontmatter, core] +created: "2026-07-10" +updated_at: "2026-07-10" +--- +# Alias lint-warn: flag legacy field names + `--fix` rename via a bounded alias table + +Implements epic 26's **Q3** decision (auto-migrate legacy field names under +`--fix`, bounded to a fixed alias table) and the **Q2** "misspelled known field" +path. Gated on ADR-0005 ratifying the two foundations below. + +## The gap (researched 2026-07-10) + +A legacy/misspelled key like `deprecated_date` (vs recognized `deprecated_at`) +today **sails through lint silently**: it's preserved in the file text but dropped +on unmarshal, and the reader uses `deprecated_at` — so the value is effectively +ignored with no signal. `KnownTaskField` exists (`internal/domain/fields.go`) and +`task set --set` already rejects unknown keys, but `lint` has no equivalent guard. + +## Why it's a real (small) feature, not a bolt-on + +`Service.Lint` (`internal/core/service.go:216`) operates on parsed `Task`/`Epic` +**structs**, which have already dropped unknown keys. So there is **no current +path that surfaces raw frontmatter keys to the reporter** — only the `--fix` text +walk (`fixFrontmatterText`, `internal/store/fix.go`) sees them. Flagging an alias +needs a new "raw keys → lint" path, which is the **same foundation every future +epic-26 field check needs** — so build it once, deliberately, not bolted on. + +## Proposed change + +1. **Alias table (domain).** Add `fieldAliases map[string]string` beside + `taskFields` in `internal/domain/fields.go` (seed: `deprecated_date → + deprecated_at`) + an `AliasFor(key) (canonical string, ok bool)` accessor. + Frozen/append-only, like the field registry. +2. **Raw-key surfacing (the foundation).** Give lint access to each file's raw + top-level frontmatter keys. Two candidate shapes — pick in review: + - carry a `RawKeys []string` (or an `Unknown`/alias slice) off the store read + alongside the parsed struct, or + - a store-side scan that returns alias findings as `FileProblem`s (an alias is + a file-level defect; `FileProblem` is the existing vehicle). +3. **Detect (domain).** `AliasIssues(rawKeys []string) []Issue` → "legacy field + `deprecated_date` — use `deprecated_at` (`lint --fix` renames it)". Wire into + `Service.Lint`; coordinate with the audit-lint roster task (`6fm8p1cj11qf`) so + audits get it too. +4. **Repair (`--fix`).** Rename alias keys at the text level in `fixFrontmatterText` + (it already iterates raw `key: value` lines) — a bounded key rewrite that + preserves value, comments, and key order. Only for keys in the alias table. + +## Severity — sidestep Q6 + +Ride the **existing binary lint** as a *fixable issue* (same class as +`MissingIDIssue`/`IDDriftIssue`: hard-but-`--fix`-repairs), so this lands without +waiting on Q6's `error|warn|info` decision. If Q6 later adds a `warn` tier, demote +then. (A silently-ignored field is arguably a real defect, so fixable-error is +defensible now.) + +## Acceptance criteria + +- [ ] `tskflwctl lint` flags a task/audit whose frontmatter carries a known alias + key (e.g. `deprecated_date`), naming the canonical field. +- [ ] `lint --fix` renames the alias to its canonical key, preserving value, + comments, and key order; idempotent on a second run. +- [ ] `lint --json` includes the alias finding. +- [ ] The alias table is a single frozen source of truth in `domain/fields.go`. +- [ ] Tests: a file with an alias is flagged, `--fix` repairs it, and a file with + only canonical keys is untouched. + +## Out of scope / deferred + +- General unknown-field policy (closed registry vs `x-*` escape hatch) — that's + Q2, decided in ADR-0005; this task only handles keys **in the alias table**. +- A `migrate` verb / migration framework — parked (ADR-0005 Q10, YAGNI at + `schema: 1`). + +## Related + +- Epic [26-frontmatter-schema-declared-validation-contract](../epics/26-frontmatter-schema-declared-validation-contract.md) — Q2/Q3 own this; gated on ADR-0005. +- ADR survey task `6fkkz41cax80` — Q3 (auto-migrate, bounded) / Q2 (misspelled-known → alias) notes. +- Audit-lint roster task `6fm8p1cj11qf` — share the raw-key path so audits get alias checks too. +- Prior art: `KnownTaskField` / `taskFields` (`domain/fields.go`), `fixFrontmatterText` (`store/fix.go`), `Service.Lint` (`core/service.go:216`).