Skip to content

Design/progressive disclosure#145

Merged
josealekhine merged 11 commits into
mainfrom
design/progressive-disclosure
Jul 18, 2026
Merged

Design/progressive disclosure#145
josealekhine merged 11 commits into
mainfrom
design/progressive-disclosure

Conversation

@josealekhine

Copy link
Copy Markdown
Member

No description provided.

Canonical knowledge files grow without bound while their entries stay
valid, so nothing can be dropped (time-sharding and a load-excluded cold
bucket are already rejected). At scale an agent that legitimately wants
system understanding reads every decision, then every learning, and
exhausts its context window. Headings-first consumption is necessary but
not sufficient — an agent can always choose to read the whole file — and
consolidation does not help (the 2026-07-16 pass moved LEARNINGS only
98 -> 88 because the corpus is dense with distinct signal, not
redundancy).

Design: each canonical file becomes a BOUNDED ROOT — a staging zone
(where `add` already writes) plus a `## Themes` section carrying a
"just enough" per-theme gist and a link to a theme file holding that
theme's bodies. Staging IS the watermark (un-digested by construction),
so no state file is needed. An agent-driven, human-gated pass folds
staged entries into themes and refreshes the touched gists. The
structure is self-similar, so an overgrown theme file can itself become
a root — nesting deferred, not precluded.

Verified against the code: the add anchor (first line-start `## [`,
falling back to AfterHeader) always lands above `## Themes` even when
staging is empty, so the add path needs zero change; and because the
root is bounded, the `ctx agent` packet needs no rewire either.

Scope LEARNINGS/DECISIONS/CONVENTIONS; CONSTITUTION (small by design)
and TASKS (auto-archived) excluded. Implementation remains open; the
pass codified as a reusable skill is the final deliverable.

Spec: specs/progressive-disclosure.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Decompose specs/progressive-disclosure.md into specs/plans/pd-m1.md: 15
tasks across 5 epics, each with a falsifiable acceptance criterion, plus
a test matrix where every invariant row is exercised by a named task.

Milestone 1 deliberately moves nothing. The pass that relocates entry
bodies (M2+) is the clobber/data-loss risk class, so the refusal
machinery lands first: Parse (byte-exact round-trip), Validate, and the
cross-file invariants (pairing, uniqueness, links). Staging is kept raw
so the mover gets byte-exact bodies and nothing is silently normalized.

T10-T12 are the milestone's measurement gate: they prove the design's
load-bearing premise that `add` needs zero change because its anchor
always lands above `## Themes` -- including the empty-staging
AfterHeader fallback. If they fail, the spec's Layout section is wrong
and must be revisited via /ctx-plan rather than patched downstream.

Blocking-TBD gate found one real blocker: the spec's validate said
"exactly one ## Themes" while the pass "creates it on first run" -- a
contradiction for an un-migrated root. Resolved IN THE SPEC (validate
accepts zero-or-one; zero = first run; 2+ refuses), since a
resolution living only in the plan would be minting. Four deferrable
TBDs are carried in Out of scope, annotated with the milestone at which
each graduates to blocking.

TASKS.md gains epic anchors only (E1-E5, partitioning T01-T15); the plan
is the single source of truth for milestone progress.

Spec: specs/progressive-disclosure.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
insert.AfterHeader returned content[:insertPoint] + entry, truncating
the file at the insertion point and silently discarding the tail. Its
sole caller is beforeFirstEntry's fallback, taken when a knowledge file
has no `## [` entry — so `ctx learning add` / `ctx decision add` against
an entry-less LEARNINGS.md/DECISIONS.md destroyed any content below the
H1 header and its comment block. Same family as the clobber bug
index.Validate guards: silent loss of persisted memory, git-only
recovery.

Masked today because an entry-less file from `ctx init` has nothing
after the comment block (empty tail), so nothing was lost yet — but any
hand-written section below the preamble of an as-yet-entry-less file was
one add away from deletion.

Fix: preserve the tail (the pattern sibling Task already uses). Empty
tail returns the original bytes unchanged, so every file shape that
works today is byte-identical; only the losing shape changes. When a
tail follows, the entry is separated from it by a `---` rule.

The package had no tests. Added insert_test.go (tail-preserved,
empty-tail-unchanged, primary-path-unchanged) and a TestMain calling
lookup.Init() — without it desc.Text returns "" and strings.Index(s,"")
== 0 silently rewrites every insert anchor to offset 0, so tests would
pass exercising a path production never takes. Verified end-to-end
against the built binary: a Notes section below the preamble survives
`ctx learning add` and the entry lands above it.

Found by the layout proof for specs/progressive-disclosure.md; the fix
stands alone and ships alone.

Spec: specs/fix-afterheader-tail-truncation.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Two gotchas from fixing the AfterHeader truncation:
- desc.Text() returns "" in a test binary without lookup.Init(), and
  strings.Index(s, "") == 0 turns anchor-based inserts into offset-0
  matches — tests pass exercising a path production never takes.
- An insert helper returning content[:i]+x without content[i:] is
  append-with-truncation, not insert; a lone content[:i] is the smell.

Spec: specs/fix-afterheader-tail-truncation.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Prove the progressive-disclosure premise that `ctx X add` needs no
accommodation: its insert anchor always lands a new entry in the staging
zone for all three file kinds, populated AND empty staging.

