/** * What the left rail shows, as data. * * The rail is two trees over things the session already has — the saved views * with the reviewer's staged view operations merged over them (ADR 0114), and * the subjects the model declares — so everything it decides is a pure * function of those plus what the reviewer typed. It lives outside the * component for the reason `filterToReresolve` does: this repo renders React * with `renderToStaticMarkup` and has no DOM test environment, so logic that * hides inside a component is logic no test can reach. * * No import of React, cytoscape, or anything from `node:` — the same * zero-dependency discipline as `badges.ts` and `kind-icons.ts`. */ import type { CanvasNode } from "../graph-projection.js"; import type { Layer } from "../profile.js"; import type { VisualViewOperation, VisualViewSummary } from "../adapters/visual/protocol-contract.js"; import { type FilterableSubject } from "./subject-filter.js"; export declare const VIEWS_ROOT_KEY = "views"; export declare const MODEL_ROOT_KEY = "model"; /** Collapse keys are strings so one set holds every branch of both trees. */ export declare const folderKey: (folder: string) => string; export declare const layerKey: (layer: string) => string; /** A declared folder in the MODEL tree, kept apart from a layer of the same * name: an author may call a folder `business`, and it is not that layer. */ export declare const modelFolderKey: (folder: string) => string; /** Where a subject with no resolved layer is grouped, last. */ export declare const UNLAYERED = "unlayered"; /** * How a row relates to the pending changeset (ADR 0114). `"new"` is a staged * `write-view` at a path nothing has landed; `"overwrite"` is one at a path a * landed view occupies; `"delete"` is a staged `delete-view`. A landed row * nothing pending touches carries `null`. */ export type ViewRowStaging = "new" | "overwrite" | "delete"; export interface ViewTreeRow { readonly id: string; readonly title: string; readonly path: string; /** * `null` when nothing has measured it: a staged NEW view has no landed * document, and resolving its query needs the semantic graph the browser * does not hold — and, while the tree filter narrows, every landed view * but the active one, whose subjects live only in that same graph (#317). * A number is either the server's measure of what LANDED (a staged * overwrite keeps the landed count, the same staleness story * `VisualViewSummary.subjectCount` already tells) or, on the active row, * a count of the drawn subjects that survive the typed filter. */ readonly subjectCount: number | null; readonly active: boolean; readonly staged: ViewRowStaging | null; } export interface ViewTreeFolder { readonly key: string; /** The label its views declare, verbatim. */ readonly name: string; readonly views: readonly ViewTreeRow[]; } export interface ViewTree { readonly folders: readonly ViewTreeFolder[]; /** Views in no folder, shown under the root above the folders. */ readonly loose: readonly ViewTreeRow[]; /** Rows that survived the filter, across folders and all. */ readonly matched: number; } export interface ModelSubjectRow { readonly id: string; readonly name: string; readonly kindLabel: string; readonly layer: Layer | null; /** The folder the author filed it under, or null. */ readonly folder: string | null; /** * Whether the canvas is drawing this subject. Read from the standing * filter's match set, never from the graph: the graph holds every subject * the workspace declares, and the whole point of the marker is to name the * ones it holds that the active view leaves out. */ readonly inView: boolean; /** * How many subjects this one holds inside it when the active view draws it * FOLDED, or `null` where it is not a folded box (#473). * * The rail is where a reader looks when the canvas has hidden something, so * a folded instance has to say what it swallowed. `null` rather than `0`: * "not a folded box" and "a folded box holding nothing" are different, and * only the second should draw a count. */ readonly foldedCount: number | null; } export interface ModelTreeGroup { readonly key: string; readonly label: string; /** * What put these subjects together. A `layer` group is DERIVED and always * correct - every subject has one, or is unlayered - and a `folder` group is * DECLARED, which is neither. The rail says which so a reader knows whether * moving a subject means editing a document or changing its kind. */ readonly grouping: "folder" | "layer"; readonly subjects: readonly ModelSubjectRow[]; } /** * Whether a label survives what the reviewer typed into the rail. * * Exported for the one row that is not a saved view — the unfiltered "all * subjects" entry — which has no summary to pass through `buildViewTree` but * must narrow with everything else, or a search that matches nothing still * leaves a view row standing above the line saying nothing matched. */ export declare const matchesFilter: (label: string, filterText: string) => boolean; /** * The folder a view files itself under, or `""` for none. * * DECLARED, never derived (ADR 0104). Folders used to fall out of the * directories the projections happened to sit in, which made a folder a * consequence of the filesystem: a workspace could not name one without moving * files, a manifest whose patterns reach no subdirectory could not have one at * all, and "New folder" meant "write somewhere the manifest may not load". * A label answers all three, and it is the same word `yarramate/likec4-project/v1` * already uses for the same thing (ADR 0067). * * One level: the tree draws a folder, not a folder tree, so `current/target` * is one folder called `current/target` rather than two nested ones. The * separator is reserved so nesting can be drawn later without the label * meaning something different. */ export declare const folderOf: (view: VisualViewSummary) => string; export interface ViewTreeInput { readonly views: readonly VisualViewSummary[]; /** * The pending changeset's view operations, merged over `views` so the rail * shows the reviewer's own staged intent beside landed truth (ADR 0114). * A staged `write-view` at a new path becomes a row; one at a landed path * marks that row and shows what WILL land; a staged `delete-view` marks the * row rather than hiding it. Discarding an operation removes it from this * list, which is the whole revert — the tree derives, it never remembers. */ readonly stagedOperations: readonly VisualViewOperation[]; readonly activeViewId: string; /** * The subjects the active view is drawing right now, from the standing * filter's match set, or `null` when nothing is filtering the canvas. The * subjects themselves rather than their count (#317): the active row's * number is how many of them survive the typed filter, which a * pre-computed count could not answer. Unfiltered it is their plain * length — the number the reviewer can check by looking, where the * server's `subjectCount` was only true when the frame carrying it was * sent. */ readonly activeSubjects: readonly FilterableSubject[] | null; readonly filterText: string; } export declare const buildViewTree: ({ views, stagedOperations, activeViewId, activeSubjects, filterText, }: ViewTreeInput) => ViewTree; export interface ModelTreeInput { readonly nodes: readonly CanvasNode[]; /** * The subjects the canvas is drawing, or `null` when nothing is filtering it * and every subject is therefore in view. */ readonly inViewIds: ReadonlySet | null; readonly filterText: string; /** * What the active view draws folded, and what each of those boxes holds * (#473). Optional, so every existing caller keeps compiling: absent means * nothing is folded, which is what the rail said before folding existed. */ readonly folded?: ReadonlySet; readonly insideCounts?: ReadonlyMap; } export declare const buildModelTree: ({ nodes, inViewIds, filterText, folded, insideCounts, }: ModelTreeInput) => readonly ModelTreeGroup[];