~bigbes/sr-ht-dolt

07c2a63f985ba138d2945288aff792b8c645b74e — Eugene Blikh 5 days ago bbaaa1f
mcpsrv: list_issues names every clipped table it drew from

Data.Truncated/ShownOf only ever meant the issues/dependencies read that
decides list_issues' total and truncated, so a clipped labels table took
label pills off every card and narrowed the label filter in silence, and a
clipped custom_statuses table could put a card in the wrong lane with
table_truncated still reading false.

list_issues now carries beads.Data.Clipped as clipped: one entry per
clipped table in read order, named, with the rows read against the rows
that exist and what that specific clip costs this listing. table_truncated
and table_total are unchanged; clipped is additive, an empty list on a
complete read.
2 files changed, 136 insertions(+), 7 deletions(-)

M mcpsrv/beads.go
M mcpsrv/beads_test.go
M mcpsrv/beads.go => mcpsrv/beads.go +49 -0
@@ 177,6 177,47 @@ type listIssuesOutput struct {
	// TableTotal is the number of rows in the issues table at this ref, which is
	// what makes TableTruncated checkable rather than a bare warning.
	TableTotal int `json:"table_total"`

	// Clipped names every table this listing drew from that came back clipped —
	// one entry per table, in read order, each with the rows read against the
	// rows that exist and what that specific clip costs this listing.
	//
	// It exists because TableTruncated cannot say this: that flag is paired with
	// Total and Truncated above, so it can only mean the issues/dependencies read
	// that decides them. This listing also draws label pills and its whole label
	// filter from a labels table, and every card's lane from custom_statuses, and
	// a clip in either degrades the listing without moving Total or Truncated by
	// one. Clipped is where those are named instead. A complete read carries an
	// empty list, never a null one.
	Clipped []clippedTableJSON `json:"clipped"`
}

// clippedTableJSON is one table a listing read that came back clipped: its name,
// how many rows were read against how many exist, and the one line saying what
// this listing lost by the rest. It carries beads.ClippedTable across the wire
// unchanged, under the snake_case vocabulary this surface already uses.
type clippedTableJSON struct {
	Table  string `json:"table"`
	Shown  int    `json:"shown"`
	Total  int    `json:"total"`
	Effect string `json:"effect"`
}

// clippedTablesJSON carries a projection's per-table clip list into the wire
// shape, table by table. A nil or empty input answers an empty slice rather than
// a null one, so "nothing was clipped" and "the field is unset" are never the
// same JSON value.
func clippedTablesJSON(clipped []beads.ClippedTable) []clippedTableJSON {
	out := make([]clippedTableJSON, 0, len(clipped))
	for _, c := range clipped {
		out = append(out, clippedTableJSON{
			Table:  c.Table,
			Shown:  c.Shown,
			Total:  c.Total,
			Effect: c.Effect,
		})
	}
	return out
}