- LEARNINGS/DECISIONS (beforeFirstEntry): entry lands above `## Themes`,
  including the empty-staging AfterHeader fallback.
- CONVENTIONS (AppendAtEnd): new `###` section lands inside the trailing
  `## Recent` staging zone, below `## Themes`.

The gate fired first: T10's empty-staging case caught the pre-existing
AfterHeader tail-truncation bug (destroying the Themes section). That
bug is fixed separately and merged; these proofs are now green with
unchanged assertions. Amendment logged in the plan.

Plan: specs/plans/pd-m1.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Milestone 1 of progressive disclosure: the refusal machinery and the
structural vocabulary, with tests. Nothing moves — no entry body is
relocated and no gist is authored (M2+ moves bodies, the clobber risk
class, so the guards land first).

New package internal/disclosure:
- Parse splits a root (preamble | staging | themes) per kind and
  round-trips byte-for-byte via Reconstruct — the mover needs verbatim
  bodies, so nothing is normalized. Parse is total; all refusal is in
  Validate.
- Validate is the fail-loud precondition: zero-or-one ## Themes (zero =
  not-yet-migrated first run), no entry below ## Themes, staging parses
  into discrete entries. It never regenerates from "what it recognized"
  — the failure mode behind the historic clobber bug.
- CheckPairing / CheckUniqueness / CheckLinks are the cross-file
  invariants: gists <-> theme files 1:1, every entry in exactly one
  place (identity is timestamp+title, since same-second entries share a
  timestamp), every theme link resolves.

Supporting: internal/config/disclosure (structural vocabulary),
internal/err/disclosure (entity.Sentinel guards + err.disclosure.* i18n
text). internal/compliance runs the invariants against this repo's own
.context/ (vacuous pass — un-migrated) and proves-both-ways with a
planted double-Themes violation.

E4 (layout proofs, T10-T12) landed earlier; this commit is E1-E3 + E5.
Full suite green (714 pkgs), lint 0, make audit all-pass; canonical
knowledge files byte-identical.

Plan: specs/plans/pd-m1.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Resolve the M2-blocking gist-format TBD in the spec (### Gist format):
one bullet `- <name> — <gist> → [<name>](<noun>/<slug>.md)`, gist one
line ≤~140 chars describing coverage, authored by the pass. Then
decompose M2 into specs/plans/pd-m2.md: 14 tasks / 3 epics.

M2 is the digesting pass in DRY-RUN only: a read-only
`ctx disclosure inspect` CLI (built on M1's Parse) and a `ctx-digest`
skill that proposes a theme per staged entry, authors proposed gists,
and shows the plan -- moving nothing. The mover (append->verify->remove,
gist write-back) is M3, kept behind this fully-exercised read+plan path.
T12 is the measurement gate: the dry-run must produce a coherent theme
plan and leave the fixture byte-identical.

Rolling-wave gate satisfied (M1 DoD all [x]). TASKS.md gains E1-E3 epic
anchors only, partitioning T01-T14.

Spec: specs/progressive-disclosure.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
The dry-run pass's read foundation: the disclosure inspect model plus a
read-only CLI, built on M1's Parse. Nothing writes.

- disclosure.KindFor infers a root's kind from its canonical filename;
  StagedEntries lists the staging zone's un-digested entries;
  Inspect assembles the {kind, staging, themes} view (json-tagged).
- `ctx disclosure inspect <file> [--json]`: reports staged entries and
  current themes for LEARNINGS/DECISIONS/CONVENTIONS, human or JSON.
  Rejects a non-knowledge file with a typed, path-naming error. It is
  read-only and verified to leave the file byte-identical.

New surfaces follow the project's conventions: err/disclosure sentinel +
format wrapper, config/embed/cmd Use/DescKey constants, commands.yaml +
write.yaml i18n text (bijection holds), write/disclosure output package,
and registration in bootstrap/group.go under the artifacts group.

Verified end-to-end against a built binary (human + JSON + rejection).
The mover (M3) will consume this same Inspection. E3 (the ctx-digest
dry-run skill) is next.

Plan: specs/plans/pd-m2.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
The dry-run half of progressive disclosure, as a shipped skill. It
inspects a knowledge root (ctx disclosure inspect --json), proposes a
theme per staged entry (semantic, human-overridable), authors a
one-line coverage gist per theme (spec ### Gist format), and presents
the digest plan -- moving nothing. The skill explicitly writes no file
and names the apply step (a later milestone) as where the move happens.

Copilot CLI copy created and in sync.

Measurement gate (T12) driven on a fixture of 5 staged learnings: the
dry-run produced a coherent 2-theme plan with gists, every entry
assigned once, and left the fixture byte-identical.

Milestone 2 complete: all 14 tasks [x], DoD met, make audit passes,
717 packages green, canonical knowledge files untouched. The mover
(append->verify->remove, gist write-back) is M3, which consumes the
Inspection this milestone builds.

Plan: specs/plans/pd-m2.md
Signed-off-by: Jose Alekhinne <jose@ctx.ist>
Persist two learnings (the /ctx-task-out measurement gate that caught the
AfterHeader data-loss bug; the new-package audit gauntlet), the PD-M3
task pointer, and the session handover.

Signed-off-by: Jose Alekhinne <jose@ctx.ist>
@josealekhine josealekhine self-assigned this Jul 18, 2026
@josealekhine
josealekhine merged commit 249935b into main Jul 18, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant