[Post-v0.1/P2] Require a point/stream class for every proposed verb #35

Open
opened 2026-09-03 16:18:11 +09:00 by natsukium · 1 comment
Owner

Follow-up from #12 (P2).

Why

A verb whose output class depends on payload size gives consumers unstable framing and terminal rules. Future CLI/bridge proposals need to choose Point, Stream, or Exempt before implementation.

Scope

  • Record the proposal-time classification rule in the owning control-surface explanation and contributor workflow.
  • Require one terminal rule for every Stream proposal.
  • State that “maybe a stream if large” is always a Stream.
  • Add review guidance without creating a second planning or decision-record layer.

Acceptance criteria

  • The placement/classification checklist is discoverable from the implementation workflow.
  • Existing verbs remain fully classified in the normative CLI reference.
  • New verb proposals have an explicit rejection criterion when class or terminal behavior is missing.
Follow-up from #12 (P2). ## Why A verb whose output class depends on payload size gives consumers unstable framing and terminal rules. Future CLI/bridge proposals need to choose Point, Stream, or Exempt before implementation. ## Scope - Record the proposal-time classification rule in the owning control-surface explanation and contributor workflow. - Require one terminal rule for every Stream proposal. - State that “maybe a stream if large” is always a Stream. - Add review guidance without creating a second planning or decision-record layer. ## Acceptance criteria - [ ] The placement/classification checklist is discoverable from the implementation workflow. - [ ] Existing verbs remain fully classified in the normative CLI reference. - [ ] New verb proposals have an explicit rejection criterion when class or terminal behavior is missing.
Author
Owner

Triage plan (2026-09-03)

