Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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`).
Loading
Loading