From e514f937472b3d28bb78b562d161752d6e5a0b43 Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Sat, 25 Jul 2026 13:36:09 -0700 Subject: [PATCH 1/6] docs(pd-m4): capture session learnings, decisions, and the M5 task Session knowledge residue from the merged pd-m4 (CONVENTIONS digestion) work, landed as the first commit of the M5 branch. Learnings: - A guard derived from a capability accessor silently lifts when the accessor is extended (T04 opening the mover before T13). - Skill/doc examples of a serialized structure must round-trip through the real parser (the latent ctx-digest plan-JSON bug). - Using the proprietary sibling repo as design evidence leaks its internals into tracked files. Decisions: - ctx convention add requires --section; no default; placeholders rejected (strict). - Theme declaration via the Themes section keyword, on all three canonical kinds. Task: - M5: suggest-only trigger wiring for progressive disclosure. Spec: specs/progressive-disclosure.md Signed-off-by: Jose Alekhinne --- .context/DECISIONS.md | 28 ++++++++++++++++++++++++++++ .context/LEARNINGS.md | 30 ++++++++++++++++++++++++++++++ .context/TASKS.md | 4 ++++ 3 files changed, 62 insertions(+) diff --git a/.context/DECISIONS.md b/.context/DECISIONS.md index 2fb1ea97..ea5fdf80 100644 --- a/.context/DECISIONS.md +++ b/.context/DECISIONS.md @@ -45,6 +45,34 @@ For significant decisions: --> +## [2026-07-25-132759] Theme declaration via the Themes section keyword, on all three canonical kinds + +**Status**: Accepted + +**Context**: pd-m4 needed a way to NAME a theme up front (ctx add --section Themes), distinct from folding entries into it. The section-vs-entry distinction in conventions surfaced the need. + +**Decision**: Theme declaration via the Themes section keyword, on all three canonical kinds + +**Rationale**: Content is '', split on the same em-dash separator the theme parser reads back, so it round-trips. It writes BOTH the gist bullet and the theme file, because a bullet without its file fails the gist-to-file pairing invariant and would leave a root ctx disclosure refuses to touch. Generalized to learnings/decisions since they also have a ## Themes region. + +**Consequence**: New internal/write/theme package + disclosure.AddTheme; theme adds bypass the per-kind body-flag gate (a theme has only a name and gist); a new user-facing CLI surface across all three canonical kinds. + +--- + +## [2026-07-25-132719] ctx convention add requires --section; no default; placeholders rejected (strict) + +**Status**: Accepted + +**Context**: The convention add-path (pd-m4). A default or catch-all section is where an undecided caller dumps every convention, defeating the H2-section grouping the digest pass folds on. + +**Decision**: ctx convention add requires --section; no default; placeholders rejected (strict) + +**Rationale**: Choosing the section IS the thinking; the CLI refuses to do it for the caller. Enforced strictly via validate.RejectPlaceholder, which rejects empty/whitespace AND the shipped placeholder set (TBD, n/a, none, pending, …). Deviates deliberately from the plan's original T16 ('just move the anchor'). + +**Consequence**: ctx convention add errors without a concrete --section; agents and scripts must name the target H2 section. Pinned by TestConventionAddRequiresSection (7 cases); a refused add leaves the root byte-identical. + +--- + ## [2026-07-19-100259] M4 conventions digestion: curated ## -section taxonomy, unified into the entry-kind mover **Status**: Accepted diff --git a/.context/LEARNINGS.md b/.context/LEARNINGS.md index 9b32060c..a64dcc58 100644 --- a/.context/LEARNINGS.md +++ b/.context/LEARNINGS.md @@ -15,6 +15,36 @@ DO NOT UPDATE FOR: --> +## [2026-07-25-124457] Using the proprietary sibling repo as design evidence leaks its internals into tracked files + +**Context**: While deciding the pd-m4 add-path shape, I read the sibling repo's convention file to settle the question, then quoted its guide text and attributed the decision to it in a tracked plan file. An unrelated build warning prompted the sweep that caught it. + +**Lesson**: The leak vector is not NAMING the sibling (allowed) — it is citing its designs or guide text as evidence in tracked specs/plans. Both the quote and the attribution are leaks. + +**Application**: Cite ctx's OWN files as design evidence and describe designs on their merits; never quote or attribute to the sibling in tracked files; git grep the staged tree before committing any spec authored while consulting it. Per specs/public-repo-hygiene.md. + +--- + +## [2026-07-25-124457] Skill and doc examples of a serialized structure must round-trip through the real parser + +**Context**: The ctx-digest skill documented digest-plan entries as JSON strings, but Assignment.Entries is []StagedEntry{timestamp,title}; the documented form failed json.Unmarshal. Latent since M3 — any agent following the skill verbatim failed at the apply step. + +**Lesson**: A hand-authored example of a serialized structure drifts from the code silently; nothing tests the prose. + +**Application**: Verify skill/doc JSON/YAML examples against the actual unmarshal target — build the binary and run the exact documented command — before shipping the skill. + +--- + +## [2026-07-25-124457] A guard derived from a capability accessor silently lifts when the accessor is extended + +**Context**: pd-m4 T04 made ThemeDir(convention) return true; Apply's refusal of the convention kind was DERIVED from ThemeDir returning false. That one-line 'vocabulary' change silently opened the destructive mover to CONVENTIONS.md before the convention parse/validate/dup-title paths existed. Caught by TestApply_Refusals. + +**Lesson**: When a refusal is derived from a capability check (if !capable { refuse }) instead of an explicit guard, extending that capability lifts the refusal as an invisible side effect — and the two tasks look independent in the plan. + +**Application**: Before extending a capability accessor, grep for guards that read it. When a multi-task arc will open a destructive path, add an EXPLICIT gate that the opening task removes deliberately, so it cannot open early. + +--- + ## [2026-07-19-210439] The disclosure parser is a deliberately dumb line-scanner (skips comments, not code fences) **Context**: M4 unifies conventions into the ## -section entry model, where ## collides far more than ## [. Fence detection was considered and rejected. diff --git a/.context/TASKS.md b/.context/TASKS.md index 0a2fb259..9e7d1fbb 100644 --- a/.context/TASKS.md +++ b/.context/TASKS.md @@ -3046,3 +3046,7 @@ E5[T16–17] E6[T18–19] E7[T20–22] = 22. - [ ] Drift path-checker false-positives on angle-bracket placeholders: `ctx drift` flags CONVENTIONS.md template placeholders (`internal/assets/claude/skills/ctx-/SKILL.md`, `docs/recipes/.md`, `docs/cli/.md`) as missing path references. These are illustrative `<…>` placeholders, not real paths — the path checker should skip any path segment containing `<`/`>`. (Same class as the just-consolidated "detection scripts flag illustrative examples" learning.) #priority:low #session:87e465a0 #branch:main #added:2026-07-16 - [ ] ARCHITECTURE.md package-doc drift: `ctx drift` reports ~44 internal/ packages "not documented" in ARCHITECTURE.md (incl. the renamed internal/heading). Either backfill the package coverage via /ctx-architecture, or reframe ARCHITECTURE.md's scope so the drift check doesn't expect exhaustive per-package coverage. #priority:low #session:87e465a0 #branch:main #added:2026-07-16 + +### Progressive disclosure — Milestone 5 (suggest-only triggers) + +- [ ] M5: suggest-only trigger wiring for progressive disclosure — growth nudge points at /ctx-digest; /ctx-remember surfaces oversized roots; /ctx-wrap-up surfaces them at session end. Suggest only, never auto-fold. Plan: pd-m4.md Out-of-scope table. #session:951e1535 #branch:main #commit:1a880bf3 #added:2026-07-25-132759 From 7b20f14b2a89ddd85450e3cac29dcca577860e0b Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Sat, 25 Jul 2026 20:12:22 -0700 Subject: [PATCH 2/6] =?UTF-8?q?docs(pd-m5):=20design=20suggest-only=20trig?= =?UTF-8?q?gers=20=E2=80=94=20two-signal=20knowledge=20health?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formalizes phase 5 of progressive disclosure (the suggest-only triggers) into specs/progressive-disclosure.md, resolving the nudge-threshold TBD the pd-m4 plan flagged as blocking. One knowledge.Health(ctxDir, thresholds) function feeds all three surfaces (the check-knowledge hook, /ctx-remember, /ctx-wrap-up), so the Go hook and the prose skills cannot drift. It emits two signal kinds, because "should I fold?" and "is this too heavy to be useful context?" are different questions: - Foldable root (a COUNT): staged entries via disclosure.StagedEntries (## [ blocks / ## sections, excluding ## Themes) over a per-kind threshold → suggest /ctx-digest. Reads a never-migrated root correctly. - Heavy page (a BYTE count): bytes over the root AND every theme file under .context// → advisory split-or-extract. The root-only measure was blind to the bloat folding relocates into theme files. The heavy-page remedy is deliberately not tier-2 auto-fold: past a byte ceiling, an LLM is a poor linter and more Markdown is the wrong fix, so the human decides whether the content belongs in tooling instead. convention_line_count (a size measure) is retired for convention_section_count (the watermark unit); a shared theme_page_byte_ceiling is added. No state file — staging plus on-disk bytes are self-describing. The check-knowledge throttle / log-first plumbing is reused so a declining user is not nagged. Records both decisions in DECISIONS.md. Implementation follows via /ctx-task-out --milestone pd-m5. Spec: specs/progressive-disclosure.md Signed-off-by: Jose Alekhinne --- .context/DECISIONS.md | 28 +++++++++++ specs/progressive-disclosure.md | 84 +++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/.context/DECISIONS.md b/.context/DECISIONS.md index ea5fdf80..d5f911ce 100644 --- a/.context/DECISIONS.md +++ b/.context/DECISIONS.md @@ -45,6 +45,34 @@ For significant decisions: --> +## [2026-07-25-190410] Beyond a byte ceiling, knowledge content should become tooling, not more Markdown + +**Status**: Accepted + +**Context**: Designing M5's heavy-page signal; the failure case is a convention theme file growing toward ~1MB. + +**Decision**: Beyond a byte ceiling, knowledge content should become tooling, not more Markdown + +**Rationale**: An LLM is a poor linter: it cannot reliably apply a large ruleset expressed as prose. Past a ceiling, recursive folding (more Markdown) is the wrong fix; the right move is extracting the content to actual tooling (a linter), code, or docs. So the heavy-page remedy is an advisory 'split OR extract to tooling', decided by the human, not tier-2 auto-fold. + +**Consequence**: The heavy-page nudge frames the ceiling as a smell ('a context file this heavy is a linter in prose'), prompting extraction rather than endless subdivision. Tier-2 theme recursion remains deferred, not precluded. + +--- + +## [2026-07-25-190410] M5 knowledge health is two suggest-only signals: foldable root (staging count) and heavy page (bytes) + +**Status**: Accepted + +**Context**: M5 wires the growth nudge, /ctx-remember, and /ctx-wrap-up to progressive disclosure. The 2026-07-16 foundational decision already fixed staging-as-watermark, the three surfaces, and suggest-only. Left open: the signal/threshold, and that the original root-only measure was blind to theme-file bloat that folding itself creates. + +**Decision**: M5 knowledge health is two suggest-only signals: foldable root (staging count) and heavy page (bytes) + +**Rationale**: Two questions need two measures. 'Should I fold?' is a COUNT of staged entries/sections via disclosure.StagedEntries (suggest /ctx-digest). 'Is this page too heavy to be useful context?' is a BYTE count, scanned over the root AND every theme file under .context// (suggest split or extract). Folding relocates bulk into theme files, so a root-only signal is blind to the bloat it creates. One knowledge.Health function feeds all three surfaces so hook and skills cannot drift. No state file: staging plus on-disk bytes are self-describing. + +**Consequence**: convention_line_count (200 lines) is replaced by convention_section_count (foldability) plus a shared theme_page_byte_ceiling (weight); a deliberate behavior change, framed as user education. Theme files are now scanned. Reuses disclosure.StagedEntries and the check-knowledge daily-throttle / log-first plumbing. + +--- + ## [2026-07-25-132759] Theme declaration via the Themes section keyword, on all three canonical kinds **Status**: Accepted diff --git a/specs/progressive-disclosure.md b/specs/progressive-disclosure.md index e9573a67..7814817a 100644 --- a/specs/progressive-disclosure.md +++ b/specs/progressive-disclosure.md @@ -124,13 +124,57 @@ Agent-driven, human-gated, never inline in another ceremony: themes alone. 5. Create `## Themes` on first run. -### Triggers — suggestion only +### Triggers — suggestion only (milestone 5) The growth/threshold nudge, `/ctx-remember`, and `/ctx-wrap-up` may **suggest** the pass. None of them perform it. Wrap-up especially must stay light: the human is closing the laptop to go live their life, and semantic work there is against their interest. +All three surfaces read one function — `knowledge.Health(ctxDir, +thresholds) → []Finding` — so the hook (Go) and the skills (prose calling +a `ctx` report path) can never drift. It emits **two signal kinds**, +because "should I fold this?" and "is this too heavy to be useful +context?" are different questions with different units. No state file: +the staging zone and on-disk bytes are self-describing (the watermark +principle from the M1 decision). + +**Signal 1 — foldable root (a *count*).** The number of staged entries, +via `disclosure.StagedEntries` (`## [` blocks for decisions/learnings, +`## ` sections for conventions, both excluding the structural +`## Themes`). This reads a never-migrated root correctly — with no +`## Themes`, the whole entry region is staging. Over its per-kind +threshold, the finding suggests **`/ctx-digest`** (fold staging into +themes); `/ctx-consolidate` and `/ctx-drift` are demoted to secondary +("if entries overlap rather than just accrete"). + +**Signal 2 — heavy page (a *byte count*).** Bytes, not lines: a line +hides 10 or 200 characters, but bytes are the true context cost. Scanned +over the root **and every theme file** under `.context//*.md` — +the root-only measure was blind to the bloat folding itself relocates +into theme files. Over the byte ceiling, the finding is advisory: *split +the theme, or extract it to actual tooling.* Past a ceiling, more +Markdown is the wrong fix — an LLM is a poor linter and cannot reliably +apply a large ruleset written as prose, so the remedy prompts the human +to decide whether the content still belongs in an LLM-read file at all. +Tier-2 auto-fold is **not** the heavy-page remedy (it stays deferred; see +Non-Goals). + +When both signals fire on one root (a large un-migrated file is both +foldable and heavy), the formatter leads with `/ctx-digest` — folding +reduces both. + +**Config** (rc-tunable; `0` disables a check, preserving the existing +convention): per-kind staging counts (`entry_count_learnings` 30, +`entry_count_decisions` 20 carry over unchanged; `convention_line_count` +is **replaced** by `convention_section_count`, since a section count is +the watermark unit — a deliberate behavior change framed as user +education), and a shared `theme_page_byte_ceiling` for signal 2. + +The existing `check-knowledge` daily-throttle + snooze plumbing and the +`NudgeBox`/`EmitAndRelay` log-first path are reused, so a user who +declines to fold is not nagged every session. + ## Guards 1. **Append → verify → remove.** Never remove-then-append. Any verify @@ -162,6 +206,12 @@ semantic work there is against their interest. - Every theme link resolves (existing `ctx drift` path check). - Every entry lives in **exactly one** place: staging XOR one theme file. - Convention staging titles are **unique** — identity is the section title, so duplicates fail loud (`ErrDuplicateStagedTitle`). +- **(M5)** The foldability signal counts the *staging zone*, not the + whole file: a folded root (entries moved to theme files) reports a + count below threshold and stops nudging; a never-migrated root reports + its full entry count. +- **(M5)** The weight scan covers the root **and** every theme file; a + heavy theme file is flagged even when the root is lean. ## Tests @@ -174,6 +224,16 @@ semantic work there is against their interest. above `## Themes` too (correcting its original `AppendAtEnd`). - **Abort**: corrupt the root → pass refuses, file byte-identical. - **Idempotency**: pass with empty staging = no-op. +- **(M5) `knowledge.Health` fixtures**: un-migrated large root → + foldable finding; migrated root + oversized theme file → heavy finding; + both at once → foldable leads; all under threshold → no findings. +- **(M5) Boundary**: each threshold fires at `> N`, not `>= N`; `0` + disables the check. +- **(M5) Convention measure**: a folded 5-section / 250-line convention + root does **not** foldable-nudge (section count, not lines). +- **(M5) Surface parity**: the `check-knowledge` hook and the skill + report path produce the same findings from the same root (shared + `Health`). ## Acceptance @@ -187,6 +247,13 @@ semantic work there is against their interest. correcting the original inconsistency — the one deliberate add-path change. - The pass is codified as a reusable skill. +- **(M5)** The growth nudge, `/ctx-remember`, and `/ctx-wrap-up` each + surface foldable roots (→ `/ctx-digest`) and heavy pages (→ split / + extract), from one shared `knowledge.Health`, suggestion-only. +- **(M5)** The foldability signal is staging-based across all three + kinds; `convention_line_count` is retired for `convention_section_count`. +- **(M5)** Theme files are scanned for weight; nothing auto-folds and no + state file is introduced. ## Non-Goals @@ -195,7 +262,11 @@ semantic work there is against their interest. - **No change to the decision/learning `add` write path** (the convention path changes: `AppendAtEnd` → prepend above `## Themes`, correcting an original inconsistency). - **No `ctx agent` packet rewire** — boundedness makes it unnecessary. - **No taxonomy/nesting machinery now** — the structure is self-similar, - so nesting is available when themes outgrow their file. + so nesting is available when themes outgrow their file. **(M5)** The + heavy-page signal deliberately does **not** auto-recurse (tier-2 fold); + it advises the human to split or extract-to-tooling, because past a + byte ceiling the right answer may be "this belongs in a linter, not a + context file," which no automatic subdivision can decide. - **CONSTITUTION.md and TASKS.md are out of scope** — the former is small by design, the latter is auto-archived. - **KB pipeline untouched.** @@ -208,4 +279,11 @@ semantic work there is against their interest. 3. First real rollout on LEARNINGS (largest corpus), then DECISIONS. 4. CONVENTIONS (curated `## `-section model, unified into the entry-kind mover + prepend add-path, edits-behind-a-link UX). -5. Wire the suggest-only triggers. +5. **Wire the suggest-only triggers (milestone 5).** One + `knowledge.Health` emitting foldable (staging count → `/ctx-digest`) + and heavy (bytes over root + theme files → split/extract) findings; + consumed by the `check-knowledge` hook and a report path the + `/ctx-remember` and `/ctx-wrap-up` skills call. Retire + `convention_line_count` for `convention_section_count`; add + `theme_page_byte_ceiling`. Suggestion-only, no state file, throttle + reused. Decompose via `/ctx-task-out --milestone pd-m5`. From 104f8fb12ffe5e60d662355208920102ea628d97 Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Sat, 25 Jul 2026 20:16:31 -0700 Subject: [PATCH 3/6] docs(pd-m5): decompose into implementation plan (23 tasks, 6 epics) Plan document at specs/plans/pd-m5.md: data model, contracts, an 8-row test matrix, 23 falsifiable tasks in 6 epics, execution waves, and a measurement gate (T22). Epic anchors synced to TASKS.md, partitioning T01-T23 (3+5+4+2+6+3). Blocking TBD (nudge-threshold) resolved in the spec + DECISIONS; default threshold values (section_count=12, byte_ceiling=64KB) carried as deferrable, tunable via rc. Spec: specs/progressive-disclosure.md Signed-off-by: Jose Alekhinne --- .context/TASKS.md | 24 +++++++- specs/plans/pd-m5.md | 132 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 specs/plans/pd-m5.md diff --git a/.context/TASKS.md b/.context/TASKS.md index 9e7d1fbb..22b6c4bd 100644 --- a/.context/TASKS.md +++ b/.context/TASKS.md @@ -3049,4 +3049,26 @@ E5[T16–17] E6[T18–19] E7[T20–22] = 22. ### Progressive disclosure — Milestone 5 (suggest-only triggers) -- [ ] M5: suggest-only trigger wiring for progressive disclosure — growth nudge points at /ctx-digest; /ctx-remember surfaces oversized roots; /ctx-wrap-up surfaces them at session end. Suggest only, never auto-fold. Plan: pd-m4.md Out-of-scope table. #session:951e1535 #branch:main #commit:1a880bf3 #added:2026-07-25-132759 +- [ ] M5: suggest-only trigger wiring for progressive disclosure — growth nudge points at /ctx-digest; /ctx-remember surfaces oversized roots; /ctx-wrap-up surfaces them at session end. Suggest only, never auto-fold. Plan: specs/plans/pd-m5.md #session:951e1535 #branch:main #commit:1a880bf3 #added:2026-07-25-132759 + +Plan: `specs/plans/pd-m5.md` · Spec: `specs/progressive-disclosure.md` +Two suggest-only signals from one `knowledge.Health`: **foldable root** +(staging count → `/ctx-digest`) and **heavy page** (bytes over root + +theme files → split/extract). No auto-fold, no state file. + +**Completion rule**: an epic is `[x]` only when every task in its range is +`[x]`/`[o]` in `specs/plans/pd-m5.md` (the plan is the source of truth). +Epics partition T01–T23: E1[T01–03] E2[T04–08] E3[T09–12] E4[T13–14] +E5[T15–20] E6[T21–23] = 23. + +- [ ] [E1] Config: retire `ConventionLineCount`; add `ConventionSectionCount` + `ThemePageByteCeiling` + runtime defaults (T01–T03). Plan: specs/plans/pd-m5.md #priority:medium #session:951e1535 #branch:design/pd-m5-triggers #added:2026-07-25 + +- [ ] [E2] Health core: `finding` Kind/Path; foldable signal via `disclosure.StagedEntries`; heavy signal over root + theme-file bytes; combine with foldable-first ordering (T04–T08). Plan: specs/plans/pd-m5.md #priority:medium #session:951e1535 #branch:design/pd-m5-triggers #added:2026-07-25 + +- [ ] [E3] Wiring: warning text (`/ctx-digest` primary + split/extract); format routing by kind; `check-knowledge` report path; hook uses `Health` (T09–T12). Plan: specs/plans/pd-m5.md #priority:medium #session:951e1535 #branch:design/pd-m5-triggers #added:2026-07-25 + +- [ ] [E4] Skills: `/ctx-remember` + `/ctx-wrap-up` surface foldable/heavy, suggest-only (T13–T14). Plan: specs/plans/pd-m5.md #priority:medium #session:951e1535 #branch:design/pd-m5-triggers #added:2026-07-25 + +- [ ] [E5] Tests: health fixtures, heavy root + theme file, both-fire ordering, convention measure, boundary/disable, surface parity (T15–T20). Plan: specs/plans/pd-m5.md #priority:medium #session:951e1535 #branch:design/pd-m5-triggers #added:2026-07-25 + +- [ ] [E6] Sync + gates: copilot skill sync, measurement gate (T22), milestone gate (T21–T23). Plan: specs/plans/pd-m5.md #priority:medium #session:951e1535 #branch:design/pd-m5-triggers #added:2026-07-25 diff --git a/specs/plans/pd-m5.md b/specs/plans/pd-m5.md new file mode 100644 index 00000000..0b955a77 --- /dev/null +++ b/specs/plans/pd-m5.md @@ -0,0 +1,132 @@ +# pd-m5 Plan — suggest-only triggers (two-signal knowledge health) + +**Spec:** `specs/progressive-disclosure.md` · **Status:** Ready +**Blocking TBDs resolved:** nudge-threshold — decided **two-signal +health (foldability count + weight bytes)** in the spec's revised +`### Triggers` and `DECISIONS.md` (2026-07-25: two-signal health; the +Markdown-linter ceiling). + +## Scope & DoD + +Wire the three existing session surfaces — the `check-knowledge` growth +nudge, `/ctx-remember`, `/ctx-wrap-up` — to progressive disclosure via +one shared `knowledge.Health`, emitting two suggestion-only signals: +**foldable root** (staging count → `/ctx-digest`) and **heavy page** +(bytes over root + theme files → split / extract-to-tooling). No +auto-fold, no state file. + +DoD: +- [ ] `knowledge.Health` emits foldable + heavy findings from one scan; + both consumed by the hook and the skill report path. +- [ ] Foldability is staging-based across all three kinds via + `disclosure.StagedEntries`; `convention_line_count` retired for + `convention_section_count`. +- [ ] Weight scan covers the root **and** every theme file, in bytes. +- [ ] The growth nudge suggests `/ctx-digest` (foldable) and + split/extract (heavy); `/ctx-remember` and `/ctx-wrap-up` surface both. +- [ ] Suggestion-only; existing daily-throttle/snooze reused; no new + state file. +- [ ] `make lint` 0, `go test ./...` green, `make audit` pass. + +## Data model & storage + +- No persistence. Signals derive from on-disk content each scan. +- `finding` (types.go) gains a **signal kind** (`foldable` | `heavy`) + and a **Path** (for heavy theme-file findings, not just a canonical + basename); `Unit` extends to `bytes`/`sections`. +- rc (`internal/rc/types.go`, accessors `rc.go`): **retire** + `ConventionLineCount`; **add** `ConventionSectionCount` and + `ThemePageByteCeiling`. +- runtime defaults (`internal/config/runtime/runtime.go`): retire + `DefaultConventionLineCount (200)`; add + `DefaultConventionSectionCount = 12` and + `DefaultThemePageByteCeiling = 65536` (64 KB). Both tunable; `0` + disables (existing convention). + +## Contracts + +- `knowledge.Health(ctxDir string, cfg Thresholds) []finding` — the + single source; foldable via `len(disclosure.StagedEntries(Parse(...)))`, + heavy via `len(bytes)` over root + `disclosure.ThemeDir(kind)` files. +- Both-fire on one root → foldable finding ordered first (fold reduces + both). +- `ctx system check-knowledge` gains a report path emitting the findings + (text) the skills relay; the hook path is unchanged in shape. +- Warning text (`hooks.yaml`, `check-knowledge/warning.txt`): foldable → + `/ctx-digest` primary, consolidate/drift secondary; heavy → "split the + theme, or extract to tooling (a context file this heavy is a linter in + prose)". + +## Test matrix + +| # | Invariant / behavior | Violation attempt | Expected | Task | +|---|---|---|---|---| +| M1 | Foldability counts staging, not whole file | un-migrated root, 40 entries | foldable finding, count 40 | T15 | +| M2 | Folded root quiets | migrated root, 3 staged + 12 themes | no foldable finding | T15 | +| M3 | Weight scans theme files | migrated root + one 80 KB theme file | heavy finding on that file | T16 | +| M4 | Both signals on one root | large un-migrated root | foldable ordered before heavy | T17 | +| M5 | Convention measure is sections | 5-section / 250-line CONVENTIONS | no foldable nudge | T18 | +| M6 | Threshold fires at `> N` | count == N exactly | no finding; `N+1` → finding | T19 | +| M7 | `0` disables a check | threshold 0 | that signal never fires | T19 | +| M8 | Surface parity | same root via hook + report | identical findings | T20 | + +## Task breakdown + +| id | st | task | deps | files | [P] | acceptance criterion | spec ref | +|---|---|---|---|---|---|---|---| +| T01 | [ ] | runtime: retire `DefaultConventionLineCount`; add `DefaultConventionSectionCount=12`, `DefaultThemePageByteCeiling=65536` | — | `internal/config/runtime/runtime.go`, `doc.go` | [P] | build; `grep DefaultConventionLineCount` → none | Triggers/Config | +| T02 | [ ] | rc types+accessors: retire `ConventionLineCount`; add `ConventionSectionCount`, `ThemePageByteCeiling` | T01 | `internal/rc/types.go`, `rc.go`, `default.go` | | unit: accessors return defaults; `rc.ConventionSectionCount()==12` | Config | +| T03 | [ ] | retire `ConventionLineCount` refs project-wide | T02 | `internal/**`, validate_test yaml keys | | `grep -rn ConventionLineCount internal/` → no non-test hits; config test green | Config | +| T04 | [ ] | `finding` type: add `Kind` (foldable\|heavy) + `Path`; extend `Unit` | — | `internal/cli/system/core/knowledge/types.go` | [P] | unit: zero-value + both kinds construct | Data model | +| T05 | [ ] | foldable signal: count staging via `disclosure.StagedEntries` per kind (replaces ParseEntryBlocks + line-count) | T02,T04 | `internal/cli/system/core/knowledge/knowledge.go` | | unit: un-migrated 40-entry → foldable(40); folded → none (**M1,M2**) | Signal 1 | +| T06 | [ ] | heavy signal: byte scan of the root file | T04 | `knowledge.go` | | unit: root > ceiling → heavy finding on root path | Signal 2 | +| T07 | [ ] | heavy signal: byte scan of theme files via `disclosure.ThemeDir(kind)` | T04,T06 | `knowledge.go` | | unit: 80 KB theme file → heavy finding on that file (**M3**) | Signal 2 | +| T08 | [ ] | `knowledge.Health`: combine signals; both-fire → foldable first | T05,T06,T07 | `knowledge.go` | | unit: large un-migrated root → foldable ordered before heavy (**M4**) | Contracts | +| T09 | [ ] | warning text: foldable→/ctx-digest primary (consolidate/drift secondary); heavy→split/extract | T08 | `internal/assets/commands/text/hooks.yaml`, `internal/assets/hooks/messages/check-knowledge/warning.txt` | | build embeds; message-registry test green; text names /ctx-digest | Contracts | +| T10 | [ ] | `FormatWarnings`/`EmitWarning` route by `Kind` | T08,T09 | `knowledge.go` | | unit: foldable vs heavy render distinct remedy lines | Contracts | +| T11 | [ ] | `ctx system check-knowledge` report path the skills call | T08 | `internal/cli/system/cmd/checkknowledge/*.go` | | e2e: command prints findings for an oversized fixture root | Contracts | +| T12 | [ ] | hook wiring: `CheckHealth` uses `Health`; throttle/log-first unchanged | T08,T10 | `knowledge.go` | | e2e: hook nudge names /ctx-digest for a foldable root; throttled once/day | Triggers | +| T13 | [ ] | `/ctx-remember` skill: surface foldable/heavy at session start (read-only) | T11 | `internal/assets/claude/skills/ctx-remember/SKILL.md` | [P] | frontmatter test green; body calls the report path, relays suggestions | Triggers | +| T14 | [ ] | `/ctx-wrap-up` skill: surface at session end, suggest-only never inline | T11 | `internal/assets/claude/skills/ctx-wrap-up/SKILL.md` | [P] | frontmatter test green; body surfaces + explicitly does not fold | Triggers | +| T15 | [ ] | `Health` unit tests: foldable, folded-quiet fixtures | T05 | `internal/cli/system/core/knowledge/*_test.go` | | **M1,M2** pass | Tests | +| T16 | [ ] | `Health` unit tests: heavy root + heavy theme file | T07 | `knowledge` test | [P] | **M3** pass | Tests | +| T17 | [ ] | `Health` unit test: both-fire ordering | T08 | `knowledge` test | [P] | **M4** pass | Tests | +| T18 | [ ] | convention measure test: sections not lines | T05 | `knowledge` test | [P] | **M5** pass | Tests | +| T19 | [ ] | boundary + disable tests (`>N`, `0` disables) | T08 | `knowledge` test | [P] | **M6,M7** pass | Tests | +| T20 | [ ] | surface parity test: hook findings == report findings | T11,T12 | `knowledge`/checkknowledge test | | **M8** pass | Tests | +| T21 | [ ] | copilot skill sync | T13,T14 | `internal/assets/integrations/copilot-cli/skills/**` | [P] | `make check-copilot-skills` green | Skill | +| T22 | [ ] |XX **measurement gate**: drive Health on a realistic .context (oversized root + fat theme file) | T12,T11 | scratchpad fixture | | foldable + heavy both surface; suggestions correct; throttle holds | Risks | +| T23 | [ ] |XX milestone gate | T01–T22 | — | | `make lint` 0, `go test ./...` green, `make audit` pass; DoD confirmed | Scope/DoD | + +**Execution waves** (each `∥` group is file-disjoint): `T01 ∥ T04` → +`T02` → `T03 ∥ T05 ∥ T06` → `T07` → `T08` → `T09` → `T10 ∥ T11` → +`T12` → `T13 ∥ T14 ∥ T15 ∥ T16 ∥ T17 ∥ T18 ∥ T19` → `T20 ∥ T21` → +`T22` → `T23`. + +## Risks & measurement gates + +- **T22 measurement gate**: driving `Health` on a real oversized + `.context` (not unit fixtures) is where a signal-ordering or + theme-scan-path bug hides — expect it to find something (every prior + disclosure milestone's gate did). Verify both signals surface with + correct remedies before T23. +- **Default values are guesses** (`section_count=12`, `byte_ceiling=64KB`): + the gate may show them too eager or too lax on real content; tune in an + amendment, not by editing acceptance criteria. +- **Throttle coverage**: confirm the `check-knowledge` daily throttle + actually suppresses the fold nudge across a session (assumption from + the stress-test); if it does not, that is a new task, not a silent gap. + +## Out of scope (deferred, with pointers) + +| Deferred | Milestone | Note | +|---|---|---| +| Tier-2 theme recursion (overgrown theme file → its own root) | — | deferred indefinitely (spec Non-Goals); heavy-page advises split/extract instead | +| Auto-fold at any trigger | — | precluded by design (suggest-only) | +| CONSTITUTION.md / TASKS.md health | — | out of scope by spec | + +## Amendments + +| date | what | why | +|---|---|---| +| — | — | — | From 591e40d925fac9ded88c7afee8f39083b9e62728 Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Sat, 25 Jul 2026 20:30:23 -0700 Subject: [PATCH 4/6] feat(pd-m5): config + two-signal knowledge.Health (E1-E2, tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E1 config: retire ConventionLineCount (line measure) for ConventionSectionCount (staged sections, default 12) and add ThemePageByteCeiling (default 65536); runtime defaults, rc accessors, .ctxrc schema, and the unit text keys (sections, bytes) updated together. E2 core: knowledge.Health emits two signals from one scan — foldable roots (staging count via disclosure.StagedEntries, so a never-migrated root reads full and a folded one quiets) and heavy pages (bytes over the root AND every theme file, closing the fold-created blindness). Both-fire orders foldable first. Unexported scan helpers split into scan.go; Thresholds/finding types in types.go (file-convention + mixed-visibility audits). Tests (M1-M7): un-migrated foldable, folded-quiet, heavy theme file, both-fire ordering, convention-measure-is-sections, boundary (>N) and disable (0). Full suite + make lint green. Spec: specs/progressive-disclosure.md Signed-off-by: Jose Alekhinne --- internal/assets/commands/text/write.yaml | 6 +- internal/assets/schema/ctxrc.schema.json | 9 +- internal/cli/system/core/knowledge/doc.go | 19 +- .../cli/system/core/knowledge/knowledge.go | 117 +++++++------ .../system/core/knowledge/knowledge_test.go | 165 ++++++++++++++++++ internal/cli/system/core/knowledge/scan.go | 90 ++++++++++ internal/cli/system/core/knowledge/types.go | 43 ++++- internal/config/embed/text/check_knowledge.go | 8 +- internal/config/runtime/doc.go | 9 +- internal/config/runtime/runtime.go | 12 +- internal/rc/default.go | 6 +- internal/rc/rc.go | 46 +++-- internal/rc/types.go | 3 +- internal/rc/validate_test.go | 3 +- specs/plans/pd-m5.md | 26 +-- 15 files changed, 449 insertions(+), 113 deletions(-) create mode 100644 internal/cli/system/core/knowledge/knowledge_test.go create mode 100644 internal/cli/system/core/knowledge/scan.go diff --git a/internal/assets/commands/text/write.yaml b/internal/assets/commands/text/write.yaml index 58402549..00b46503 100644 --- a/internal/assets/commands/text/write.yaml +++ b/internal/assets/commands/text/write.yaml @@ -932,8 +932,10 @@ write.provenance-context: write.knowledge-unit-entries: short: "entries" -write.knowledge-unit-lines: - short: "lines" +write.knowledge-unit-sections: + short: "sections" +write.knowledge-unit-bytes: + short: "bytes" write.version-drift-fallback: short: "VERSION (%s), plugin.json (%s), marketplace.json (%s) are out of sync. Update all three before releasing." diff --git a/internal/assets/schema/ctxrc.schema.json b/internal/assets/schema/ctxrc.schema.json index 5718c053..38f572cd 100644 --- a/internal/assets/schema/ctxrc.schema.json +++ b/internal/assets/schema/ctxrc.schema.json @@ -45,9 +45,14 @@ "description": "Entry count threshold for DECISIONS.md drift warning. Default: 20. 0 disables.", "minimum": 0 }, - "convention_line_count": { + "convention_section_count": { "type": "integer", - "description": "Line count threshold for CONVENTIONS.md drift warning. Default: 200. 0 disables.", + "description": "Staged-section count threshold for CONVENTIONS.md before a fold nudge (/ctx-digest). The foldability watermark unit; replaces convention_line_count. Default: 12. 0 disables.", + "minimum": 0 + }, + "theme_page_byte_ceiling": { + "type": "integer", + "description": "Byte ceiling above which a root or theme file is flagged heavy (split or extract-to-tooling). Default: 65536. 0 disables.", "minimum": 0 }, "injection_token_warn": { diff --git a/internal/cli/system/core/knowledge/doc.go b/internal/cli/system/core/knowledge/doc.go index f988f6e4..a93b7901 100644 --- a/internal/cli/system/core/knowledge/doc.go +++ b/internal/cli/system/core/knowledge/doc.go @@ -12,9 +12,12 @@ // // # Public Surface // -// - **[ScanFiles](contextDir)**: counts entries -// in DECISIONS.md and LEARNINGS.md, and lines -// in CONVENTIONS.md, and returns the result. +// - **[Health](contextDir, thresholds)**: emits the +// two M5 signals — foldable roots (staging count +// via disclosure.StagedEntries → /ctx-digest) and +// heavy pages (bytes over the root and every theme +// file → split / extract-to-tooling), foldable +// first. // - **[FormatWarnings](report, thresholds)**: // turns the scan into the human-readable // warning text emitted via the VERBATIM relay. @@ -32,11 +35,15 @@ // 0 disables) // - `entry_count_learnings` (default 30; // 0 disables) -// - `convention_line_count` (default 200; +// - `convention_section_count` (default 12; +// 0 disables) +// - `theme_page_byte_ceiling` (default 65536; // 0 disables) // -// Crossing a threshold means "consider running -// `/ctx-consolidate`", not "stop adding entries". +// Crossing a foldability threshold means "consider +// running `/ctx-digest`"; crossing the weight ceiling +// means "split the theme or extract to tooling" — +// never "stop adding entries". // // # Concurrency // diff --git a/internal/cli/system/core/knowledge/knowledge.go b/internal/cli/system/core/knowledge/knowledge.go index ba6a772c..14a576f6 100644 --- a/internal/cli/system/core/knowledge/knowledge.go +++ b/internal/cli/system/core/knowledge/knowledge.go @@ -7,7 +7,6 @@ package knowledge import ( - "bytes" "fmt" "strings" @@ -19,75 +18,77 @@ import ( "github.com/ActiveMemory/ctx/internal/config/hook" "github.com/ActiveMemory/ctx/internal/config/knowledge" "github.com/ActiveMemory/ctx/internal/config/token" - "github.com/ActiveMemory/ctx/internal/heading" + "github.com/ActiveMemory/ctx/internal/disclosure" "github.com/ActiveMemory/ctx/internal/io" "github.com/ActiveMemory/ctx/internal/notify" "github.com/ActiveMemory/ctx/internal/rc" ) -// ScanFiles checks knowledge files against their configured -// thresholds and returns any that exceed the limits. +// Health scans the canonical knowledge roots and returns the two M5 +// signals: foldable roots (staging accretion → /ctx-digest) and heavy +// pages (byte weight over the ceiling → split / extract-to-tooling). One +// scan feeds the check-knowledge hook and the skill report path alike, so +// they cannot drift. +// +// Foldability is the staging-zone count via disclosure.StagedEntries, so +// it reads a never-migrated root correctly and quiets once a root is +// folded. Weight is bytes over the root AND every theme file, closing the +// blindness the root-only measure had to the bulk folding relocates. +// +// Findings are ordered foldable-first: when a root trips both, folding is +// the single move that reduces both. // // Parameters: // - contextDir: absolute path to the context directory -// - decThreshold: max decision entries (0 = disabled) -// - lrnThreshold: max learning entries (0 = disabled) -// - convThreshold: max convention lines (0 = disabled) +// - t: the four thresholds (0 disables a check) // // Returns: -// - []KnowledgeFinding: files exceeding thresholds, -// or nil if all within limits -func ScanFiles( - contextDir string, decThreshold, lrnThreshold, convThreshold int, -) []finding { - var findings []finding - - if decThreshold > 0 { - data, readErr := io.SafeReadFile(contextDir, ctx.Decision) - if readErr == nil { - count := len(heading.ParseEntryBlocks(string(data))) - if count > decThreshold { - findings = append(findings, finding{ - File: ctx.Decision, - Count: count, - Threshold: decThreshold, - Unit: desc.Text(text.DescKeyWriteKnowledgeUnitEntries), - }) - } - } +// - []finding: signals found, or nil when every root is within limits +func Health(contextDir string, t Thresholds) []finding { + roots := []struct { + file string + threshold int + }{ + {ctx.Learning, t.Learnings}, + {ctx.Decision, t.Decisions}, + {ctx.Convention, t.Conventions}, } - if lrnThreshold > 0 { - data, readErr := io.SafeReadFile(contextDir, ctx.Learning) - if readErr == nil { - count := len(heading.ParseEntryBlocks(string(data))) - if count > lrnThreshold { - findings = append(findings, finding{ - File: ctx.Learning, - Count: count, - Threshold: lrnThreshold, - Unit: desc.Text(text.DescKeyWriteKnowledgeUnitEntries), + var foldables, heavies []finding + for _, r := range roots { + data, readErr := io.SafeReadFile(contextDir, r.file) + if readErr != nil { + continue + } + kind, ok := disclosure.KindFor(r.file) + if !ok { + continue + } + + // Signal 1 — foldable root (staging count). + if r.threshold > 0 { + n := len(disclosure.StagedEntries(disclosure.Parse(string(data), kind))) + if n > r.threshold { + foldables = append(foldables, finding{ + Kind: foldable, File: r.file, Count: n, + Threshold: r.threshold, Unit: foldUnit(kind), }) } } - } - if convThreshold > 0 { - data, readErr := io.SafeReadFile(contextDir, ctx.Convention) - if readErr == nil { - lineCount := bytes.Count(data, []byte(token.NewlineLF)) - if lineCount > convThreshold { - findings = append(findings, finding{ - File: ctx.Convention, - Count: lineCount, - Threshold: convThreshold, - Unit: desc.Text(text.DescKeyWriteKnowledgeUnitLines), - }) + // Signal 2 — heavy pages (bytes): the root, then its theme files. + if t.PageBytes > 0 { + if len(data) > t.PageBytes { + heavies = append(heavies, heavyFinding(r.file, len(data), t.PageBytes)) } + heavies = append(heavies, heavyThemeFiles(contextDir, kind, t.PageBytes)...) } } - return findings + if len(foldables) == 0 && len(heavies) == 0 { + return nil + } + return append(foldables, heavies...) } // FormatWarnings builds a pre-formatted findings list string @@ -162,18 +163,20 @@ func EmitWarning(sessionID, fileWarnings string) (string, error) { // log-first principle and skip printing the box when the relay // audit entry could not be written. func CheckHealth(sessionID, ctxDir string) (string, bool, error) { - lrnThreshold := rc.EntryCountLearnings() - decThreshold := rc.EntryCountDecisions() - convThreshold := rc.ConventionLineCount() + t := Thresholds{ + Learnings: rc.EntryCountLearnings(), + Decisions: rc.EntryCountDecisions(), + Conventions: rc.ConventionSectionCount(), + PageBytes: rc.ThemePageByteCeiling(), + } // All disabled - nothing to check - if lrnThreshold == 0 && decThreshold == 0 && convThreshold == 0 { + if t.Learnings == 0 && t.Decisions == 0 && + t.Conventions == 0 && t.PageBytes == 0 { return "", false, nil } - findings := ScanFiles( - ctxDir, decThreshold, lrnThreshold, convThreshold, - ) + findings := Health(ctxDir, t) if len(findings) == 0 { return "", false, nil } diff --git a/internal/cli/system/core/knowledge/knowledge_test.go b/internal/cli/system/core/knowledge/knowledge_test.go new file mode 100644 index 00000000..5941dfc4 --- /dev/null +++ b/internal/cli/system/core/knowledge/knowledge_test.go @@ -0,0 +1,165 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +package knowledge + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// writeFile writes content under dir/name, creating parent dirs. +func writeFile(t *testing.T, dir, name, content string) { + t.Helper() + path := filepath.Join(dir, name) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +// unmigratedLearnings builds an un-migrated LEARNINGS root with n staged +// entries (no ## Themes) — the whole entry region is staging. +func unmigratedLearnings(n int) string { + var b strings.Builder + b.WriteString("# Learnings\n\n\n\n") + for i := 0; i < n; i++ { + b.WriteString("## [2026-07-15-1200") + b.WriteByte(byte('0' + i%10)) + b.WriteString("0] entry\n\nbody.\n\n") + } + return b.String() +} + +// M1: an un-migrated root over threshold yields a foldable finding whose +// count is the full staged-entry count. +func TestHealth_FoldableUnmigrated(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "LEARNINGS.md", unmigratedLearnings(40)) + + got := Health(dir, Thresholds{Learnings: 30}) + if len(got) != 1 { + t.Fatalf("findings = %d, want 1: %+v", len(got), got) + } + if got[0].Kind != foldable || got[0].Count != 40 { + t.Errorf("finding = %+v, want foldable count 40", got[0]) + } +} + +// M2: a folded root (few staged, bulk in themes) stays under threshold +// and does not foldable-nudge. +func TestHealth_FoldedQuiet(t *testing.T) { + dir := t.TempDir() + // 3 staged entries above ## Themes; theme gists are bullets, not "## [". + root := "# Learnings\n\n" + + "## [2026-07-15-120000] a\n\nx.\n\n" + + "## [2026-07-15-120001] b\n\nx.\n\n" + + "## [2026-07-15-120002] c\n\nx.\n\n" + + "## Themes\n\n- hooks — gist → [hooks](learnings/hooks.md)\n" + writeFile(t, dir, "LEARNINGS.md", root) + + got := Health(dir, Thresholds{Learnings: 30}) + for _, f := range got { + if f.Kind == foldable { + t.Errorf("unexpected foldable finding on a folded root: %+v", f) + } + } +} + +// M3: a theme file over the byte ceiling yields a heavy finding on that +// file, even when the root is lean. +func TestHealth_HeavyThemeFile(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "CONVENTIONS.md", + "# Conventions\n\n## Themes\n\n- x — g → [x](conventions/x.md)\n") + writeFile(t, dir, "conventions/x.md", + "# x\n\n"+strings.Repeat("padding line\n", 8000)) // ~100 KB + + got := Health(dir, Thresholds{Conventions: 12, PageBytes: 65536}) + var heavyFile string + for _, f := range got { + if f.Kind == heavy && strings.Contains(f.File, "conventions/x.md") { + heavyFile = f.File + } + } + if heavyFile == "" { + t.Errorf("no heavy finding on the fat theme file: %+v", got) + } +} + +// M4: a large un-migrated root trips both signals; foldable is ordered +// before heavy (folding reduces both). +func TestHealth_BothFire_FoldableFirst(t *testing.T) { + dir := t.TempDir() + // 40 entries and > 64 KB of body. + big := "# Learnings\n\n" + for i := 0; i < 40; i++ { + big += "## [2026-07-15-12000" + string(rune('0'+i%10)) + + "] e\n\n" + strings.Repeat("x", 2000) + "\n\n" + } + writeFile(t, dir, "LEARNINGS.md", big) + + got := Health(dir, Thresholds{Learnings: 30, PageBytes: 65536}) + if len(got) < 2 { + t.Fatalf("want both signals, got %+v", got) + } + if got[0].Kind != foldable { + t.Errorf("first finding = %+v, want foldable to lead", got[0]) + } + sawHeavy := false + for _, f := range got { + if f.Kind == heavy { + sawHeavy = true + } + } + if !sawHeavy { + t.Error("no heavy finding on an oversized root") + } +} + +// M5 (measure): conventions are counted by section, not lines — a short- +// section-count but long root does not foldable-nudge. +func TestHealth_ConventionMeasureIsSections(t *testing.T) { + dir := t.TempDir() + // 5 sections, each padded long (many lines) but only 5 sections. + root := "# Conventions\n\n" + for i := 0; i < 5; i++ { + root += "## Section " + string(rune('A'+i)) + "\n\n" + + strings.Repeat("- a rule line\n", 20) + "\n" + } + writeFile(t, dir, "CONVENTIONS.md", root) + + got := Health(dir, Thresholds{Conventions: 12}) // byte check disabled + for _, f := range got { + if f.Kind == foldable { + t.Errorf("5 sections should not foldable-nudge at 12: %+v", f) + } + } +} + +// M6/M7: threshold fires at > N (not >=), and 0 disables the check. +func TestHealth_BoundaryAndDisable(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "LEARNINGS.md", unmigratedLearnings(30)) + + // Exactly at threshold → no finding. + if got := Health(dir, Thresholds{Learnings: 30}); len(got) != 0 { + t.Errorf("count == threshold should not fire: %+v", got) + } + // One over → fires. + writeFile(t, dir, "LEARNINGS.md", unmigratedLearnings(31)) + if got := Health(dir, Thresholds{Learnings: 30}); len(got) != 1 { + t.Errorf("count > threshold should fire: %+v", got) + } + // Disabled (0) → never fires. + if got := Health(dir, Thresholds{Learnings: 0}); len(got) != 0 { + t.Errorf("threshold 0 should disable: %+v", got) + } +} diff --git a/internal/cli/system/core/knowledge/scan.go b/internal/cli/system/core/knowledge/scan.go new file mode 100644 index 00000000..03b88616 --- /dev/null +++ b/internal/cli/system/core/knowledge/scan.go @@ -0,0 +1,90 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +package knowledge + +import ( + "os" + "path/filepath" + "strings" + + "github.com/ActiveMemory/ctx/internal/assets/read/desc" + "github.com/ActiveMemory/ctx/internal/config/embed/text" + cfgFile "github.com/ActiveMemory/ctx/internal/config/file" + "github.com/ActiveMemory/ctx/internal/disclosure" +) + +// foldUnit is the foldability count's unit: sections for a convention +// root (its identity is the section title), entries for the timestamped +// kinds. +// +// Parameters: +// - kind: the root kind being measured +// +// Returns: +// - string: the resolved unit noun ("sections" | "entries") +func foldUnit(kind disclosure.Kind) string { + if kind == disclosure.KindConvention { + return desc.Text(text.DescKeyWriteKnowledgeUnitSections) + } + return desc.Text(text.DescKeyWriteKnowledgeUnitEntries) +} + +// heavyFinding builds a heavy-page finding for a page of size bytes. +// +// Parameters: +// - file: the page's display name (root basename or theme-file path) +// - size: the page's byte size +// - ceiling: the configured byte ceiling it exceeded +// +// Returns: +// - finding: a heavy-kind finding for the page +func heavyFinding(file string, size, ceiling int) finding { + return finding{ + Kind: heavy, File: file, Count: size, + Threshold: ceiling, + Unit: desc.Text(text.DescKeyWriteKnowledgeUnitBytes), + } +} + +// heavyThemeFiles returns heavy findings for a kind's theme files that +// exceed the byte ceiling, sized by stat rather than a full read. A kind +// with no theme directory (never folded) yields nothing. +// +// Parameters: +// - contextDir: absolute path to the context directory +// - kind: the root kind whose theme directory is scanned +// - ceiling: the byte ceiling a theme file must exceed to be flagged +// +// Returns: +// - []finding: heavy findings for oversized theme files, nil if none +func heavyThemeFiles( + contextDir string, kind disclosure.Kind, ceiling int, +) []finding { + noun, ok := disclosure.ThemeDir(kind) + if !ok { + return nil + } + entries, readErr := os.ReadDir(filepath.Join(contextDir, noun)) + if readErr != nil { + return nil + } + var out []finding + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), cfgFile.ExtMarkdown) { + continue + } + info, infoErr := e.Info() + if infoErr != nil { + continue + } + if int(info.Size()) > ceiling { + out = append(out, heavyFinding( + filepath.Join(noun, e.Name()), int(info.Size()), ceiling)) + } + } + return out +} diff --git a/internal/cli/system/core/knowledge/types.go b/internal/cli/system/core/knowledge/types.go index 296d8776..159c5131 100644 --- a/internal/cli/system/core/knowledge/types.go +++ b/internal/cli/system/core/knowledge/types.go @@ -6,15 +6,46 @@ package knowledge -// finding describes a single knowledge file that exceeds its -// configured threshold. +// signalKind distinguishes the two knowledge-health signals (M5): a +// foldable root (staging has accreted; the remedy is /ctx-digest) versus +// a heavy page (bytes over the ceiling; the remedy is split or +// extract-to-tooling). They answer different questions and carry +// different remedies, so the formatter routes on this. +type signalKind int + +const ( + // foldable is a staging-count finding on a canonical root. + foldable signalKind = iota + // heavy is a byte-weight finding on a page (a root or a theme file). + heavy +) + +// finding describes one knowledge-health signal on one page. type finding struct { - // File is the context filename (e.g., DECISIONS.md). + // Kind selects the signal (foldable | heavy) and thus the remedy. + Kind signalKind + // File is the display name of the page: a canonical basename + // (e.g. DECISIONS.md) for a root, or the context-relative theme-file + // path (e.g. conventions/code-style.md) for a heavy theme page. File string - // Count is the actual entry or line count. + // Count is the measured value — staged entries for foldable, bytes + // for heavy. Count int - // Threshold is the configured maximum. + // Threshold is the configured limit the measure exceeded. Threshold int - // Unit is the measurement unit ("entries" or "lines"). + // Unit is the measurement unit ("entries" | "sections" | "bytes"). Unit string } + +// Thresholds carries the four M5 knowledge-health limits. A zero in any +// field disables that check (the existing convention). +// +// Fields: +// - Learnings, Decisions, Conventions: staging-count ceilings per kind +// - PageBytes: the heavy-page byte ceiling (root and theme files) +type Thresholds struct { + Learnings int + Decisions int + Conventions int + PageBytes int +} diff --git a/internal/config/embed/text/check_knowledge.go b/internal/config/embed/text/check_knowledge.go index fa20aff2..45fddbc7 100644 --- a/internal/config/embed/text/check_knowledge.go +++ b/internal/config/embed/text/check_knowledge.go @@ -27,7 +27,9 @@ const ( // DescKeyWriteKnowledgeUnitEntries is the text key for write knowledge unit // entries messages. DescKeyWriteKnowledgeUnitEntries = "write.knowledge-unit-entries" - // DescKeyWriteKnowledgeUnitLines is the text key for write knowledge unit - // lines messages. - DescKeyWriteKnowledgeUnitLines = "write.knowledge-unit-lines" + // DescKeyWriteKnowledgeUnitSections is the unit for a convention root's + // staged-section foldability count. + DescKeyWriteKnowledgeUnitSections = "write.knowledge-unit-sections" + // DescKeyWriteKnowledgeUnitBytes is the unit for a heavy-page byte weight. + DescKeyWriteKnowledgeUnitBytes = "write.knowledge-unit-bytes" ) diff --git a/internal/config/runtime/doc.go b/internal/config/runtime/doc.go index ab2e1fee..ec701175 100644 --- a/internal/config/runtime/doc.go +++ b/internal/config/runtime/doc.go @@ -25,8 +25,13 @@ // [DefaultEntryCountDecisions] (20): entry // count thresholds that trigger consolidation // nudges. -// - [DefaultConventionLineCount] (200): line -// limit for CONVENTIONS.md before a nudge. +// - [DefaultConventionSectionCount] (12): staged +// `## ` sections in CONVENTIONS.md before a +// fold nudge (the watermark unit; replaces the +// old line count). +// - [DefaultThemePageByteCeiling] (65 536): byte +// ceiling above which a root or theme file is +// flagged heavy (split / extract-to-tooling). // - [DefaultInjectionTokenWarn] (15 000): token // count that triggers an oversize injection // warning. diff --git a/internal/config/runtime/runtime.go b/internal/config/runtime/runtime.go index 218d36c9..59bc7e5e 100644 --- a/internal/config/runtime/runtime.go +++ b/internal/config/runtime/runtime.go @@ -17,8 +17,16 @@ const ( DefaultEntryCountLearnings = 30 // DefaultEntryCountDecisions is the entry count threshold for DECISIONS.md. DefaultEntryCountDecisions = 20 - // DefaultConventionLineCount is the line count threshold for CONVENTIONS.md. - DefaultConventionLineCount = 200 + // DefaultConventionSectionCount is the staged-section count threshold + // for CONVENTIONS.md — the foldability watermark unit (M5). It replaces + // the former line-count measure: a section count is what the digest + // pass acts on, so it is the honest "should I fold?" signal. + DefaultConventionSectionCount = 12 + // DefaultThemePageByteCeiling is the byte ceiling above which a page + // (a root or a theme file) is flagged heavy (M5). Bytes, not lines: + // a line hides 10 or 200 characters. Past this, the advice is split or + // extract-to-tooling — an LLM is a poor linter. + DefaultThemePageByteCeiling = 65536 // DefaultInjectionTokenWarn is the token threshold for // oversize injection warning. DefaultInjectionTokenWarn = 15000 diff --git a/internal/rc/default.go b/internal/rc/default.go index 39a73093..739d353b 100644 --- a/internal/rc/default.go +++ b/internal/rc/default.go @@ -21,8 +21,10 @@ const ( DefaultEntryCountLearnings = runtime.DefaultEntryCountLearnings // DefaultEntryCountDecisions is the max decisions shown. DefaultEntryCountDecisions = runtime.DefaultEntryCountDecisions - // DefaultConventionLineCount is the max convention lines. - DefaultConventionLineCount = runtime.DefaultConventionLineCount + // DefaultConventionSectionCount is the staged-section fold threshold. + DefaultConventionSectionCount = runtime.DefaultConventionSectionCount + // DefaultThemePageByteCeiling is the heavy-page byte ceiling. + DefaultThemePageByteCeiling = runtime.DefaultThemePageByteCeiling // DefaultInjectionTokenWarn is the injection warn threshold. DefaultInjectionTokenWarn = runtime.DefaultInjectionTokenWarn // DefaultContextWindow is the default context window size. diff --git a/internal/rc/rc.go b/internal/rc/rc.go index 4e743190..c3518b0d 100644 --- a/internal/rc/rc.go +++ b/internal/rc/rc.go @@ -37,17 +37,18 @@ import ( // (8000 token budget, 7-day archive, etc.) func Default() *CtxRC { return &CtxRC{ - TokenBudget: DefaultTokenBudget, - PriorityOrder: nil, // nil means use config.ReadOrder - AutoArchive: true, - ArchiveAfterDays: DefaultArchiveAfterDays, - EntryCountLearnings: DefaultEntryCountLearnings, - EntryCountDecisions: DefaultEntryCountDecisions, - ConventionLineCount: DefaultConventionLineCount, - InjectionTokenWarn: DefaultInjectionTokenWarn, - ContextWindow: DefaultContextWindow, - TaskNudgeInterval: DefaultTaskNudgeInterval, - StaleAgeDays: DefaultStaleAgeDays, + TokenBudget: DefaultTokenBudget, + PriorityOrder: nil, // nil means use config.ReadOrder + AutoArchive: true, + ArchiveAfterDays: DefaultArchiveAfterDays, + EntryCountLearnings: DefaultEntryCountLearnings, + EntryCountDecisions: DefaultEntryCountDecisions, + ConventionSections: DefaultConventionSectionCount, + ThemePageByteCeiling: DefaultThemePageByteCeiling, + InjectionTokenWarn: DefaultInjectionTokenWarn, + ContextWindow: DefaultContextWindow, + TaskNudgeInterval: DefaultTaskNudgeInterval, + StaleAgeDays: DefaultStaleAgeDays, } } @@ -200,14 +201,27 @@ func EntryCountDecisions() int { return RC().EntryCountDecisions } -// ConventionLineCount returns the line count threshold for CONVENTIONS.md. +// ConventionSectionCount returns the staged-section count threshold for +// CONVENTIONS.md — the foldability watermark unit (M5). It replaces the +// former line-count measure. // -// Returns 0 if the check is disabled. Default: 200. +// Returns 0 if the check is disabled. Default: 12. // // Returns: -// - int: Threshold above which a drift warning is emitted -func ConventionLineCount() int { - return RC().ConventionLineCount +// - int: staged-section count above which a fold nudge is emitted +func ConventionSectionCount() int { + return RC().ConventionSections +} + +// ThemePageByteCeiling returns the byte ceiling above which a page (a +// root or a theme file) is flagged heavy (M5). +// +// Returns 0 if the check is disabled. Default: 65536. +// +// Returns: +// - int: byte size above which a split/extract nudge is emitted +func ThemePageByteCeiling() int { + return RC().ThemePageByteCeiling } // InjectionTokenWarn returns the token threshold for diff --git a/internal/rc/types.go b/internal/rc/types.go index 6aba0ff5..ed884fde 100644 --- a/internal/rc/types.go +++ b/internal/rc/types.go @@ -97,7 +97,8 @@ type CtxRC struct { ScratchpadEncrypt *bool `yaml:"scratchpad_encrypt"` EntryCountLearnings int `yaml:"entry_count_learnings"` EntryCountDecisions int `yaml:"entry_count_decisions"` - ConventionLineCount int `yaml:"convention_line_count"` + ConventionSections int `yaml:"convention_section_count"` + ThemePageByteCeiling int `yaml:"theme_page_byte_ceiling"` InjectionTokenWarn int `yaml:"injection_token_warn"` ContextWindow int `yaml:"context_window"` BillingTokenWarn int `yaml:"billing_token_warn"` diff --git a/internal/rc/validate_test.go b/internal/rc/validate_test.go index 748c3200..f54149da 100644 --- a/internal/rc/validate_test.go +++ b/internal/rc/validate_test.go @@ -90,7 +90,8 @@ archive_after_days: 7 scratchpad_encrypt: true entry_count_learnings: 30 entry_count_decisions: 20 -convention_line_count: 200 +convention_section_count: 12 +theme_page_byte_ceiling: 65536 injection_token_warn: 15000 context_window: 200000 event_log: false diff --git a/specs/plans/pd-m5.md b/specs/plans/pd-m5.md index 0b955a77..4b0eccbe 100644 --- a/specs/plans/pd-m5.md +++ b/specs/plans/pd-m5.md @@ -74,25 +74,25 @@ DoD: | id | st | task | deps | files | [P] | acceptance criterion | spec ref | |---|---|---|---|---|---|---|---| -| T01 | [ ] | runtime: retire `DefaultConventionLineCount`; add `DefaultConventionSectionCount=12`, `DefaultThemePageByteCeiling=65536` | — | `internal/config/runtime/runtime.go`, `doc.go` | [P] | build; `grep DefaultConventionLineCount` → none | Triggers/Config | -| T02 | [ ] | rc types+accessors: retire `ConventionLineCount`; add `ConventionSectionCount`, `ThemePageByteCeiling` | T01 | `internal/rc/types.go`, `rc.go`, `default.go` | | unit: accessors return defaults; `rc.ConventionSectionCount()==12` | Config | -| T03 | [ ] | retire `ConventionLineCount` refs project-wide | T02 | `internal/**`, validate_test yaml keys | | `grep -rn ConventionLineCount internal/` → no non-test hits; config test green | Config | -| T04 | [ ] | `finding` type: add `Kind` (foldable\|heavy) + `Path`; extend `Unit` | — | `internal/cli/system/core/knowledge/types.go` | [P] | unit: zero-value + both kinds construct | Data model | -| T05 | [ ] | foldable signal: count staging via `disclosure.StagedEntries` per kind (replaces ParseEntryBlocks + line-count) | T02,T04 | `internal/cli/system/core/knowledge/knowledge.go` | | unit: un-migrated 40-entry → foldable(40); folded → none (**M1,M2**) | Signal 1 | -| T06 | [ ] | heavy signal: byte scan of the root file | T04 | `knowledge.go` | | unit: root > ceiling → heavy finding on root path | Signal 2 | -| T07 | [ ] | heavy signal: byte scan of theme files via `disclosure.ThemeDir(kind)` | T04,T06 | `knowledge.go` | | unit: 80 KB theme file → heavy finding on that file (**M3**) | Signal 2 | -| T08 | [ ] | `knowledge.Health`: combine signals; both-fire → foldable first | T05,T06,T07 | `knowledge.go` | | unit: large un-migrated root → foldable ordered before heavy (**M4**) | Contracts | +| T01 | [x] | runtime: retire `DefaultConventionLineCount`; add `DefaultConventionSectionCount=12`, `DefaultThemePageByteCeiling=65536` | — | `internal/config/runtime/runtime.go`, `doc.go` | [P] | build; `grep DefaultConventionLineCount` → none | Triggers/Config | +| T02 | [x] | rc types+accessors: retire `ConventionLineCount`; add `ConventionSectionCount`, `ThemePageByteCeiling` | T01 | `internal/rc/types.go`, `rc.go`, `default.go` | | unit: accessors return defaults; `rc.ConventionSectionCount()==12` | Config | +| T03 | [x] | retire `ConventionLineCount` refs project-wide | T02 | `internal/**`, validate_test yaml keys | | `grep -rn ConventionLineCount internal/` → no non-test hits; config test green | Config | +| T04 | [x] | `finding` type: add `Kind` (foldable\|heavy) + `Path`; extend `Unit` | — | `internal/cli/system/core/knowledge/types.go` | [P] | unit: zero-value + both kinds construct | Data model | +| T05 | [x] | foldable signal: count staging via `disclosure.StagedEntries` per kind (replaces ParseEntryBlocks + line-count) | T02,T04 | `internal/cli/system/core/knowledge/knowledge.go` | | unit: un-migrated 40-entry → foldable(40); folded → none (**M1,M2**) | Signal 1 | +| T06 | [x] | heavy signal: byte scan of the root file | T04 | `knowledge.go` | | unit: root > ceiling → heavy finding on root path | Signal 2 | +| T07 | [x] | heavy signal: byte scan of theme files via `disclosure.ThemeDir(kind)` | T04,T06 | `knowledge.go` | | unit: 80 KB theme file → heavy finding on that file (**M3**) | Signal 2 | +| T08 | [x] | `knowledge.Health`: combine signals; both-fire → foldable first | T05,T06,T07 | `knowledge.go` | | unit: large un-migrated root → foldable ordered before heavy (**M4**) | Contracts | | T09 | [ ] | warning text: foldable→/ctx-digest primary (consolidate/drift secondary); heavy→split/extract | T08 | `internal/assets/commands/text/hooks.yaml`, `internal/assets/hooks/messages/check-knowledge/warning.txt` | | build embeds; message-registry test green; text names /ctx-digest | Contracts | | T10 | [ ] | `FormatWarnings`/`EmitWarning` route by `Kind` | T08,T09 | `knowledge.go` | | unit: foldable vs heavy render distinct remedy lines | Contracts | | T11 | [ ] | `ctx system check-knowledge` report path the skills call | T08 | `internal/cli/system/cmd/checkknowledge/*.go` | | e2e: command prints findings for an oversized fixture root | Contracts | | T12 | [ ] | hook wiring: `CheckHealth` uses `Health`; throttle/log-first unchanged | T08,T10 | `knowledge.go` | | e2e: hook nudge names /ctx-digest for a foldable root; throttled once/day | Triggers | | T13 | [ ] | `/ctx-remember` skill: surface foldable/heavy at session start (read-only) | T11 | `internal/assets/claude/skills/ctx-remember/SKILL.md` | [P] | frontmatter test green; body calls the report path, relays suggestions | Triggers | | T14 | [ ] | `/ctx-wrap-up` skill: surface at session end, suggest-only never inline | T11 | `internal/assets/claude/skills/ctx-wrap-up/SKILL.md` | [P] | frontmatter test green; body surfaces + explicitly does not fold | Triggers | -| T15 | [ ] | `Health` unit tests: foldable, folded-quiet fixtures | T05 | `internal/cli/system/core/knowledge/*_test.go` | | **M1,M2** pass | Tests | -| T16 | [ ] | `Health` unit tests: heavy root + heavy theme file | T07 | `knowledge` test | [P] | **M3** pass | Tests | -| T17 | [ ] | `Health` unit test: both-fire ordering | T08 | `knowledge` test | [P] | **M4** pass | Tests | -| T18 | [ ] | convention measure test: sections not lines | T05 | `knowledge` test | [P] | **M5** pass | Tests | -| T19 | [ ] | boundary + disable tests (`>N`, `0` disables) | T08 | `knowledge` test | [P] | **M6,M7** pass | Tests | +| T15 | [x] | `Health` unit tests: foldable, folded-quiet fixtures | T05 | `internal/cli/system/core/knowledge/*_test.go` | | **M1,M2** pass | Tests | +| T16 | [x] | `Health` unit tests: heavy root + heavy theme file | T07 | `knowledge` test | [P] | **M3** pass | Tests | +| T17 | [x] | `Health` unit test: both-fire ordering | T08 | `knowledge` test | [P] | **M4** pass | Tests | +| T18 | [x] | convention measure test: sections not lines | T05 | `knowledge` test | [P] | **M5** pass | Tests | +| T19 | [x] | boundary + disable tests (`>N`, `0` disables) | T08 | `knowledge` test | [P] | **M6,M7** pass | Tests | | T20 | [ ] | surface parity test: hook findings == report findings | T11,T12 | `knowledge`/checkknowledge test | | **M8** pass | Tests | | T21 | [ ] | copilot skill sync | T13,T14 | `internal/assets/integrations/copilot-cli/skills/**` | [P] | `make check-copilot-skills` green | Skill | | T22 | [ ] |XX **measurement gate**: drive Health on a realistic .context (oversized root + fat theme file) | T12,T11 | scratchpad fixture | | foldable + heavy both surface; suggestions correct; throttle holds | Risks | From 532558f17199fda229e1c4ce6bd7775c09c9d4fd Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Sat, 25 Jul 2026 20:42:26 -0700 Subject: [PATCH 5/6] feat(pd-m5): per-kind nudge remedies + on-demand report path (E3) Warning text routes by signal kind: foldable roots lead with /ctx-digest (consolidate/drift demoted to secondary), heavy pages advise split-or-extract-to-tooling. FormatWarnings groups findings by kind and attaches each remedy; the generic remedy block is retired from the template and the now-dead fallback key removed. New knowledge.Report (no throttle, no relay) and a ctx system check-knowledge --report path the /ctx-remember and /ctx-wrap-up skills call for the live state on demand. thresholds() shared by CheckHealth and Report. Flag wired via flagbind + a system flag desc key; print via write/setup. Spec: specs/progressive-disclosure.md Signed-off-by: Jose Alekhinne --- internal/assets/commands/flags.yaml | 2 + internal/assets/commands/text/hooks.yaml | 15 ++--- .../messages/check-knowledge/warning.txt | 5 -- internal/cli/system/cmd/checkknowledge/cmd.go | 14 ++++- internal/cli/system/cmd/checkknowledge/run.go | 24 +++++++ .../cli/system/core/knowledge/knowledge.go | 63 ++++++++++++++----- internal/cli/system/core/knowledge/scan.go | 15 +++++ internal/config/embed/flag/system.go | 3 + internal/config/embed/text/check_knowledge.go | 9 ++- internal/config/flag/flag.go | 1 + specs/plans/pd-m5.md | 8 +-- 11 files changed, 125 insertions(+), 34 deletions(-) diff --git a/internal/assets/commands/flags.yaml b/internal/assets/commands/flags.yaml index 1238b2bc..e8c4d703 100644 --- a/internal/assets/commands/flags.yaml +++ b/internal/assets/commands/flags.yaml @@ -263,6 +263,8 @@ usage.last: short: Show last N entries usage.session: short: Filter by session ID (prefix match) +system.checkknowledge.report: + short: Print current knowledge-health findings on demand (skills), bypassing the daily throttle system.markjournal.check: short: Check if stage is set (exit 1 if not) system.pause.session-id: diff --git a/internal/assets/commands/text/hooks.yaml b/internal/assets/commands/text/hooks.yaml index dfa60d5f..9b790d3a 100644 --- a/internal/assets/commands/text/hooks.yaml +++ b/internal/assets/commands/text/hooks.yaml @@ -166,16 +166,17 @@ check-journal.relay-prefix: short: 'IMPORTANT: Relay this journal reminder to the user VERBATIM before answering their question.' check-knowledge.box-title: short: Knowledge File Growth -check-knowledge.fallback: - short: |- - Large knowledge files dilute agent context. Consider: - • Review and remove outdated entries - • Use /ctx-consolidate to merge overlapping entries - • Use /ctx-drift for semantic drift (stale patterns) - • Move stale entries to .context/archive/ manually check-knowledge.finding-format: short: | %s has %d %s (recommended: ≤%d). +check-knowledge.remedy-foldable: + short: |2 + • Run /ctx-digest to fold the staging zone into themes + • /ctx-consolidate or /ctx-drift if entries overlap rather than accrete +check-knowledge.remedy-heavy: + short: |2 + • Split the theme, or extract it to tooling — a context file this + heavy is a linter in prose, and an LLM is a poor linter check-knowledge.relay-message: short: Knowledge file growth detected check-knowledge.relay-prefix: diff --git a/internal/assets/hooks/messages/check-knowledge/warning.txt b/internal/assets/hooks/messages/check-knowledge/warning.txt index ca466036..d72d4ae7 100644 --- a/internal/assets/hooks/messages/check-knowledge/warning.txt +++ b/internal/assets/hooks/messages/check-knowledge/warning.txt @@ -1,6 +1 @@ {{.FileWarnings}} -Large knowledge files dilute agent context. Consider: - • Review and remove outdated entries - • Use /ctx-consolidate to merge overlapping entries - • Use /ctx-drift for semantic drift (stale patterns) - • Move stale entries to .context/archive/ manually \ No newline at end of file diff --git a/internal/cli/system/cmd/checkknowledge/cmd.go b/internal/cli/system/cmd/checkknowledge/cmd.go index 4120402b..174d4459 100644 --- a/internal/cli/system/cmd/checkknowledge/cmd.go +++ b/internal/cli/system/cmd/checkknowledge/cmd.go @@ -13,6 +13,9 @@ import ( "github.com/ActiveMemory/ctx/internal/assets/read/desc" "github.com/ActiveMemory/ctx/internal/config/embed/cmd" + embedFlag "github.com/ActiveMemory/ctx/internal/config/embed/flag" + cFlag "github.com/ActiveMemory/ctx/internal/config/flag" + "github.com/ActiveMemory/ctx/internal/flagbind" ) // Cmd returns the "ctx system check-knowledge" subcommand. @@ -22,14 +25,23 @@ import ( func Cmd() *cobra.Command { short, long := desc.Command(cmd.DescKeySystemCheckKnowledge) - return &cobra.Command{ + var report bool + c := &cobra.Command{ Use: cmd.UseSystemCheckKnowledge, Short: short, Long: long, Example: desc.Example(cmd.DescKeySystemCheckKnowledge), Hidden: true, RunE: func(cmd *cobra.Command, _ []string) error { + // --report is the on-demand skill path (no stdin, no + // throttle, no relay); bare invocation is the hook path. + if report { + return RunReport(cmd) + } return Run(cmd, os.Stdin) }, } + flagbind.BoolFlag(c, &report, + cFlag.Report, embedFlag.DescKeySystemCheckKnowledgeReport) + return c } diff --git a/internal/cli/system/cmd/checkknowledge/run.go b/internal/cli/system/cmd/checkknowledge/run.go index bafc3e32..2acc6e3e 100644 --- a/internal/cli/system/cmd/checkknowledge/run.go +++ b/internal/cli/system/cmd/checkknowledge/run.go @@ -16,9 +16,33 @@ import ( coreKnowledge "github.com/ActiveMemory/ctx/internal/cli/system/core/knowledge" "github.com/ActiveMemory/ctx/internal/config/knowledge" internalIo "github.com/ActiveMemory/ctx/internal/io" + "github.com/ActiveMemory/ctx/internal/rc" writeSetup "github.com/ActiveMemory/ctx/internal/write/setup" ) +// RunReport prints the current knowledge-health findings on demand, for the +// /ctx-remember and /ctx-wrap-up skills to relay. Unlike the hook [Run] +// it reads no stdin, does not throttle, and does not relay: the skills +// call it deliberately at session boundaries and want the live state. +// Prints nothing when every root is within limits (a clean session start +// says nothing about knowledge health). +// +// Parameters: +// - cmd: Cobra command for output +// +// Returns: +// - error: nil; a missing context directory is not an error here +func RunReport(cmd *cobra.Command) error { + ctxDir, dirErr := rc.ContextDir() + if dirErr != nil { + return nil + } + if report := coreKnowledge.Report(ctxDir); report != "" { + writeSetup.Nudge(cmd, report) + } + return nil +} + // Run executes the check-knowledge hook logic. // // Reads hook input from stdin, checks knowledge file sizes against diff --git a/internal/cli/system/core/knowledge/knowledge.go b/internal/cli/system/core/knowledge/knowledge.go index 14a576f6..510a4a9f 100644 --- a/internal/cli/system/core/knowledge/knowledge.go +++ b/internal/cli/system/core/knowledge/knowledge.go @@ -21,7 +21,6 @@ import ( "github.com/ActiveMemory/ctx/internal/disclosure" "github.com/ActiveMemory/ctx/internal/io" "github.com/ActiveMemory/ctx/internal/notify" - "github.com/ActiveMemory/ctx/internal/rc" ) // Health scans the canonical knowledge roots and returns the two M5 @@ -100,11 +99,33 @@ func Health(contextDir string, t Thresholds) []finding { // Returns: // - string: formatted warning lines for template injection func FormatWarnings(findings []finding) string { - var b strings.Builder - findingFmt := desc.Text(text.DescKeyCheckKnowledgeFindingFormat) + var foldables, heavies []finding for _, f := range findings { - io.SafeFprintf(&b, findingFmt, f.File, f.Count, f.Unit, f.Threshold) + if f.Kind == heavy { + heavies = append(heavies, f) + continue + } + foldables = append(foldables, f) + } + + findingFmt := desc.Text(text.DescKeyCheckKnowledgeFindingFormat) + var b strings.Builder + writeGroup := func(group []finding, remedyKey string) { + if len(group) == 0 { + return + } + if b.Len() > 0 { + b.WriteString(token.NewlineLF) + } + for _, f := range group { + io.SafeFprintf(&b, findingFmt, f.File, f.Count, f.Unit, f.Threshold) + } + b.WriteString(desc.Text(remedyKey)) } + // Foldable first: when a root trips both, folding is the move that + // reduces both, so its remedy leads. + writeGroup(foldables, text.DescKeyCheckKnowledgeRemedyFoldable) + writeGroup(heavies, text.DescKeyCheckKnowledgeRemedyHeavy) return b.String() } @@ -120,11 +141,11 @@ func FormatWarnings(findings []finding) string { // honor the log-first principle: if the relay audit entry or // webhook fails, the nudge box should not be printed. func EmitWarning(sessionID, fileWarnings string) (string, error) { - fallback := fileWarnings + token.NewlineLF + desc.Text( - text.DescKeyCheckKnowledgeFallback, - ) + // fileWarnings already carries its per-kind remedy lines (see + // FormatWarnings), so it is a complete fallback on template-load + // failure — no generic remedy to append. content := message.Load(hook.CheckKnowledge, hook.VariantWarning, - map[string]any{knowledge.VarFileWarnings: fileWarnings}, fallback) + map[string]any{knowledge.VarFileWarnings: fileWarnings}, fileWarnings) if content == "" { return "", nil } @@ -163,12 +184,7 @@ func EmitWarning(sessionID, fileWarnings string) (string, error) { // log-first principle and skip printing the box when the relay // audit entry could not be written. func CheckHealth(sessionID, ctxDir string) (string, bool, error) { - t := Thresholds{ - Learnings: rc.EntryCountLearnings(), - Decisions: rc.EntryCountDecisions(), - Conventions: rc.ConventionSectionCount(), - PageBytes: rc.ThemePageByteCeiling(), - } + t := thresholds() // All disabled - nothing to check if t.Learnings == 0 && t.Decisions == 0 && @@ -188,3 +204,22 @@ func CheckHealth(sessionID, ctxDir string) (string, bool, error) { } return box, true, nil } + +// Report returns the formatted knowledge-health findings for on-demand +// display by the /ctx-remember and /ctx-wrap-up skills. Unlike +// [CheckHealth] it neither throttles nor relays: the skills call it +// deliberately, want the current state every time, and must not spam the +// hook audit trail. Empty string when every root is within limits. +// +// Parameters: +// - ctxDir: absolute path to the context directory +// +// Returns: +// - string: the formatted findings + per-kind remedies, or "" if clean +func Report(ctxDir string) string { + findings := Health(ctxDir, thresholds()) + if len(findings) == 0 { + return "" + } + return FormatWarnings(findings) +} diff --git a/internal/cli/system/core/knowledge/scan.go b/internal/cli/system/core/knowledge/scan.go index 03b88616..4896be73 100644 --- a/internal/cli/system/core/knowledge/scan.go +++ b/internal/cli/system/core/knowledge/scan.go @@ -15,8 +15,23 @@ import ( "github.com/ActiveMemory/ctx/internal/config/embed/text" cfgFile "github.com/ActiveMemory/ctx/internal/config/file" "github.com/ActiveMemory/ctx/internal/disclosure" + "github.com/ActiveMemory/ctx/internal/rc" ) +// thresholds builds the four M5 limits from rc, shared by CheckHealth +// (the throttled hook path) and Report (the on-demand skill path). +// +// Returns: +// - Thresholds: the current rc-configured limits +func thresholds() Thresholds { + return Thresholds{ + Learnings: rc.EntryCountLearnings(), + Decisions: rc.EntryCountDecisions(), + Conventions: rc.ConventionSectionCount(), + PageBytes: rc.ThemePageByteCeiling(), + } +} + // foldUnit is the foldability count's unit: sections for a convention // root (its identity is the section title), entries for the timestamped // kinds. diff --git a/internal/config/embed/flag/system.go b/internal/config/embed/flag/system.go index ea46ed71..6af393b9 100644 --- a/internal/config/embed/flag/system.go +++ b/internal/config/embed/flag/system.go @@ -12,6 +12,9 @@ package flag // prune, resource, stats) live in their own per-command files in // this package since those commands were promoted to top-level. const ( + // DescKeySystemCheckKnowledgeReport is the description key for the system + // check-knowledge --report flag. + DescKeySystemCheckKnowledgeReport = "system.checkknowledge.report" // DescKeySystemMarkJournalCheck is the description key for the system mark // journal check flag. DescKeySystemMarkJournalCheck = "system.markjournal.check" diff --git a/internal/config/embed/text/check_knowledge.go b/internal/config/embed/text/check_knowledge.go index 45fddbc7..820701dd 100644 --- a/internal/config/embed/text/check_knowledge.go +++ b/internal/config/embed/text/check_knowledge.go @@ -11,12 +11,15 @@ const ( // DescKeyCheckKnowledgeBoxTitle is the text key for check knowledge box title // messages. DescKeyCheckKnowledgeBoxTitle = "check-knowledge.box-title" - // DescKeyCheckKnowledgeFallback is the text key for check knowledge fallback - // messages. - DescKeyCheckKnowledgeFallback = "check-knowledge.fallback" // DescKeyCheckKnowledgeFindingFormat is the text key for check knowledge // finding format messages. DescKeyCheckKnowledgeFindingFormat = "check-knowledge.finding-format" + // DescKeyCheckKnowledgeRemedyFoldable is the remedy line for foldable + // roots (accreted staging): fold via /ctx-digest. + DescKeyCheckKnowledgeRemedyFoldable = "check-knowledge.remedy-foldable" + // DescKeyCheckKnowledgeRemedyHeavy is the remedy line for heavy pages + // (over the byte ceiling): split or extract-to-tooling. + DescKeyCheckKnowledgeRemedyHeavy = "check-knowledge.remedy-heavy" // DescKeyCheckKnowledgeRelayMessage is the text key for check knowledge relay // message messages. DescKeyCheckKnowledgeRelayMessage = "check-knowledge.relay-message" diff --git a/internal/config/flag/flag.go b/internal/config/flag/flag.go index 420117ee..e8f7c017 100644 --- a/internal/config/flag/flag.go +++ b/internal/config/flag/flag.go @@ -104,6 +104,7 @@ const ( Scope = "scope" Peers = "peers" Port = "port" + Report = "report" Serve = "serve" Share = "share" Show = "show" diff --git a/specs/plans/pd-m5.md b/specs/plans/pd-m5.md index 4b0eccbe..dd3342d6 100644 --- a/specs/plans/pd-m5.md +++ b/specs/plans/pd-m5.md @@ -82,10 +82,10 @@ DoD: | T06 | [x] | heavy signal: byte scan of the root file | T04 | `knowledge.go` | | unit: root > ceiling → heavy finding on root path | Signal 2 | | T07 | [x] | heavy signal: byte scan of theme files via `disclosure.ThemeDir(kind)` | T04,T06 | `knowledge.go` | | unit: 80 KB theme file → heavy finding on that file (**M3**) | Signal 2 | | T08 | [x] | `knowledge.Health`: combine signals; both-fire → foldable first | T05,T06,T07 | `knowledge.go` | | unit: large un-migrated root → foldable ordered before heavy (**M4**) | Contracts | -| T09 | [ ] | warning text: foldable→/ctx-digest primary (consolidate/drift secondary); heavy→split/extract | T08 | `internal/assets/commands/text/hooks.yaml`, `internal/assets/hooks/messages/check-knowledge/warning.txt` | | build embeds; message-registry test green; text names /ctx-digest | Contracts | -| T10 | [ ] | `FormatWarnings`/`EmitWarning` route by `Kind` | T08,T09 | `knowledge.go` | | unit: foldable vs heavy render distinct remedy lines | Contracts | -| T11 | [ ] | `ctx system check-knowledge` report path the skills call | T08 | `internal/cli/system/cmd/checkknowledge/*.go` | | e2e: command prints findings for an oversized fixture root | Contracts | -| T12 | [ ] | hook wiring: `CheckHealth` uses `Health`; throttle/log-first unchanged | T08,T10 | `knowledge.go` | | e2e: hook nudge names /ctx-digest for a foldable root; throttled once/day | Triggers | +| T09 | [x] | warning text: foldable→/ctx-digest primary (consolidate/drift secondary); heavy→split/extract | T08 | `internal/assets/commands/text/hooks.yaml`, `internal/assets/hooks/messages/check-knowledge/warning.txt` | | build embeds; message-registry test green; text names /ctx-digest | Contracts | +| T10 | [x] | `FormatWarnings`/`EmitWarning` route by `Kind` | T08,T09 | `knowledge.go` | | unit: foldable vs heavy render distinct remedy lines | Contracts | +| T11 | [x] | `ctx system check-knowledge` report path the skills call | T08 | `internal/cli/system/cmd/checkknowledge/*.go` | | e2e: command prints findings for an oversized fixture root | Contracts | +| T12 | [x] | hook wiring: `CheckHealth` uses `Health`; throttle/log-first unchanged | T08,T10 | `knowledge.go` | | e2e: hook nudge names /ctx-digest for a foldable root; throttled once/day | Triggers | | T13 | [ ] | `/ctx-remember` skill: surface foldable/heavy at session start (read-only) | T11 | `internal/assets/claude/skills/ctx-remember/SKILL.md` | [P] | frontmatter test green; body calls the report path, relays suggestions | Triggers | | T14 | [ ] | `/ctx-wrap-up` skill: surface at session end, suggest-only never inline | T11 | `internal/assets/claude/skills/ctx-wrap-up/SKILL.md` | [P] | frontmatter test green; body surfaces + explicitly does not fold | Triggers | | T15 | [x] | `Health` unit tests: foldable, folded-quiet fixtures | T05 | `internal/cli/system/core/knowledge/*_test.go` | | **M1,M2** pass | Tests | From 5e4649bb73ce8b4d971b92995feb6cd571e6d587 Mon Sep 17 00:00:00 2001 From: Jose Alekhinne Date: Sat, 25 Jul 2026 20:49:32 -0700 Subject: [PATCH 6/6] feat(pd-m5): wire /ctx-remember + /ctx-wrap-up, parity test, ledger (E4-E6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E4: /ctx-remember runs 'ctx system check-knowledge --report' after recall and surfaces foldable/heavy roots in the readback (read-only); /ctx-wrap-up surfaces them as a next-session suggestion and explicitly never folds inline (the human is leaving). Copilot skills synced. E6: surface-parity test (M8) — the hook path and the report path derive from one Health scan, so the report text is exactly what the nudge box wraps. Measurement gate (T22) driven on a realistic .context (35-entry LEARNINGS + 91 KB theme file): both signals surface, foldable first, with correct remedies. Milestone gate (T23): make lint 0, go test green, make audit pass. All 23 tasks and every DoD box complete. Spec: specs/progressive-disclosure.md Signed-off-by: Jose Alekhinne --- .../claude/skills/ctx-remember/SKILL.md | 22 ++++++++++++- .../assets/claude/skills/ctx-wrap-up/SKILL.md | 18 +++++++++++ .../copilot-cli/skills/ctx-remember/SKILL.md | 22 ++++++++++++- .../copilot-cli/skills/ctx-wrap-up/SKILL.md | 18 +++++++++++ .../system/core/knowledge/knowledge_test.go | 31 +++++++++++++++++++ .../system/core/knowledge/testmain_test.go | 22 +++++++++++++ specs/plans/pd-m5.md | 24 +++++++------- 7 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 internal/cli/system/core/knowledge/testmain_test.go diff --git a/internal/assets/claude/skills/ctx-remember/SKILL.md b/internal/assets/claude/skills/ctx-remember/SKILL.md index ae91f1a6..de52ff29 100644 --- a/internal/assets/claude/skills/ctx-remember/SKILL.md +++ b/internal/assets/claude/skills/ctx-remember/SKILL.md @@ -62,7 +62,21 @@ feel like a file search rather than genuine recall: `/ctx-remember` does not run any editorial pass. If the directory does not exist or holds no postdated entries, skip the step. -6. **Present the structured readback** (see format below) +6. **Check knowledge health (read-only).** Run: + + ```bash + ctx system check-knowledge --report + ``` + + It prints nothing when every canonical root is within limits. + When it prints findings, surface them in the readback's + **Knowledge health** line: a *foldable* root suggests + `/ctx-digest` (fold the staging zone into themes); a *heavy* + page suggests splitting the theme or extracting it to tooling. + Suggest only — never run `/ctx-digest` or edit a file here. + If it prints nothing, omit the line. + +7. **Present the structured readback** (see format below) ## Readback Format @@ -80,6 +94,12 @@ relevant. Pick the most recent or most impactful. **Next step**: Suggest what to work on next based on the active tasks, or ask the user for direction if priorities are unclear. +**Knowledge health** (only when `ctx system check-knowledge +--report` printed findings): name the foldable roots and/or heavy +pages it reported and the suggested remedy (`/ctx-digest`, or +split/extract). Suggestion only; omit the line entirely when the +report was empty. + ## Readback Rules - Open directly with the readback: instead of "I don't have memory", diff --git a/internal/assets/claude/skills/ctx-wrap-up/SKILL.md b/internal/assets/claude/skills/ctx-wrap-up/SKILL.md index 2b469333..9cf5da40 100644 --- a/internal/assets/claude/skills/ctx-wrap-up/SKILL.md +++ b/internal/assets/claude/skills/ctx-wrap-up/SKILL.md @@ -171,6 +171,24 @@ Do not auto-commit; the user decides. But always run the `git status` check and always surface non-empty output. Do not skip this phase silently when the working tree is dirty. +### Phase 4.4: Surface knowledge health (suggest-only) + +Run: + +```bash +ctx system check-knowledge --report +``` + +When it prints findings, surface them as a closing suggestion: +a *foldable* root → run `/ctx-digest` **next session**; a *heavy* +page → split the theme or extract it to tooling. Prints nothing +when every root is within limits — then say nothing. + +**Never fold inline here.** The human is closing the laptop to go +live their life; running a semantic pass at wrap-up is against +their interest (spec: progressive-disclosure `### Triggers`). This +phase suggests for *next* session; it does not act. + ### Phase 4.5: Capture the session journal (best-effort) Before handing over, sweep this session — and any others that diff --git a/internal/assets/integrations/copilot-cli/skills/ctx-remember/SKILL.md b/internal/assets/integrations/copilot-cli/skills/ctx-remember/SKILL.md index ac30089a..02576447 100644 --- a/internal/assets/integrations/copilot-cli/skills/ctx-remember/SKILL.md +++ b/internal/assets/integrations/copilot-cli/skills/ctx-remember/SKILL.md @@ -61,7 +61,21 @@ feel like a file search rather than genuine recall: `/ctx-remember` does not run any editorial pass. If the directory does not exist or holds no postdated entries, skip the step. -6. **Present the structured readback** (see format below) +6. **Check knowledge health (read-only).** Run: + + ```bash + ctx system check-knowledge --report + ``` + + It prints nothing when every canonical root is within limits. + When it prints findings, surface them in the readback's + **Knowledge health** line: a *foldable* root suggests + `/ctx-digest` (fold the staging zone into themes); a *heavy* + page suggests splitting the theme or extracting it to tooling. + Suggest only — never run `/ctx-digest` or edit a file here. + If it prints nothing, omit the line. + +7. **Present the structured readback** (see format below) ## Readback Format @@ -79,6 +93,12 @@ relevant. Pick the most recent or most impactful. **Next step**: Suggest what to work on next based on the active tasks, or ask the user for direction if priorities are unclear. +**Knowledge health** (only when `ctx system check-knowledge +--report` printed findings): name the foldable roots and/or heavy +pages it reported and the suggested remedy (`/ctx-digest`, or +split/extract). Suggestion only; omit the line entirely when the +report was empty. + ## Readback Rules - Open directly with the readback: instead of "I don't have memory", diff --git a/internal/assets/integrations/copilot-cli/skills/ctx-wrap-up/SKILL.md b/internal/assets/integrations/copilot-cli/skills/ctx-wrap-up/SKILL.md index ee581fea..db156de6 100644 --- a/internal/assets/integrations/copilot-cli/skills/ctx-wrap-up/SKILL.md +++ b/internal/assets/integrations/copilot-cli/skills/ctx-wrap-up/SKILL.md @@ -170,6 +170,24 @@ Do not auto-commit; the user decides. But always run the `git status` check and always surface non-empty output. Do not skip this phase silently when the working tree is dirty. +### Phase 4.4: Surface knowledge health (suggest-only) + +Run: + +```bash +ctx system check-knowledge --report +``` + +When it prints findings, surface them as a closing suggestion: +a *foldable* root → run `/ctx-digest` **next session**; a *heavy* +page → split the theme or extract it to tooling. Prints nothing +when every root is within limits — then say nothing. + +**Never fold inline here.** The human is closing the laptop to go +live their life; running a semantic pass at wrap-up is against +their interest (spec: progressive-disclosure `### Triggers`). This +phase suggests for *next* session; it does not act. + ### Phase 4.5: Capture the session journal (best-effort) Before handing over, sweep this session — and any others that diff --git a/internal/cli/system/core/knowledge/knowledge_test.go b/internal/cli/system/core/knowledge/knowledge_test.go index 5941dfc4..69aa9f1b 100644 --- a/internal/cli/system/core/knowledge/knowledge_test.go +++ b/internal/cli/system/core/knowledge/knowledge_test.go @@ -163,3 +163,34 @@ func TestHealth_BoundaryAndDisable(t *testing.T) { t.Errorf("threshold 0 should disable: %+v", got) } } + +// M8: the hook path (CheckHealth) and the on-demand report path (Report) +// derive from one Health scan, so they surface the same findings. The +// report's text is exactly what the hook's nudge box wraps. +func TestSurfaceParity(t *testing.T) { + dir := t.TempDir() + // rc defaults apply in the test env (learnings threshold 30). + writeFile(t, dir, "LEARNINGS.md", unmigratedLearnings(35)) + + report := Report(dir) + if report == "" { + t.Fatal("report is empty on a foldable root") + } + if !strings.Contains(report, "LEARNINGS.md") || + !strings.Contains(report, "/ctx-digest") { + t.Errorf("report missing finding or remedy:\n%s", report) + } + + box, warned, err := CheckHealth("sess1234", dir) + if err != nil { + t.Fatalf("CheckHealth err = %v", err) + } + if !warned { + t.Fatal("hook did not warn on a foldable root the report flagged") + } + // The hook's box wraps exactly the report's finding text. + if !strings.Contains(box, "LEARNINGS.md") || + !strings.Contains(box, "/ctx-digest") { + t.Errorf("hook box and report diverged:\nbox=%s\nreport=%s", box, report) + } +} diff --git a/internal/cli/system/core/knowledge/testmain_test.go b/internal/cli/system/core/knowledge/testmain_test.go new file mode 100644 index 00000000..c692c48d --- /dev/null +++ b/internal/cli/system/core/knowledge/testmain_test.go @@ -0,0 +1,22 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +package knowledge + +import ( + "os" + "testing" + + "github.com/ActiveMemory/ctx/internal/assets/read/lookup" +) + +// TestMain initializes the embedded asset lookup so desc.Text resolves +// in FormatWarnings/Report — otherwise the finding format and remedy +// lines come back empty. (See the "uninitialized desc.Text" learning.) +func TestMain(m *testing.M) { + lookup.Init() + os.Exit(m.Run()) +} diff --git a/specs/plans/pd-m5.md b/specs/plans/pd-m5.md index dd3342d6..175b77c1 100644 --- a/specs/plans/pd-m5.md +++ b/specs/plans/pd-m5.md @@ -16,17 +16,17 @@ one shared `knowledge.Health`, emitting two suggestion-only signals: auto-fold, no state file. DoD: -- [ ] `knowledge.Health` emits foldable + heavy findings from one scan; +- [x] `knowledge.Health` emits foldable + heavy findings from one scan; both consumed by the hook and the skill report path. -- [ ] Foldability is staging-based across all three kinds via +- [x] Foldability is staging-based across all three kinds via `disclosure.StagedEntries`; `convention_line_count` retired for `convention_section_count`. -- [ ] Weight scan covers the root **and** every theme file, in bytes. -- [ ] The growth nudge suggests `/ctx-digest` (foldable) and +- [x] Weight scan covers the root **and** every theme file, in bytes. +- [x] The growth nudge suggests `/ctx-digest` (foldable) and split/extract (heavy); `/ctx-remember` and `/ctx-wrap-up` surface both. -- [ ] Suggestion-only; existing daily-throttle/snooze reused; no new +- [x] Suggestion-only; existing daily-throttle/snooze reused; no new state file. -- [ ] `make lint` 0, `go test ./...` green, `make audit` pass. +- [x] `make lint` 0, `go test ./...` green, `make audit` pass. ## Data model & storage @@ -86,17 +86,17 @@ DoD: | T10 | [x] | `FormatWarnings`/`EmitWarning` route by `Kind` | T08,T09 | `knowledge.go` | | unit: foldable vs heavy render distinct remedy lines | Contracts | | T11 | [x] | `ctx system check-knowledge` report path the skills call | T08 | `internal/cli/system/cmd/checkknowledge/*.go` | | e2e: command prints findings for an oversized fixture root | Contracts | | T12 | [x] | hook wiring: `CheckHealth` uses `Health`; throttle/log-first unchanged | T08,T10 | `knowledge.go` | | e2e: hook nudge names /ctx-digest for a foldable root; throttled once/day | Triggers | -| T13 | [ ] | `/ctx-remember` skill: surface foldable/heavy at session start (read-only) | T11 | `internal/assets/claude/skills/ctx-remember/SKILL.md` | [P] | frontmatter test green; body calls the report path, relays suggestions | Triggers | -| T14 | [ ] | `/ctx-wrap-up` skill: surface at session end, suggest-only never inline | T11 | `internal/assets/claude/skills/ctx-wrap-up/SKILL.md` | [P] | frontmatter test green; body surfaces + explicitly does not fold | Triggers | +| T13 | [x] | `/ctx-remember` skill: surface foldable/heavy at session start (read-only) | T11 | `internal/assets/claude/skills/ctx-remember/SKILL.md` | [P] | frontmatter test green; body calls the report path, relays suggestions | Triggers | +| T14 | [x] | `/ctx-wrap-up` skill: surface at session end, suggest-only never inline | T11 | `internal/assets/claude/skills/ctx-wrap-up/SKILL.md` | [P] | frontmatter test green; body surfaces + explicitly does not fold | Triggers | | T15 | [x] | `Health` unit tests: foldable, folded-quiet fixtures | T05 | `internal/cli/system/core/knowledge/*_test.go` | | **M1,M2** pass | Tests | | T16 | [x] | `Health` unit tests: heavy root + heavy theme file | T07 | `knowledge` test | [P] | **M3** pass | Tests | | T17 | [x] | `Health` unit test: both-fire ordering | T08 | `knowledge` test | [P] | **M4** pass | Tests | | T18 | [x] | convention measure test: sections not lines | T05 | `knowledge` test | [P] | **M5** pass | Tests | | T19 | [x] | boundary + disable tests (`>N`, `0` disables) | T08 | `knowledge` test | [P] | **M6,M7** pass | Tests | -| T20 | [ ] | surface parity test: hook findings == report findings | T11,T12 | `knowledge`/checkknowledge test | | **M8** pass | Tests | -| T21 | [ ] | copilot skill sync | T13,T14 | `internal/assets/integrations/copilot-cli/skills/**` | [P] | `make check-copilot-skills` green | Skill | -| T22 | [ ] |XX **measurement gate**: drive Health on a realistic .context (oversized root + fat theme file) | T12,T11 | scratchpad fixture | | foldable + heavy both surface; suggestions correct; throttle holds | Risks | -| T23 | [ ] |XX milestone gate | T01–T22 | — | | `make lint` 0, `go test ./...` green, `make audit` pass; DoD confirmed | Scope/DoD | +| T20 | [x] | surface parity test: hook findings == report findings | T11,T12 | `knowledge`/checkknowledge test | | **M8** pass | Tests | +| T21 | [x] | copilot skill sync | T13,T14 | `internal/assets/integrations/copilot-cli/skills/**` | [P] | `make check-copilot-skills` green | Skill | +| T22 | [x] | **measurement gate**: drive Health on a realistic .context (oversized root + fat theme file) | T12,T11 | scratchpad fixture | | foldable + heavy both surface; suggestions correct; throttle holds | Risks | +| T23 | [x] | milestone gate | T01–T22 | — | | `make lint` 0, `go test ./...` green, `make audit` pass; DoD confirmed | Scope/DoD | **Execution waves** (each `∥` group is file-disjoint): `T01 ∥ T04` → `T02` → `T03 ∥ T05 ∥ T06` → `T07` → `T08` → `T09` → `T10 ∥ T11` →