Source-grounded triage against main at 69076d42, reviewed through seven rounds of an independent reviewer (pi sol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12.

Claim check

Mostly already true at the reference and code level; the gap is the proposal-time rule and the contributor workflow.

  • The classification is normative and exhaustive today: docs/reference/cli.md:52-84 defines Point/Stream/Exempt, lists every verb in exactly one row, and already states the extension rule: "Any machine-output verb added later lands with its classification in this table; a verb with no row here has no --format flag." The stream terminal rule is at cli.md:557-575 and its rationale at docs/explanation/architecture/control-surfaces.md:236-300 (why the class is explicit; --json rejected) and :310-330 (one terminal object, clean vs failing are different objects).
  • The code enforces it: crates/felis-cli/src/cli_output.rs:68-91 (PointFormat / StreamFormat clap shapes, the wrong framing refused before any dial), :253-370 (Class enum with debug_asserts so a point verb cannot emit items and a stream cannot emit a result). crates/felis-cli/src/tests.rs:261-300 pins every verb's class.
  • The bridge has a second classification: crates/felis-cli/src/cli_bridge.rs:387 fn is_streaming_op(op: &str) is a hard-coded list that must agree with the CLI table (cli.md:702-716 documents the bridge's point/stream envelopes). Nothing ties the two together today.
  • The contributor workflow does not carry the rule at proposal time. .claude/skills/implement-feature/SKILL.md:20 routes "New IPC message, felis sessions verb, or CLI surface" to extend-ipc; extend-ipc/SKILL.md:92 mentions the class only as an implementation step ("declare the class by flattening PointFormat or StreamFormat"). The specific rule the issue wants — "maybe a stream if large" is always a Stream; every Stream names its terminal — appears nowhere (grep -rn "payload size\|if large" docs/ .claude/skills/ is empty).

Verdict

accept-with-changes: this is a docs-and-skill change of size S with no contract impact, so it does not need to wait for the tag — but it also does not need to block it. Two adjustments to the issue:

  1. The natural moment to write the rule is #23 (simplify the CLI machine contract) and #29 (publish schemas and golden conversations), which re-state the classification anyway; if #23's cascade touches control-surfaces.md "Shared conventions", add the paragraph there and close this issue with it. Otherwise land it immediately post-tag.
  2. Add one mechanical acceptance criterion the issue lacks: the bridge's is_streaming_op and the CLI table are one source of truth (or a test asserts they agree). "Existing verbs remain fully classified" is otherwise a prose promise.

Deferral cost with respect to the freeze: none. The classification itself freezes with #23/#29; this issue only writes down how future verbs join it, and that text can be added to an explanation doc any time without a contract change.

Approach

  1. docs/explanation/architecture/control-surfaces.md "Shared conventions", the --format bullet (:236): add a short "Proposing a verb" paragraph — the class is chosen at proposal time and is a property of the verb, never of the payload; a verb whose output might grow large is a Stream (the size-dependent verb is the rejected shape: it gives consumers unstable framing and terminal rules); each Stream proposal names its terminal count noun (count = sessions/rows/matches/notifications) and its failing terminal; Exempt is only for "nothing structured" or "a fixed framing of its own" (cli.md:69). Rejection criterion, in the doc's own voice: a proposal with no class, or a Stream with no terminal, is not reviewable. This is the decision record; no ADR layer (CLAUDE.md "Recording design decisions").
  2. docs/reference/cli.md:80-84: extend the existing "lands with its classification" sentence with "and, for a Stream, its terminal" so the reference twin states the fact.
  3. .claude/skills/extend-ipc/SKILL.md: a step before wiring ("0. Classify: Point / Stream / Exempt, terminal named") and, in step 4, the bridge mirror (cli_bridge.rs is_streaming_op) beside PointFormat/StreamFormat. implement-feature/SKILL.md:20 row text: "…(classify Point/Stream/Exempt first)".
  4. Code (small, optional but makes the criterion mechanical): a single pub(crate) const STREAM_VERBS table in cli_output.rs consumed by is_streaming_op and by the tests.rs:261 pin, or a test asserting the bridge list equals the Stream row. No wire, no schema, no CHANGELOG (contributor-facing only); skills/felis unchanged unless a verb changes.
  5. Tests: the existing each_verb_accepts_only_its_own_classs_format (sic — typo in the test name at tests.rs:264, fix while there) plus the bridge/CLI agreement test.

Dependencies

  • #23 and #29 should settle the classification vocabulary first (if #23 renames anything, the paragraph should use the final words). #21 touches cli_bridge.rs admission and is the natural place to route is_streaming_op through the shared table.
  • Nothing blocks on this issue.

Risk/effort

S. Risk is only doc drift between the three places (control-surfaces, cli.md, extend-ipc skill) — the doc-cascade grep sweep covers it.

Labels

Keep priority/P2. Consider a note that it may close as part of #23/#29's doc cascade rather than as a standalone change; if it does not ride those, it is a good first post-tag docs: commit.

## Triage plan (2026-09-03) Source-grounded triage against `main` at `69076d42`, reviewed through seven rounds of an independent reviewer (`pi` sol/luna) until it passed with no findings. The dependency order that supersedes the tracker's is posted on #12. ## Claim check Mostly already true at the reference and code level; the gap is the *proposal-time* rule and the contributor workflow. - The classification is normative and exhaustive today: `docs/reference/cli.md:52-84` defines Point/Stream/Exempt, lists every verb in exactly one row, and already states the extension rule: "Any machine-output verb added later lands with its classification in this table; a verb with no row here has no `--format` flag." The stream terminal rule is at `cli.md:557-575` and its rationale at `docs/explanation/architecture/control-surfaces.md:236-300` (why the class is explicit; `--json` rejected) and `:310-330` (one terminal object, clean vs failing are different objects). - The code enforces it: `crates/felis-cli/src/cli_output.rs:68-91` (`PointFormat` / `StreamFormat` clap shapes, the wrong framing refused before any dial), `:253-370` (`Class` enum with `debug_assert`s so a point verb cannot emit items and a stream cannot emit a result). `crates/felis-cli/src/tests.rs:261-300` pins every verb's class. - The bridge has a **second** classification: `crates/felis-cli/src/cli_bridge.rs:387 fn is_streaming_op(op: &str)` is a hard-coded list that must agree with the CLI table (`cli.md:702-716` documents the bridge's point/stream envelopes). Nothing ties the two together today. - The contributor workflow does not carry the rule at proposal time. `.claude/skills/implement-feature/SKILL.md:20` routes "New IPC message, `felis sessions` verb, or CLI surface" to `extend-ipc`; `extend-ipc/SKILL.md:92` mentions the class only as an implementation step ("declare the class by flattening `PointFormat` or `StreamFormat`"). The specific rule the issue wants — "maybe a stream if large" is always a Stream; every Stream names its terminal — appears nowhere (`grep -rn "payload size\|if large" docs/ .claude/skills/` is empty). ## Verdict **accept-with-changes**: this is a docs-and-skill change of size S with no contract impact, so it does not need to wait for the tag — but it also does not need to block it. Two adjustments to the issue: 1. The natural moment to write the rule is **#23** (simplify the CLI machine contract) and **#29** (publish schemas and golden conversations), which re-state the classification anyway; if #23's cascade touches `control-surfaces.md` "Shared conventions", add the paragraph there and close this issue with it. Otherwise land it immediately post-tag. 2. Add one mechanical acceptance criterion the issue lacks: the bridge's `is_streaming_op` and the CLI table are one source of truth (or a test asserts they agree). "Existing verbs remain fully classified" is otherwise a prose promise. Deferral cost with respect to the freeze: none. The classification itself freezes with #23/#29; this issue only writes down how future verbs join it, and that text can be added to an explanation doc any time without a contract change. ## Approach 1. **`docs/explanation/architecture/control-surfaces.md`** "Shared conventions", the `--format` bullet (`:236`): add a short "Proposing a verb" paragraph — the class is chosen at proposal time and is a property of the verb, never of the payload; a verb whose output *might* grow large is a Stream (the size-dependent verb is the rejected shape: it gives consumers unstable framing and terminal rules); each Stream proposal names its terminal count noun (`count` = sessions/rows/matches/notifications) and its failing terminal; Exempt is only for "nothing structured" or "a fixed framing of its own" (`cli.md:69`). Rejection criterion, in the doc's own voice: a proposal with no class, or a Stream with no terminal, is not reviewable. This is the decision record; no ADR layer (CLAUDE.md "Recording design decisions"). 2. **`docs/reference/cli.md:80-84`**: extend the existing "lands with its classification" sentence with "and, for a Stream, its terminal" so the reference twin states the fact. 3. **`.claude/skills/extend-ipc/SKILL.md`**: a step before wiring ("0. Classify: Point / Stream / Exempt, terminal named") and, in step 4, the bridge mirror (`cli_bridge.rs` `is_streaming_op`) beside `PointFormat`/`StreamFormat`. **`implement-feature/SKILL.md:20`** row text: "…(classify Point/Stream/Exempt first)". 4. **Code** (small, optional but makes the criterion mechanical): a single `pub(crate) const STREAM_VERBS` table in `cli_output.rs` consumed by `is_streaming_op` and by the `tests.rs:261` pin, or a test asserting the bridge list equals the Stream row. No wire, no schema, no CHANGELOG (contributor-facing only); `skills/felis` unchanged unless a verb changes. 5. **Tests**: the existing `each_verb_accepts_only_its_own_classs_format` (sic — typo in the test name at `tests.rs:264`, fix while there) plus the bridge/CLI agreement test. ## Dependencies - **#23** and **#29** should settle the classification vocabulary first (if #23 renames anything, the paragraph should use the final words). **#21** touches `cli_bridge.rs` admission and is the natural place to route `is_streaming_op` through the shared table. - Nothing blocks on this issue. ## Risk/effort **S.** Risk is only doc drift between the three places (control-surfaces, cli.md, extend-ipc skill) — the `doc-cascade` grep sweep covers it. ## Labels Keep `priority/P2`. Consider a note that it may close as part of #23/#29's doc cascade rather than as a standalone change; if it does not ride those, it is a good first post-tag `docs:` commit.
Sign in to join this conversation.
No description provided.