relore 0.3.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relore/__init__.py +12 -0
- relore/api/__init__.py +0 -0
- relore/api/schemas.py +363 -0
- relore/api/server.py +781 -0
- relore/api/tokens.py +219 -0
- relore/api/ui.py +812 -0
- relore/bench/__init__.py +10 -0
- relore/bench/dataset.py +220 -0
- relore/bench/judge.py +155 -0
- relore/bench/mine.py +550 -0
- relore/bench/run.py +433 -0
- relore/cli.py +972 -0
- relore/code/__init__.py +53 -0
- relore/code/api.py +105 -0
- relore/code/blame.py +398 -0
- relore/code/clone.py +105 -0
- relore/code/defs.py +61 -0
- relore/code/providers/__init__.py +0 -0
- relore/code/providers/ctags.py +104 -0
- relore/code/providers/python.py +65 -0
- relore/code/providers/treesitter.py +176 -0
- relore/code/refresh.py +79 -0
- relore/code/refs.py +97 -0
- relore/code/registry.py +137 -0
- relore/code/repomap.py +123 -0
- relore/code/survey.py +297 -0
- relore/code/walk.py +96 -0
- relore/daemon.py +828 -0
- relore/duration.py +25 -0
- relore/entrypoints.py +42 -0
- relore/freshness.py +14 -0
- relore/github/__init__.py +0 -0
- relore/github/bulk.py +142 -0
- relore/github/client.py +243 -0
- relore/github/fetch_thread.py +107 -0
- relore/github/graphql.py +235 -0
- relore/guidance.py +176 -0
- relore/ingest/__init__.py +0 -0
- relore/ingest/authority.py +202 -0
- relore/ingest/backfill.py +305 -0
- relore/ingest/chunk.py +107 -0
- relore/ingest/extract.py +534 -0
- relore/ingest/index_thread.py +536 -0
- relore/ingest/normalize.py +101 -0
- relore/ingest/poll.py +236 -0
- relore/ingest/relationships.py +92 -0
- relore/ingest/sample.py +251 -0
- relore/ingest/sweep.py +85 -0
- relore/ingest/timestamps.py +31 -0
- relore/ingest/versions.py +18 -0
- relore/render.py +1044 -0
- relore/search/__init__.py +55 -0
- relore/search/backends/__init__.py +35 -0
- relore/search/backends/base.py +1595 -0
- relore/search/backends/postgres.py +162 -0
- relore/search/backends/sqlite.py +96 -0
- relore/search/expansion.py +335 -0
- relore/search/queries.py +640 -0
- relore/search/ranking.py +224 -0
- relore/security/__init__.py +0 -0
- relore/security/untrusted.py +201 -0
- relore/store/__init__.py +0 -0
- relore/store/dialect.py +189 -0
- relore/store/migrations/__init__.py +252 -0
- relore/store/repository.py +772 -0
- relore/store/schema.py +346 -0
- relore/wire.py +95 -0
- relore-0.3.17.dist-info/METADATA +125 -0
- relore-0.3.17.dist-info/RECORD +73 -0
- relore-0.3.17.dist-info/WHEEL +5 -0
- relore-0.3.17.dist-info/entry_points.txt +6 -0
- relore-0.3.17.dist-info/licenses/LICENSE +201 -0
- relore-0.3.17.dist-info/top_level.txt +1 -0
relore/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""relore -- GitHub project memory."""
|
|
2
|
+
|
|
3
|
+
#: The one place the version is written. ``pyproject.toml`` reads it from here, so the
|
|
4
|
+
#: distribution, ``relore --version``, ``relored --version``, the ``/api/v1/status``
|
|
5
|
+
#: payload and the wire handshake (:mod:`relore.wire`) can never disagree.
|
|
6
|
+
#:
|
|
7
|
+
#: The minor is the milestone the build plan considers complete; the patch is every
|
|
8
|
+
#: release inside it. **Bump it in the same commit as any change a client can see** --
|
|
9
|
+
#: a wire payload, a renderer, a CLI flag -- because the client and the daemon must run
|
|
10
|
+
#: the same version to talk at all (:mod:`relore.wire`), which makes this string the
|
|
11
|
+
#: compatibility contract rather than a label.
|
|
12
|
+
__version__ = "0.3.17"
|
relore/api/__init__.py
ADDED
|
File without changes
|
relore/api/schemas.py
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Request and response shapes, versioned from the first commit (section 7).
|
|
2
|
+
|
|
3
|
+
The moment a second consumer exists, renaming a field breaks a stranger -- so the fields
|
|
4
|
+
of section 7 are all present now, including the ones whose *effect* is a later milestone.
|
|
5
|
+
``kind`` is the clearest case: it selects section 6's decay regime and section 6.2's trust
|
|
6
|
+
floor, neither of which exists yet, and it is accepted and echoed anyway because adding it
|
|
7
|
+
later would be a breaking change to a shipped API for no reason.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime as dt
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
from relore.freshness import THREAD_STAMP_NOTE
|
|
18
|
+
from relore.search.queries import (
|
|
19
|
+
HUMAN_TRUST,
|
|
20
|
+
MACHINE_TRUST,
|
|
21
|
+
MAX_HITS,
|
|
22
|
+
QUERY_KINDS,
|
|
23
|
+
SORTS,
|
|
24
|
+
Hit,
|
|
25
|
+
InflightView,
|
|
26
|
+
ThreadView,
|
|
27
|
+
WhyView,
|
|
28
|
+
render_stamp,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SearchRequest(BaseModel):
|
|
33
|
+
"""``POST /api/v1/search``.
|
|
34
|
+
|
|
35
|
+
``limit`` is clamped, not validated: a client asking for 500 gets 10 (section 6). The
|
|
36
|
+
signal filters read the tables section 5.3's extraction pass fills.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
query: str = ""
|
|
40
|
+
kind: str | None = Field(default=None, description=f"one of {list(QUERY_KINDS)}")
|
|
41
|
+
trust: str | None = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
description=(
|
|
44
|
+
f"raise the floor to one of {list(HUMAN_TRUST)}, or {MACHINE_TRUST!r} to ask the "
|
|
45
|
+
"separate question of what the deployment's own bots claimed. The server decides "
|
|
46
|
+
"the default and a request can never lower it"
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
repos: list[str] = Field(
|
|
50
|
+
default=[],
|
|
51
|
+
description=(
|
|
52
|
+
"narrow to these repositories. It can only ever *subtract* from the token's "
|
|
53
|
+
"scope (section 11): a name the token cannot see is dropped, not granted, so "
|
|
54
|
+
"asking for one returns nothing rather than an error. Empty means every "
|
|
55
|
+
"repository already in scope"
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
files: list[str] = []
|
|
59
|
+
symbols: list[str] = []
|
|
60
|
+
errors: list[str] = []
|
|
61
|
+
tests: list[str] = []
|
|
62
|
+
labels: list[str] = []
|
|
63
|
+
since: dt.datetime | None = None
|
|
64
|
+
limit: int = MAX_HITS
|
|
65
|
+
sort: str = Field(
|
|
66
|
+
default="relevance",
|
|
67
|
+
description=(
|
|
68
|
+
f"one of {list(SORTS)}. Ordering only -- it reorders the same hits relevance "
|
|
69
|
+
"would have chosen, and never changes which ones they are"
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
compact: bool = Field(
|
|
73
|
+
default=False, description="shorter snippets, no score internals, for a tight budget"
|
|
74
|
+
)
|
|
75
|
+
render: bool = Field(
|
|
76
|
+
default=False,
|
|
77
|
+
description=(
|
|
78
|
+
"also return the exact text the CLI would print, envelope included. What "
|
|
79
|
+
"section 8's 'view as the model sees it' displays; off by default because an "
|
|
80
|
+
"agent reading the JSON would be paying for the same content twice"
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
presentation: bool = Field(
|
|
84
|
+
default=False,
|
|
85
|
+
description=(
|
|
86
|
+
"render for a person at a TTY: the same facts plus the backend tag and the "
|
|
87
|
+
"flags worth trying next. The CLI sends it when stdout is a terminal (#13)"
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
expand: bool = Field(
|
|
91
|
+
default=True,
|
|
92
|
+
description=(
|
|
93
|
+
"section 6's query expansion: fan the call out into error, test, symbol, file "
|
|
94
|
+
"and free-text legs and merge them. On by default because the plain search "
|
|
95
|
+
"ANDs every content term, which is what makes a pasted traceback match "
|
|
96
|
+
"nothing. Turn it off to ask exactly one question"
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class LabelRequest(BaseModel):
|
|
102
|
+
"""``POST /api/v1/label`` -- section 8's relevance judgement, section 10's ground truth."""
|
|
103
|
+
|
|
104
|
+
query: str
|
|
105
|
+
repo: str
|
|
106
|
+
number: int
|
|
107
|
+
source_type: str
|
|
108
|
+
verdict: str = Field(description="relevant | not_relevant | decisive")
|
|
109
|
+
kind: str | None = None
|
|
110
|
+
#: The signal filters the judged search actually carried. A label is only
|
|
111
|
+
#: interpretable next to the whole query, and two examples can share the text and
|
|
112
|
+
#: differ only in their `--file` -- folding the labels back into section 10's
|
|
113
|
+
#: evaluation set has no way to tell them apart without this.
|
|
114
|
+
filters: dict[str, list[str]] = {}
|
|
115
|
+
note: str = ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def hit_json(hit: Hit, *, compact: bool = False) -> dict[str, Any]:
|
|
119
|
+
"""One hit, as served.
|
|
120
|
+
|
|
121
|
+
``trust`` and ``age`` are always present and always rendered: age changes what a model
|
|
122
|
+
concludes, authority changes it more (section 6, section 6.2). ``compact`` drops the
|
|
123
|
+
score and its breakdown, which only a person tuning weights has any use for.
|
|
124
|
+
"""
|
|
125
|
+
out: dict[str, Any] = {
|
|
126
|
+
"repo": hit.repo,
|
|
127
|
+
"number": hit.number,
|
|
128
|
+
"type": hit.thread_type,
|
|
129
|
+
"title": hit.title,
|
|
130
|
+
"source_type": hit.source_type,
|
|
131
|
+
"url": hit.url,
|
|
132
|
+
"author": hit.author,
|
|
133
|
+
"trust": hit.trust,
|
|
134
|
+
"age": hit.age,
|
|
135
|
+
# The age is for reading, this is for citing. JSON only, and never instead of the
|
|
136
|
+
# age: a bare timestamp makes a model do arithmetic it will skip
|
|
137
|
+
# (`render_stamp`, huggingface/relore#66).
|
|
138
|
+
"date": render_stamp(hit.created_at),
|
|
139
|
+
"snippet": hit.snippet,
|
|
140
|
+
# Which comment, and which piece of it (huggingface/relore#18). `url` is not an
|
|
141
|
+
# identity: a chunk of a comment carries the comment's URL, so two hits from one
|
|
142
|
+
# 9,827-character comment were indistinguishable from two people agreeing.
|
|
143
|
+
# `document_id` is stable across rebuilds -- it is GitHub's id plus the chunk --
|
|
144
|
+
# where the row's primary key is not.
|
|
145
|
+
"document_id": f"{hit.source_type}:{hit.source_id}:{hit.chunk_index}",
|
|
146
|
+
"source_id": hit.source_id,
|
|
147
|
+
"chunk_index": hit.chunk_index,
|
|
148
|
+
}
|
|
149
|
+
# Absent rather than 1 where nobody counted: corpus-wide search does not pay for the
|
|
150
|
+
# aggregate, and "1 of 1" would be a claim it cannot make.
|
|
151
|
+
if hit.passages:
|
|
152
|
+
out["passages"] = hit.passages
|
|
153
|
+
# Only where it is true, and only where somebody measured it: `search` does not, and a
|
|
154
|
+
# `false` it did not check would be a claim. The page uses it to decide whether it owes
|
|
155
|
+
# the caller the address of the rest (huggingface/relore#71).
|
|
156
|
+
if hit.truncated:
|
|
157
|
+
out["truncated"] = True
|
|
158
|
+
if not compact:
|
|
159
|
+
out["score"] = round(hit.score, 6)
|
|
160
|
+
out["breakdown"] = {k: round(v, 6) for k, v in hit.breakdown.items()}
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def inflight_json(view: InflightView) -> dict[str, Any]:
|
|
165
|
+
"""``GET /api/v1/inflight/{n}`` -- what claims to close this thread.
|
|
166
|
+
|
|
167
|
+
``links_indexed`` is here for the reason every count in this module is: an index with
|
|
168
|
+
no relationship rows answers every question with an empty list, and an agent cannot
|
|
169
|
+
tell that from "nobody is working on this" -- which are opposite instructions.
|
|
170
|
+
"""
|
|
171
|
+
return {
|
|
172
|
+
"repo": view.repo,
|
|
173
|
+
"number": view.number,
|
|
174
|
+
"claims": [
|
|
175
|
+
{
|
|
176
|
+
"repo": claim.repo,
|
|
177
|
+
"number": claim.number,
|
|
178
|
+
"type": claim.thread_type,
|
|
179
|
+
"title": claim.title,
|
|
180
|
+
"url": claim.url,
|
|
181
|
+
"author": claim.author,
|
|
182
|
+
"state": claim.state,
|
|
183
|
+
"draft": claim.draft,
|
|
184
|
+
"merged": claim.merged,
|
|
185
|
+
"state_reason": claim.state_reason,
|
|
186
|
+
"closed_by": claim.closed_by,
|
|
187
|
+
"review_decision": claim.review_decision,
|
|
188
|
+
"age": claim.age,
|
|
189
|
+
"date": render_stamp(claim.created_at),
|
|
190
|
+
"relationship": claim.relationship,
|
|
191
|
+
}
|
|
192
|
+
for claim in view.claims
|
|
193
|
+
],
|
|
194
|
+
"claims_returned": len(view.claims),
|
|
195
|
+
"claims_total": view.total,
|
|
196
|
+
"links_indexed": view.links_indexed,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _outline_json(hit: Hit) -> dict[str, Any]:
|
|
201
|
+
"""One outline row. Deliberately not :func:`hit_json`: an outline line carries what is
|
|
202
|
+
needed to *choose* a comment and nothing that would let it be quoted -- no score, no
|
|
203
|
+
breakdown, no repeated repo and number, and a snippet cut at
|
|
204
|
+
:data:`~relore.search.queries.OUTLINE_CHARS`. The whole value of the view is that it
|
|
205
|
+
is cheap enough to serve complete."""
|
|
206
|
+
return {
|
|
207
|
+
"id": hit.source_id,
|
|
208
|
+
"source_type": hit.source_type,
|
|
209
|
+
"author": hit.author,
|
|
210
|
+
"trust": hit.trust,
|
|
211
|
+
"age": hit.age,
|
|
212
|
+
"date": render_stamp(hit.created_at),
|
|
213
|
+
"url": hit.url,
|
|
214
|
+
"passages": hit.passages,
|
|
215
|
+
"snippet": hit.snippet,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def why_json(view: WhyView, *, blame: Any) -> dict[str, Any]:
|
|
220
|
+
"""``GET /api/v1/why`` -- the commit, the pull request, and what was said on the line.
|
|
221
|
+
|
|
222
|
+
``number: null`` and an empty ``anchored`` are different answers: no pull request could
|
|
223
|
+
be resolved for the commit, versus one was and nobody reviewed this line.
|
|
224
|
+
"""
|
|
225
|
+
return {
|
|
226
|
+
"repo": view.repo,
|
|
227
|
+
"path": view.path,
|
|
228
|
+
"line": view.line,
|
|
229
|
+
"blame": {
|
|
230
|
+
"sha": blame.sha,
|
|
231
|
+
"author": blame.author,
|
|
232
|
+
"summary": blame.summary,
|
|
233
|
+
"text": blame.text,
|
|
234
|
+
},
|
|
235
|
+
"number": view.number,
|
|
236
|
+
"resolved_by": view.resolved_by,
|
|
237
|
+
"thread": thread_json(view.thread) if view.thread else None,
|
|
238
|
+
"anchored": [dict(comment) for comment in view.anchored],
|
|
239
|
+
# The widening, and what it reached (relore#63, #64). Separate keys rather than one
|
|
240
|
+
# merged list: a comment forty lines away and an approval on the pull request are
|
|
241
|
+
# both evidence and are not the same claim, and a caller merging them cannot say
|
|
242
|
+
# which it read.
|
|
243
|
+
"on_file": [dict(comment) for comment in view.on_file],
|
|
244
|
+
"reviews": [dict(review) for review in view.reviews],
|
|
245
|
+
"level": view.level,
|
|
246
|
+
"history": [dict(commit) for commit in view.history],
|
|
247
|
+
# Separate from `history` and never merged into it: one follows the line, the other
|
|
248
|
+
# follows a word, and a caller that cannot tell them apart cannot tell "this line
|
|
249
|
+
# was reformatted eight times" from "this behaviour was argued here".
|
|
250
|
+
"origin": [dict(commit) for commit in view.origin],
|
|
251
|
+
"origin_term": view.origin_term,
|
|
252
|
+
"origin_considered": [
|
|
253
|
+
{"term": term, "commits": count} for term, count in view.origin_considered
|
|
254
|
+
],
|
|
255
|
+
# Why the chain is empty, when it is. `comment` | `no-candidate` | `unreadable`,
|
|
256
|
+
# and they are three different answers (huggingface/relore#71).
|
|
257
|
+
"origin_declined": view.origin_declined,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _stamp(moment: dt.datetime | None) -> str | None:
|
|
262
|
+
"""A freshness mark a reader can compare with a GitHub timestamp, to the second.
|
|
263
|
+
|
|
264
|
+
One function for every timestamp this API serves, so a caller reading ``indexed_at``
|
|
265
|
+
next to a comment's ``date`` never has to parse two formats
|
|
266
|
+
(:func:`~relore.search.queries.render_stamp`).
|
|
267
|
+
"""
|
|
268
|
+
return render_stamp(moment)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def thread_json(view: ThreadView, *, compact: bool = False) -> dict[str, Any]:
|
|
272
|
+
return {
|
|
273
|
+
"repo": view.repo,
|
|
274
|
+
"number": view.number,
|
|
275
|
+
"type": view.thread_type,
|
|
276
|
+
"title": view.title,
|
|
277
|
+
"url": view.url,
|
|
278
|
+
"author": view.author,
|
|
279
|
+
"state": view.state,
|
|
280
|
+
"age": view.age,
|
|
281
|
+
"date": render_stamp(view.created_at),
|
|
282
|
+
"labels": list(view.labels),
|
|
283
|
+
# Events, not prose (issue #22).
|
|
284
|
+
"state_reason": view.state_reason,
|
|
285
|
+
"closed_by": view.closed_by,
|
|
286
|
+
"merged": view.merged,
|
|
287
|
+
"review_decision": view.review_decision,
|
|
288
|
+
"review_decision_by": list(view.review_decision_by),
|
|
289
|
+
"requested_reviewers": list(view.requested_reviewers),
|
|
290
|
+
"body": view.body,
|
|
291
|
+
# A cap a caller cannot see is a cap a caller reads as the whole document. `full`
|
|
292
|
+
# serves the rest; these two say whether there is a rest.
|
|
293
|
+
"body_chars": view.body_chars,
|
|
294
|
+
"body_truncated": view.body_truncated,
|
|
295
|
+
# Two keys, never one. Presence in `files_changed` says the thread changed the
|
|
296
|
+
# path; presence in `files_mentioned` says somebody typed it, which is not the
|
|
297
|
+
# same claim and was indistinguishable while both shared an array
|
|
298
|
+
# (huggingface/relore#17).
|
|
299
|
+
"files_changed": list(view.files_changed),
|
|
300
|
+
# Also the diff, and also not part of the collected page: an inline review comment
|
|
301
|
+
# can only hang on a changed file, so this recovers paths past the 100-row cap.
|
|
302
|
+
"files_anchored": list(view.files_anchored),
|
|
303
|
+
"files_mentioned": list(view.files_mentioned),
|
|
304
|
+
# The denominator of the changed-file list, and `len(files_changed)` is its
|
|
305
|
+
# numerator -- they are the same quantity, which the old bare count was not. A
|
|
306
|
+
# truncated list must never be able to answer a membership question:
|
|
307
|
+
# `files_collected < files_total` means a path that is absent may still have been
|
|
308
|
+
# touched.
|
|
309
|
+
"files_total": view.files_total,
|
|
310
|
+
"files_collected": view.files_collected,
|
|
311
|
+
"links": [dict(link) for link in view.links],
|
|
312
|
+
# Named so the cap is visible in the response rather than inferred from a short
|
|
313
|
+
# list: a caller that cannot tell truncation from a quiet thread will read ten
|
|
314
|
+
# comments as the whole argument.
|
|
315
|
+
"comments": [hit_json(hit, compact=compact) for hit in view.comments],
|
|
316
|
+
"comments_returned": len(view.comments),
|
|
317
|
+
# Comments, not documents: a long comment is several documents and counting rows
|
|
318
|
+
# called one comment two (huggingface/relore#18). Every comment the thread has,
|
|
319
|
+
# including the ones the trust floor withheld -- `comments_total` minus
|
|
320
|
+
# `comments_machine_suppressed` is what the cap and the sampling compose with, and
|
|
321
|
+
# a total computed post-filter said `0 of 0` about a thread we hold a comment for
|
|
322
|
+
# (huggingface/relore#28).
|
|
323
|
+
"comments_total": view.total_documents + view.machine_suppressed,
|
|
324
|
+
# The gap, named rather than left to be inferred from a short list. Machine
|
|
325
|
+
# authors are excluded and not down-weighted (section 6.2), so this is a filter a
|
|
326
|
+
# reader has to know fired before concluding a thread is quiet.
|
|
327
|
+
"comments_machine_suppressed": view.machine_suppressed,
|
|
328
|
+
# When this response's thread was last rebuilt from GitHub. Per document, because
|
|
329
|
+
# `status`'s per-source high-water rows cannot be composed into an answer about
|
|
330
|
+
# one thread and two field runs composed them wrongly in opposite directions
|
|
331
|
+
# (huggingface/relore#32). A string, not a datetime: `render` takes plain JSON
|
|
332
|
+
# shapes and must read the same on both sides of the wire.
|
|
333
|
+
"indexed_at": _stamp(view.indexed_at),
|
|
334
|
+
"indexed_at_note": THREAD_STAMP_NOTE,
|
|
335
|
+
# HOW those comments were chosen. A positional sample and a ranked top ten look
|
|
336
|
+
# identical on the page, and the first five and last five of a 97-comment thread
|
|
337
|
+
# were read as its ten best (huggingface/relore#16).
|
|
338
|
+
"selection": view.selection,
|
|
339
|
+
# A focus orders the comments and never selects them, so the count that matters is
|
|
340
|
+
# how many carried every term: zero next to ten returned comments says "your
|
|
341
|
+
# question matched nothing, this is the thread in order" rather than "nothing here".
|
|
342
|
+
"focus": view.focus,
|
|
343
|
+
"focus_matched": view.focus_matched,
|
|
344
|
+
# One line per comment for the whole thread (relore#70). Present and empty unless
|
|
345
|
+
# `--outline` asked, so a caller can tell "not requested" from "nothing to show".
|
|
346
|
+
"outline": [_outline_json(c) for c in view.outline],
|
|
347
|
+
"outline_returned": len(view.outline),
|
|
348
|
+
# An outline narrowed by a focus is complete *of the comments carrying a word*,
|
|
349
|
+
# which is a different completeness from the unnarrowed one and has to be
|
|
350
|
+
# distinguishable without reading the rows (huggingface/relore#71). `widened` is
|
|
351
|
+
# the case where the focus carried nothing and the whole outline came back
|
|
352
|
+
# instead, so a caller never has to infer that from a count that did not shrink.
|
|
353
|
+
"outline_matched": view.outline_matched,
|
|
354
|
+
"outline_widened": view.outline_widened,
|
|
355
|
+
# What `--comment` asked for and what became of it. `absent` and `suppressed` are
|
|
356
|
+
# both "no comment came back" and are opposite next actions, so the page says which
|
|
357
|
+
# rather than leaving it to be inferred from an empty list (huggingface/relore#71).
|
|
358
|
+
"comment": view.comment,
|
|
359
|
+
"comment_status": view.comment_status,
|
|
360
|
+
# Where a sweep resumed. A page that silently begins in the middle is worse than
|
|
361
|
+
# one that stops, so the cursor travels with the page that honoured it.
|
|
362
|
+
"after": view.after,
|
|
363
|
+
}
|