Skip to content

labkit list <node_type> shows the stored nodes of one type; get and list embed two hops by default - #609

Merged
danbarua merged 2 commits into
mainfrom
cli-get-list-as-stored
Oct 1, 2026
Merged

danbarua merged 2 commits into
mainfrom
cli-get-list-as-stored

Conversation

@danbarua

@danbarua danbarua commented Oct 1, 2026

Copy link
Copy Markdown
Owner

labkit get <handle> and labkit list <node_type> show the record as stored, for an agent that wants the raw nodes rather than an answer. Both take --json and --depth <n>, and --depth now defaults to 2 for both.

What changed

  • list <node_type> reads public.labkit_get_collection_as_hal(p_tenant_id, p_label, p_offset, p_limit, p_depth). It goes through a new collection read on ReadSurface, with collectionQuery in packages/core-domain/queries.ts and TenantGraph.collectionAsHal in packages/core-db/graph.ts, the same route get takes through resource/entityAsHal. app-cli imports nothing from core-db for it.
    • Paging options mirror the procedure's arguments: --offset (default 0) and --limit (default 25, at most 200), plus the global --depth.
    • An unknown type is refused before the record opens. The message names every type, taken from NODE_LABELS through the schema's enum. The <node_type> help lists them from the same place.
    • Plain output renders each node the way get does, under a Claim 1-2 heading, and ends with More: --offset N when the procedure returns a next link. --json emits the procedure's HAL document unchanged.
  • get now defaults to depth 2. Before, the default was 1.
  • One --depth, still global. It defaults to 2, is refused unless it is a whole number from 0 to 6, and its help names both commands. DEFAULT_DEPTH, MAX_DEPTH, DEFAULT_PAGE and MAX_PAGE are each defined once in queries.ts, and the help text reads them.
  • Help group "The record as stored", added to READ_GROUPS after "What was done". get moved there from "Finding a handle", and list joined it. Each description says it shows the record with no interpretation, and get's says that why answers the question while get shows what the answer was drawn from.
  • Plain rendering of deeper hops. At depth 2, get's plain output used to show only the first hop. Each farther hop is now indented beneath the neighbour it is reached from. This also changes get's plain output.

Choices to overrule

  • The default --limit is 25, the same as the HTTP collection handler's default. The procedure has no default of its own.
  • get moved out of "Finding a handle".

How the procedure compares with the brief

  • The signature is as the brief gives it.
  • It caps limit at 200 and depth at 6. The CLI validates both before the record opens.
  • It checks the label against the tenant graph's own vertex-label tables, not against the domain's list. In practice the result is the same, because provisioning creates every label in NODE_LABELS. The CLI refuses an unknown type before the procedure is called.
  • Its _links are relative HTTP paths (/collections/..., /graph/...). --json emits them as they are.

Not done: an out-of-range --limit or --depth gets zod's generic message, for example Too big: expected number to be <=200, which does not name the flag.

Tests

  • tests/persistence/collection-as-hal.test.ts (2 tests, at the TenantGraph level where entityAsHal is tested): limit and offset page through three claims, with the right next/prev links. Each item equals entityAsHal for the same handle at depth 0 and at depth 2.
  • tests/cli/record-as-stored.test.ts (3 tests, run through buildProgram):
    • get embeds two hops by default, --depth 1 narrows it to one, and the plain output shows the second hop.
    • list Claim --limit 1 returns the item get returns, and its plain output says More: --offset 1.
    • list Clam is refused before anything runs, and the message names every entry of NODE_LABELS.
  • bun test tests/persistence/collection-as-hal.test.ts tests/cli/ packages/app-web/tests/collection-as-hal.test.ts: 65 pass, 0 fail.
  • bun test tests/mcp tests/domain tests/consumer: 113 pass, 0 fail.
  • bun run check:quick: all 25 passed. check:cli: 23 assertions passed. check:binary: OK.

Measured with the compiled binary

