The project map helps to work out where the code, the comments, and the potential noise are concentrated. It does not replace the full list of comments found; it is a way to choose a scope for further review.
The map is built on a shared model used by the desktop, terminal, and command-line interfaces.
The main entities:
ProjectMap— an immutable snapshot of the full tree and the metrics of a singleScanId;ProjectNode— a project, directory, or file node with a fundamentalProjectNodeId;ProjectMetrics— the metrics of a node that have already been collected;ProjectMapViewModel— the derived state of the scope, the test filter, navigation, and the metric selection;- the stable node identifier — a UUIDv5 of the
ProjectId, the node type, and the relative POSIX path.
One and the same surviving path has the same ProjectNodeId in different snapshots of the
project. A module that has been added appears only in the new snapshot; one that has been deleted
is absent from it. A snapshot does not mix the metrics, comment ranges, or other facts of two
different scans.
The shared model is responsible for:
- the hierarchy of directories and files;
- the computed metrics;
- a single current project scope;
- going one level deeper and coming back up;
- the navigation chain;
- the inclusion and exclusion of tests;
- switching between metrics;
- the link between the map, the tree, and the full list.
The centre of the map, the active node of the tree, and the last element of the path chain always denote one current scope. The three-state check marks used to select comments and files in the full list belong to the future Review model: they do not stand in for the current map scope and they do not create an edit decision.
Presentation components must not read the filesystem or SQLite, and must not compute domain metrics on their own.
The map carries one of the statuses current, changes_detected, rescanning, or stale. That
fact is supplied by the application layer; the map core does not watch the filesystem and does not
correct old numbers on a single event. A change to code or to a comment made by another developer,
by an agent, or by a future Apply makes the ranges and metrics belonging to the old snapshot out
of date. After the next consistent scan the saved scope, filter, and metric are carried over by
their previous id; if the path is gone, the nearest surviving ancestor is chosen, otherwise the root.
A comment that is new after the scan is treated on the usual terms. The label "changed since the last scan" is possible only after two comments have been matched unambiguously; a doubtful case reports no more than that the file has changed. Matching and showing the old and the new text belong to history and to the full list; they are not grounds for an automatic action.
For every node there are at least:
- the volume of code;
- the volume of comments;
- the number of rule findings;
- the volume of cleanup candidates;
- the density of comments relative to code;
- the number of files;
- the split between application and test code;
- the number of protected and risky comments;
- the estimated volume of comments in tokens and the estimated volume of candidates in tokens.
Metrics must agree between the interfaces. Hiding small nodes or changing the display depth does not change the underlying data.
Files and directories excluded by .gitignore rules, by hard safety, or by an explicit discovery
policy stay in the scan diagnostics together with the reason for the exclusion, but they do not
enter the derived presentation of the map: neither the tree, nor the radial diagram, nor their
metrics. This is not the removal of a fact from the snapshot and does not require re-reading files.
The first selection of map metrics is defined exactly: Code is physical lines of code,
Comments is Tree-sitter-confirmed comment lines, and Findings and Candidates are unavailable
until the rules engine exists rather than equal to zero. Lexically observed .rc/.rc.in
comments stay separate partial facts and are not mixed with Comments.
Density equals the number of confirmed comment lines per thousand lines of code:
confirmed_comment_line_count * 1000 / code_line_count. The calculation is integer with rounding
to the nearest; with zero code it is unavailable, and while one of the input figures is partial
it stays partial.
Every computed value carries the state complete, partial, or unavailable.
A zero means a measured zero, not the absence of data. For example, the candidate figures are
unavailable rather than zero until the rules engine exists; a file with a parse error may yield
partial observable figures, but not full accuracy.
The file counters form a verifiable partition of the discovery manifest: discovered, included, successfully parsed parser-backed, parsed with errors, examined lexically, unsupported, excluded, and skipped with a stated reason. The metric of a directory equals the sum of the metrics of its descendants; the interface does not re-read files and does not recompute domain values when the presentation changes.
Line metrics use physical lines. Under a full classification
blank + comment-only + code = total lines holds. A line of code with an inline comment stays a
line of code, while inline comments are counted by a separate overlapping metric. The number of
logical comments does not equal the number of comment lines: a group of consecutive // lines is
one comment, but it covers several physical lines.
Tree-sitter-confirmed comments and lexically observed .rc/.rc.in comments are shown
separately. The latter are not mixed into the confirmed totals without an explicit mark of limited
reliability. The semantic kind, the placement, and the protection of a comment are independent
axes, so, for example, inline comments, the TODO family, and protected comments are not summed as
mutually exclusive categories.
Source code is read by language models many times over: several times within one working session of a coding agent, across many sessions, and also when differences are handed over for review and when the repository is searched. A comment that retells the line next to it takes up space in the context on every such read and says nothing that could not be derived from the code beside it.
The token estimate moves the volume of noise out of the aesthetic category and into a measurable one. It answers the question "how much context is spent on text that carries no information", and serves as an understandable summary value for a node, a file, and the whole project.
The metric describes the volume for one read. The cumulative saving depends on how often the code is read and is not estimated by the tool.
For every node the following are computed separately:
- the estimated volume of all comments;
- the estimated volume of candidate comments;
- the share of the node's total volume that the second value makes up.
For an individual comment the volume includes the text together with its markers. For a standalone comment the leading whitespace of its line and the trailing newline are counted as well: removing it makes the whole line disappear. For an inline comment only the comment itself is counted, together with the whitespace that separates it.
The candidate estimate is an upper bound on the possible saving, not a plan. A candidate is not a decision: the user will keep some of the findings, and a fragment edit removes only part of a comment. The interface must call this value possible rather than achieved, and must not turn it into an urge to remove. Review priority, risk, and the decision remain independent properties.
The exact number of tokens depends on the particular model and its vocabulary. CommentRake does not add a tokeniser: that would be a dependency taken on for a presumed benefit, and the result would still hold for one model only.
Instead, an approximate estimate is computed from the number of characters, with a coefficient that depends on the composition of the text. Latin letters, digits, and punctuation, which is what code and an English-language comment are made of, give roughly four characters per token. Cyrillic and other scripts outside basic Latin give noticeably fewer — about two and a half. The coefficient is therefore determined by the actual character composition of the comment, not by the language of the file: comments are often mixed.
The first version of the formula uses no floating point numbers: every ASCII character is assigned five arbitrary units, every base non-ASCII character eight, and standalone combining marks add no units; the total is rounded up at twenty units per token. This gives roughly four ASCII characters or two and a half non-ASCII characters per estimated token, and the same result on every platform. The version of this formula is stored together with the metric.
Requirements on the computation:
- an integer formula without
floatis used: the same text gives the same number on any platform; - the coefficients are part of the versioned metrics contract; changing them is a change of the contract and requires a new version;
- a stored snapshot contains the version of the coefficients, otherwise comparing snapshots will not be trustworthy;
- the value is called an estimate everywhere; no exact number of tokens is claimed;
- aggregation is done as for the other metrics: file, directory, project.
The value goes into the node summary, into the output of the command-line interface, and into the machine-readable JSON on the same footing as the other metrics. Adding it as a separate option of the outer ring of the radial diagram is possible, but that is a change to an approved mockup and goes through the usual interface approval procedure.
The main presentation is a multi-level radial diagram, resembling the diagrams that show how occupied disk space is distributed.
The screenshot is of a real 983-file project. On the left is the project tree with marks showing what is excluded from the counts; in the centre the diagram, where the sector angle is code volume and the outer ring is the selected secondary metric; on the right a summary of the selected scope and the way into Review. The "Angle" and "Outer ring" switches above the diagram set both channels, and the legend below spells them out.
Requirements:
- a single click on a directory — in the diagram or in the project tree — makes it the map's current scope: the diagram re-centres on it, and the tree selection and the breadcrumb path follow the move;
- a single click on a file selects it and shows its details, without re-centring the diagram: a file has no children, so there is nothing to centre it on;
- a double click drills into a directory;
- a double click on a file opens it in Review: a file has no children, so there is no entering it. This applies both to the diagram and to the project tree; opening Review is only a navigation step and selects no edit at all;
- the centre of the diagram, the highlighted node of the tree, and the last element of the path chain always denote one and the same current scope;
- the navigation chain returns to the previous level;
- the project tree and the diagram are synchronised;
- the selected scope can be opened in the full list;
- from the list one can return to the exact node of the map;
- the display depth adapts to the number of nodes and the size of the window;
- small sectors may be merged at the presentation level only; the metadata of the merge contains
every
ProjectNodeIdit represents and the metric recomputed over the leaves; - hidden small nodes stay available in the tree and in the list;
- resizing the window does not change domain data;
- light and dark themes, high pixel density, and scaling must work predictably.
The map shows code and comments at once: the sector angle means the volume of code, and the thin outer ring the value of the selected secondary metric. In the first version the ring switches between the number of comments, the number of candidates, and density; the legend explains both channels explicitly. Switching the ring does not hide the volume of code.
A file that has comments but no code gets no angle under the "angle = code" rule and by default does not reach the diagram; in the project tree it is marked with an explanation and stays available. A separate "Show comment-only files" switch brings such nodes onto the diagram. The angle still remains the volume of code: the node gets the smallest possible share, not a share computed from the number of comments. A directory that has no code either is shown along with it, otherwise the nested file would be unreachable. The switch concerns the presentation only: neither the snapshot, nor the metrics, nor the tree change because of it.
At the top level one must not try to show thousands of small files at once. After drilling in, only the level of detail of the presentation over the already collected snapshot changes; no new scan is started.
The full list is mandatory even when there is a diagram. It supports:
- sorting and search;
- filtering by file, language, rule, comment kind, and risk;
- filtering by file type, out of the files actually found and permitted by the profile;
- filtering by test and application code;
- the selection of individual items, of groups, and of files;
- viewing the source code and the current comment;
- viewing the original text of the comment with its markers and its number of lines; a continuous group of comment lines is shown as one item;
- navigation to a map node;
- the preparation of decisions for the edit plan;
- working with thousands of results.
A bulk action cannot bypass protection indicators.
The terminal interface uses the same hierarchy without necessarily drawing a circle. The basic variant:
- a proportional tree;
- a linear breakdown of the selected scope;
- metric columns;
- drilling in and moving up from the keyboard;
- the navigation chain;
- moving between the map and the full list;
- adapting the level of detail to the width of the terminal.
A circular presentation made of Unicode or Braille characters is possible only as an experimental addition, after a convenient linear variant exists.
The command-line interface outputs:
- a project summary;
- a ranked list of cleanup scopes;
- the metrics of the selected scope;
- the tree in textual form;
- JSON with the same node identifiers and metrics.
Tests are a separate analytical layer. They are neither ignored entirely nor mixed with application code without an instruction from the user.
A file can be classified as:
- application code;
- test code;
- an unknown kind.
The determination takes into account the location, the file name, the conventions of the language,
and the user configuration. It does not rely on the tests directory alone, does not read the
content in order to guess the purpose, and shows the evidence and the source of the result. A
conflict between equal rules is stored as unknown. Tests inside a Rust file and other mixed
sources will later be accounted for by parser-backed ranges, not by mistakenly reclassifying the whole file.
If the data has already been collected, switching tests must not require re-reading files. In the
first variant the modes with_tests and without_tests are available; an unknown classification
is not hidden automatically. The filter changes only the scope and the metrics of the presentation, but not the snapshot.
The following should be checked:
- projects with a deep hierarchy;
- thousands of small files;
- a large number of automatically generated tests;
- a great many excluded files;
- the time to compute the radial layout;
- the identification of the sector under the pointer;
- resizing;
- drilling in;
- the synchronisation of the tree, the map, and the list.
Optimisation is done after the correctness of the shared model has been confirmed and the bottlenecks measured.
The map keeps a metrics record for every file that was scanned. Files the rules excluded —
.gitignore, soft exclusions, an explicit policy — get no record at all: nobody reads them, parses
them or shows them, and keeping a row of metrics to state the single fact that such a file exists
is not worth it (owner decision, 2026-08-16). The answer to "why is this file missing" stays in the
discovery facts stored with the scan, which name both the path and the rule that excluded it.
Binary, oversized and unreadable files do keep their records: nobody excluded them, they lie among the sources, and "this is a picture, not text" is an answer a person needs. They earn no node on the map, but they are counted in the totals of their scope.
Measured on 2026-08-16 on a project of 1,042 included files and 21,152 comments (1,290 discovery records):
| Action | Time |
|---|---|
| Full scan | 25.7 s |
| Writing the snapshot | 2.6 s |
| Opening a stored scan whole | 5.9 s |
| Reading only the map | 0.6 s |
| Metadata freshness check | 0.1 s |
| Partial rescan of one file | 1.0 s |
The earlier figures in this section (19 seconds to read the map, 16,043 records for 1,494 files)
describe the state before the walk was pruned by .gitignore: an ignored directory is no longer
opened at all, so there are an order of magnitude fewer discovery records and that cost went with
them.
The direction of work: load the subtrees of the map as navigation goes on rather than all at once,
and stop decoding a whole snapshot on opening where only the map is needed. The recommendation is
unchanged: exclude from the scan scope the large directories that certainly contain no source code
(node_modules, build directories, caches); most of them are already excluded by default.
- One model is used by all three interfaces.
- The same identifier means one project scope.
- Metrics agree in every presentation.
- The test filter has the same semantics.
- Drilling in and coming back do not depend on the way things are displayed.
- The navigation chain is built from the shared hierarchy.
- The map, the tree, and the list use stable identifiers.
- A presentation component does not read the filesystem or SQLite.
- Merging small nodes does not delete the original nodes.
- The full list stays available regardless of the way things are displayed.