type getIssueInput struct {


@@ 538,6 579,13 @@ func (s *Server) registerBeadsTools() {
			"many issues matched before it, and `truncated` says matches were left behind. Separately, " +
			"`table_truncated` says the tracker has more than 2000 issues and only the first 2000 were " +
			"read, so the counts describe that prefix — `table_total` is the real number.\n\n" +
			"`clipped` names every *other* table this listing drew from that also came back clipped — " +
			"labels, custom_statuses — one entry per table with the rows read against the rows that exist " +
			"and what that specific clip costs this listing: a clipped labels table takes the pills off " +
			"every card and narrows what the `label` filter can match, and a clipped custom_statuses " +
			"table can put a card in the wrong lane. `table_truncated`/`table_total` never move for " +
			"either — they mean only the issues/dependencies read that decides `total` — so `clipped` is " +
			"the only place those two clips are visible. It is an empty list on a complete read.\n\n" +
			"A database that is not a beads tracker says so and points at the generic tools; a database " +
			"you may not read is reported as not existing.",
	}, func(ctx context.Context, _ *mcp.CallToolRequest, in listIssuesInput) (*mcp.CallToolResult, listIssuesOutput, error) {


@@ 664,6 712,7 @@ func (s *Server) listIssues(ctx context.Context, in listIssuesInput) (listIssues
		Limit:          limit,
		TableTruncated: data.Truncated,
		TableTotal:     data.ShownOf,
		Clipped:        clippedTablesJSON(data.Clipped),
	}
	for _, lane := range data.Lanes {
		category, ok := laneCategory(lane.Slug)

M mcpsrv/beads_test.go => mcpsrv/beads_test.go +87 -7
@@ 231,6 231,34 @@ func bulkTables(n int) []fakeTable {
// is past the first beads.Max rows any projection here reads.
func bulkTail(n int) string { return "bulk-" + itoa(n-1) }

// labelsClipCount is the labels table size of labelsClipTables: more than
// beads.Max (2000), so the labels read alone comes back clipped.
const labelsClipCount = 2001

// labelsClipTables is a tracker whose issues table stays far under the cap
// while its labels table alone exceeds it — the case list_issues' long-standing
// table_truncated/table_total cannot report at all, because that pair means
// only the issues/dependencies read that decides total and truncated. Here
// neither of those is short, so the two answer "nothing was clipped" while a
// card's pills and the label filter are quietly thinner than the tracker really
// holds; the per-table clip list is the only place that shows up.
func labelsClipTables() []fakeTable {
	labels := make([][]string, 0, labelsClipCount)
	for i := range labelsClipCount {
		labels = append(labels, []string{"lc-1", "label-" + itoa(i)})
	}
	return []fakeTable{
		beadsTable("issues", issueColumns, [][]string{
			row(issueColumns, map[string]string{
				"id": "lc-1", "title": "an issue with more labels than fit", "status": "open",
				"issue_type": "task", "created_at": "2026-04-01 10:00:00",
			}),
		}),
		beadsTable("dependencies", []string{"issue_id", "depends_on_issue_id", "type"}, nil),
		beadsTable("labels", []string{"issue_id", "label"}, labels),
	}
}

func itoa(n int) string {
	if n == 0 {
		return "0"


@@ 287,6 315,7 @@ func beadsFixtures() []fixture {
			),
		},
		{name: "bulk", visibility: core.VisibilityPublic, session: trackerStore(bulkTables(bulkIssues))},
		{name: "labelsclip", visibility: core.VisibilityPublic, session: trackerStore(labelsClipTables())},
		{name: "memories", visibility: core.VisibilityPublic, session: memoryStore()},
		{
			// A tracker whose config table holds settings and no memory at all: the


@@ 368,13 397,21 @@ type (
	}

	listIssuesResult struct {
		Ref            string            `json:"ref"`
		Issues         []issueCardResult `json:"issues"`
		Total          int               `json:"total"`
		Limit          int               `json:"limit"`
		Truncated      bool              `json:"truncated"`
		TableTruncated bool              `json:"table_truncated"`
		TableTotal     int               `json:"table_total"`
		Ref            string               `json:"ref"`
		Issues         []issueCardResult    `json:"issues"`
		Total          int                  `json:"total"`
		Limit          int                  `json:"limit"`
		Truncated      bool                 `json:"truncated"`
		TableTruncated bool                 `json:"table_truncated"`
		TableTotal     int                  `json:"table_total"`
		Clipped        []clippedTableResult `json:"clipped"`
	}

	clippedTableResult struct {
		Table  string `json:"table"`
		Shown  int    `json:"shown"`
		Total  int    `json:"total"`
		Effect string `json:"effect"`
	}

	issueResult struct {


@@ 755,6 792,49 @@ func TestListIssuesReportsTheProjectionsOwnClip(t *testing.T) {
	assert.Equal(t, 2000, got.Total, "the board was computed over the rows that were read")
	assert.Len(t, got.Issues, 10)
	assert.True(t, got.Truncated, "the limit clipped the list too, and the two are told apart")

	require.Len(t, got.Clipped, 1, "the issues table is the only one clipped in this fixture")
	assert.Equal(t, "issues", got.Clipped[0].Table)
	assert.Equal(t, 2000, got.Clipped[0].Shown)
	assert.Equal(t, bulkIssues, got.Clipped[0].Total)
	assert.NotEmpty(t, got.Clipped[0].Effect, "each entry names what the clip costs this listing")
}

// table_truncated/table_total have only ever meant the issues/dependencies read
// that decides total and truncated. A clipped labels table moves neither — the
// issues table here is read whole — so it would be invisible without a place of
// its own; clipped is that place, named per table with what was read against
// what exists and what the clip costs this listing.
func TestListIssuesCarriesThePerTableClipList(t *testing.T) {
	session := beadsServer(t)

	t.Run("a complete read carries an empty list", func(t *testing.T) {
		got := listIssues(t, session, args("board"))
		assert.Empty(t, got.Clipped)

		payload := resultJSON(t, call(t, session, "list_issues", args("board")))
		assert.Contains(t, payload, `"clipped":[]`,
			"asserted on the serialised payload: an empty slice, never a null field")
	})

	t.Run("a clipped labels table shows up with its effect", func(t *testing.T) {
		got := listIssues(t, session, args("labelsclip"))
		assert.False(t, got.TableTruncated, "the issues table itself was read whole")
		assert.Equal(t, 1, got.TableTotal)

		require.Len(t, got.Clipped, 1)
		clip := got.Clipped[0]
		assert.Equal(t, "labels", clip.Table)
		assert.Equal(t, 2000, clip.Shown, "the projection's own per-table cap")
		assert.Equal(t, labelsClipCount, clip.Total)
		assert.Contains(t, clip.Effect, "label",
			"the effect line names what a reader loses, not just that something was clipped")

		payload := resultJSON(t, call(t, session, "list_issues", args("labelsclip")))
		assert.Contains(t, payload, `"table":"labels"`)
		assert.Contains(t, payload, `"shown":2000`)
		assert.Contains(t, payload, `"total":`+itoa(labelsClipCount))
	})
}

// An omitted ref is the tracker's default branch and the answer names it; a