The binary was built with bun run cli:build and run against a throwaway record created with --db in a scratch directory. It was seeded through the write commands: an enquiry, observations, an analysis, two concluded claims (CLM_4, CLM_5), and a note on CLM_4.

$ labkit list Claim
Claim 1-2

CLM_4  Claim

  kind           exploratory
  name           the schedule moves convergence

  note:concerns            NOTE_6
  evidence:supports        EV_4
    recorded_in:artefact     ART_3
    evidenceunit:produces    EU_3

CLM_5  Claim

  kind           exploratory
  name           the effect is larger at depth 20

  evidence:supports        EV_5
    recorded_in:artefact     ART_3
    evidenceunit:produces    EU_3
[exit 0]

$ labkit list Claim --json --depth 1
{
  "count": 2,
  "limit": 25,
  "_links": {
    "self": {
      "href": "/collections/Claim?limit=25&offset=0"
    }
  },
  "offset": 0,
  "_embedded": {
    "Claim": [
      {
        "id": "CLM_4",
        "kind": "exploratory",
        "name": "the schedule moves convergence",
        "type": "Claim",
        "_links": {
          "self": {
            "href": "/graph/CLM_4",
            "type": "Claim"
          },
          "start": {
            "href": "/graph/CLM_4",
            "type": "Claim"
          },
          "note:concerns": [
            {
              "dir": "in",
              "href": "/graph/NOTE_6",
              "type": "Note"
            }
          ],
          "evidence:supports": [
            {
              "dir": "in",
              "href": "/graph/EV_4",
              "type": "Evidence"
            }
          ]
        },
        "_embedded": {
          "note:concerns": [
            {
              "id": "NOTE_6",
              "dir": "in",
              "text": "the depth 20 runs used a different seed",
              "type": "Note",
              "depth": 1,
              "_links": {
                "self": {
                  "href": "/graph/NOTE_6",
                  "type": "Note"
                }
              }
            }
          ],
          "evidence:supports": [
            {
              "id": "EV_4",
              "dir": "in",
              "type": "Evidence",
              "depth": 1,
              "_links": {
                "self": {
                  "href": "/graph/EV_4",
                  "type": "Evidence"
                },
                "recorded_in:artefact": [
                  {
                    "dir": "out",
                    "href": "/graph/ART_3",
                    "type": "Artefact"
                  }
                ],
                "evidenceunit:produces": [
                  {
                    "dir": "in",
                    "href": "/graph/EU_3",
                    "type": "EvidenceUnit"
                  }
                ]
              },
              "statement": "~3 steps earlier at every depth"
            }
          ]
        }
      },
      {
        "id": "CLM_5",
        "kind": "exploratory",
        "name": "the effect is larger at depth 20",
        "type": "Claim",
        "_links": {
          "self": {
            "href": "/graph/CLM_5",
            "type": "Claim"
          },
          "start": {
            "href": "/graph/CLM_5",
            "type": "Claim"
          },
          "evidence:supports": [
            {
              "dir": "in",
              "href": "/graph/EV_5",
              "type": "Evidence"
            }
          ]
        },
        "_embedded": {
          "evidence:supports": [
            {
              "id": "EV_5",
              "dir": "in",
              "type": "Evidence",
              "depth": 1,
              "_links": {
                "self": {
                  "href": "/graph/EV_5",
                  "type": "Evidence"
                },
                "recorded_in:artefact": [
                  {
                    "dir": "out",
                    "href": "/graph/ART_3",
                    "type": "Artefact"
                  }
                ],
                "evidenceunit:produces": [
                  {
                    "dir": "in",
                    "href": "/graph/EU_3",
                    "type": "EvidenceUnit"
                  }
                ]
              },
              "statement": "5 steps at depth 20"
            }
          ]
        }
      }
    ]
  }
}
[exit 0]

$ labkit get CLM_4
CLM_4  Claim

  kind           exploratory
  name           the schedule moves convergence

  note:concerns            NOTE_6
  evidence:supports        EV_4
    recorded_in:artefact     ART_3
    evidenceunit:produces    EU_3
[exit 0]

$ labkit --depth 1 get CLM_4
CLM_4  Claim

  kind           exploratory
  name           the schedule moves convergence

  note:concerns            NOTE_6
  evidence:supports        EV_4
[exit 0]

$ labkit list Clam
labkit: no node type `Clam`; the types are Question, LineOfEnquiry, EvidenceUnit, Evidence, Claim, Decision, Criterion, CriterionEvaluation, Gate, Review, Artefact, Computation, Task, Note
[exit 1]

$ labkit list Claim --limit 1
Claim 1-1

CLM_4  Claim

  kind           exploratory
  name           the schedule moves convergence

  note:concerns            NOTE_6
  evidence:supports        EV_4
    recorded_in:artefact     ART_3
    evidenceunit:produces    EU_3

More: --offset 1
[exit 0]

$ labkit list Claim --limit 201
labkit: Too big: expected number to be <=200
[exit 1]

labkit get CLM_4 with no --depth shows the second hop (ART_3, EU_3 beneath EV_4). --depth 1 does not.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FWZHuy7xJbVqxyLipnvwdY

danbarua and others added 2 commits October 1, 2026 01:01
…wo hops by default

`list` reads `labkit_get_collection_as_hal` through a new `collection` read, with
`--offset`, `--limit` (default 25, at most 200) and the global `--depth`. An unknown
type is refused before the record opens, naming every type in the domain's label list.
`get` and `list` share a help group, "The record as stored", and `--depth` defaults to 2
for both. Plain output indents each farther hop beneath the neighbour it is reached from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWZHuy7xJbVqxyLipnvwdY
@github-actions github-actions Bot added the area:record The record: core-domain, core-db, the CLI, the MCP server, the domain model and workspaces label Oct 1, 2026
@danbarua
danbarua enabled auto-merge (squash) October 1, 2026 00:08
@danbarua
danbarua merged commit 3de1f9a into main Oct 1, 2026
10 checks passed
@danbarua
danbarua deleted the cli-get-list-as-stored branch October 1, 2026 00:10
danbarua added a commit that referenced this pull request Oct 1, 2026
…ted enquiries say why (#611)

The follow-up to #608, #609 and #610. Measured with the compiled binary
(`bun run cli:build`) against throwaway records, always with `--db`. The
"before" binary was built from `main` at e4250a4.

## 1. Reads stop handling shapes only the retired verbs wrote

For each shape, I grepped every staged write in
`packages/core-domain/write/*.ts` (`unitOfWork.node`/`edge`).
`UnitOfWork.set` and `setEdge` have no callers, so no live write sets
any property, `retracted` included. None of the shapes below is produced
by a live write:

| shape | written only by | what went |
|---|---|---|
| `review` kind in `why` | `recordReview` | **kept**, see below.
Removed: the `ReviewRef` alias and the minted `reviewRef` codec |
| sharpening arm of `originAlready` (Decision MOTIVATES Question +
SHARPENS) | `sharpen` | the arm; `originAlready` now reads only the note
|
| `closed` gate state (Decision CLOSES Gate) | `closeGate` | the state,
`GateStatus.closure`, the `closed` lookups in `gateStatus`/`gateList`,
the `explainGate` case, `closed` in `GATE_STATES` and in the `why`
descriptions |
| `undecided` standing (GRADES) | `isUndecided` | the verdict, the
standing, the GRADES lookup in `standingsConferred`, the view branch,
the vocabulary word |
| retracted nodes | `undo` | every `retracted IS NULL` / `retracted !==
true` filter in `explain`, `happened`, `inventory`, `standing`, `core`;
`everGated` and the unlabelled `ever` match in `workList`; `retractedIn`
and the `retracting` line in `happened` |
| `revisedBy` (Decision MOTIVATES/SUPERSEDES Computation) and the review
behind it | `replaceAnalysis` | `revisedBy`, `impliedSupersession`,
`conclusionsOf`, the BASED_ON-review edge in `concluding`; the Review
match and `because` on `analysisRevision`; the Review lookups in
`whySupported` (EVALUATES, BASED_ON Review, INVALIDATED_BY) |

**Not done: the `review` kind.** `search` walks every searchable label
in `SEARCHABLE_TEXT` (core-db/domain.ts) and refuses a label with no
kind. `Review` is one of them. With the kind removed, every search
failed:

```
$ labkit --db …/rec1 --no-ansi search seed
labkit: Review is searchable but names no research-concept kind
exit 1
```

Removing the kind needs one of two changes that this PR does not make:
take `Review` out of the schema's searchable annotations, or make
`search` skip a label with no kind (its comment says that case should
fail loudly). The kind is back, and so is ", review" in the `why`
descriptions.

Tests: the undecided view test and the undecided verdict case are gone;
the `workStateFrom` test lost its retracted-gate case;
`tests/events/retraction.test.ts` is gone, since it checked that reads
hide retracted nodes. `closed` leaves the CLI vocabulary because
`tests/cli/vocabulary.test.ts` failed once no schema could say it (`+
"closed"`). The enquiry list still prints `closed`, uncoloured.

The same reads over two records, before and after: the throwaway record
(`happened` 52 lines, `now` 16 lines, `why CLM_6` on a replaced claim)
and the rebuilt Bonsai record (`now` 82 lines, `claims` 51, `enquiries`
29, `work` 11, `gates` 20). `diff` printed nothing for any of them. The
one visible change is `gates --state closed`:

```
before: Gates — 0 / nothing (exit 0)
after:  labkit: Invalid option: expected one of "never-evaluated"|"incomplete"|"blocked"|"satisfied" (exit 1)
```

`bun run bonsai:record` with this binary:

```
107/107 criteria recorded
107/107 evaluations recorded, 57 citing a measurement
gate GATE_243, task TASK_135, enquiry LOE_134, 107 criteria/evaluations from experiments/stage2b_denoising/gates.toml
OK: the live event stream and graph state are exactly what the scripts produce, commit hashes and clocks aside.
OK: record built (…/bin/labkit --db …/tmp/bonsai), replay clean.
```

### What nothing writes any more

Graph schema and migrations are unchanged. Declared: 14 node labels and
31 edge labels; written: 13 and 25.

- **Label:** `Review`.
- **Edge labels:** `REVERIFIES`, `GRADES`, `KEEPS`, `SHARPENS`,
`EVALUATES`, `INVALIDATED_BY`.
- **Endpoint pairs on edges still written elsewhere:** MOTIVATES
Decision→Question, MOTIVATES Decision→Computation, SUPERSEDES
Decision→Computation, CLOSES Decision→Gate, BASED_ON Decision→Review,
GATES Gate→Computation, PRODUCES Task→Computation, PRODUCES
Task→Artefact, and CONCERNS/MENTIONS Note→Review.
- **Properties:** `retracted` (nothing calls `UnitOfWork.set`).
`ClaimProps.kind` still allows `"undecided"`. `provisioning.ts` still
installs the `<label>_hide_retracted` RLS policy, and with
`retraction.test.ts` gone no test exercises it.

Reads of these that were outside this brief and are still in place:
`reverifiedBy`/REVERIFIES in `whySupported` and the "Re-checked by"
view; the Computation lineage, KEEPS and the changed/restated/unpaired
lists in `analysisRevision`, which now always return the empty shape;
and the withdrawn-verdict helper in `checks.ts`.

## 2. `labkit gates` lines its columns up with colour on

`rows()` padded by string length, escape codes included. It now pads by
visible width. The test renders `renderGateList` with colour on, strips
the escape codes, and compares the result with the plain rendering. It
failed before the fix:

```
- "blocked     GATE_1  no publication
+ "blocked              GATE_1  no publication
```

Compiled binary, `FORCE_COLOR=1 labkit gates | strip-ansi`:

```
before:                                     after:
never-evaluated      GATE_4  no publication   never-evaluated  GATE_4  no publication
holding up  TASK_3  write it up               holding up       TASK_3  write it up
```

## 3. The record daemon's log lines take the CLI's colour decision

`colourWanted` moves to `packages/core-db/colour.ts`, because core-db
cannot import app-cli. `stderrLine` sits beside it and writes one line
to stderr, in red when `colourWanted` says so. The daemon's `log()`, the
wire host's two lines, the daemon entry's usage line, the client's
"replacing it" line and the CLI's refusals all go through it. core-db
has no access to `--no-ansi`, so the daemon decides from the environment
alone. Bun colours `console.error` whenever `FORCE_COLOR` is set, even
beside `NO_COLOR=1`:

```
$ echo 'console.error("x")' > ce.ts
$ NO_COLOR=1 bun ce.ts 2>&1 | od -c     # FORCE_COLOR=3 inherited from the shell
033 [ 0 m 033 [ 3 1 m x 033 [ 0 m \n
```

Daemon log, fresh `--db` each, under `FORCE_COLOR=3 NO_COLOR=1`:

```
before: ^[[0m^[[31m2026-10-01T00:30:39.226Z [labkit daemon 63989] serving …^[[0m
after:  2026-10-01T00:30:42.025Z [labkit daemon 64191] serving …
```

`tests/persistence/daemon-log-colour.test.ts` runs the daemon entry
under `FORCE_COLOR=1` (the control, which must be coloured),
`FORCE_COLOR=3 NO_COLOR=1` and `FORCE_COLOR=0`.

## 4. The `--http` launcher test clears the colour variables for its
child

The child now gets `process.env` without `FORCE_COLOR`, `NO_COLOR`, `CI`
and `TERM`, which are the variables `colourWanted` reads.

```
before: FORCE_COLOR=3 bun test packages/app-acp/http.test.ts
  Received: "http://127.0.0.1:55791/acp\u001B[0m"
  13 pass, 1 fail
after:  FORCE_COLOR=3 bun test packages/app-acp/http.test.ts
  14 pass, 0 fail
```

## 5. `labkit why` on an accepted enquiry prints the reason and the
reopening condition

```
before:
LOE_1 is open — its question is accepted as unresolved because
  - (Q_1)  does the schedule move convergence? — currently accepted
after:
LOE_1 is open — its question is accepted as unresolved because
  - (Q_1)  does the schedule move convergence? — currently accepted
accepted because: the effect is below seed noise
reopens if: a run with ten seeds
```

The `accept` help text now points at `labkit why` and no longer at
`labkit --json why`. `tests/cli/enquiries.test.ts` asserts both lines
through the CLI.

## 6. The deleted root README

`infra/ci/triggers.tf` no longer lists `README.md` in `ignored_files`.
`infra/ci/README.md` had the same list in prose, and it is updated too.
No other file referred to the root README; every other `README.md` hit
is a package README or test data.

## Not in this PR: removing the MCP read-only mode

The coordinator also passed on a request to remove `labkit mcp
--read-only`, the `readOnly` paths in `app-mcp/server.ts` and `docs.ts`
with their tests, and the `MCP_CALL_WRITE` switch in the skill's
`mcp-call.sh`. I made the change, and typecheck, the MCP tests (22 pass)
and `mcp-call.sh tools/list` (9 tools, 4 with `readOnlyHint`) all
passed. The permission system then refused the commit as test removal.
The change is not in this branch. It is saved as a patch outside the
repo for Dan to decide on.

## Checks

- `bun test`, before: 1644 pass, 2 skip, **1 fail** (`http.test.ts`
under this shell's `FORCE_COLOR=3`), 1647 tests across 221 files.
- `bun test`, after: **1647 pass, 2 skip, 0 fail**, 1649 tests across
221 files. The difference: +1 gates-colour test, +3 daemon-colour tests
in a new file, −1 undecided view test, −1 `retraction.test.ts` (one
test, one file).
- `bun run check:quick`: all 25 passed.
- Rebased onto `origin/main` (e4250a4) before pushing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01FWZHuy7xJbVqxyLipnvwdY

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:record The record: core-domain, core-db, the CLI, the MCP server, the domain model and workspaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant