@getrift/rift 0.1.0-beta.12 → 0.1.0-beta.14
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.
- package/README.md +35 -9
- package/dist/src/cli/commands/doctor.d.ts +6 -0
- package/dist/src/cli/commands/doctor.d.ts.map +1 -0
- package/dist/src/cli/commands/doctor.js +183 -0
- package/dist/src/cli/commands/doctor.js.map +1 -0
- package/dist/src/cli/commands/menubar.d.ts +30 -0
- package/dist/src/cli/commands/menubar.d.ts.map +1 -0
- package/dist/src/cli/commands/menubar.js +180 -0
- package/dist/src/cli/commands/menubar.js.map +1 -0
- package/dist/src/cli/commands/onboard.d.ts +38 -0
- package/dist/src/cli/commands/onboard.d.ts.map +1 -1
- package/dist/src/cli/commands/onboard.js +203 -121
- package/dist/src/cli/commands/onboard.js.map +1 -1
- package/dist/src/cli/commands/status.d.ts +9 -7
- package/dist/src/cli/commands/status.d.ts.map +1 -1
- package/dist/src/cli/commands/status.js +29 -10
- package/dist/src/cli/commands/status.js.map +1 -1
- package/dist/src/cli/commands/update.d.ts +3 -0
- package/dist/src/cli/commands/update.d.ts.map +1 -1
- package/dist/src/cli/commands/update.js +19 -0
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +4 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/postinstall-menubar.d.ts +22 -0
- package/dist/src/cli/postinstall-menubar.d.ts.map +1 -0
- package/dist/src/cli/postinstall-menubar.js +39 -0
- package/dist/src/cli/postinstall-menubar.js.map +1 -0
- package/dist/src/cli/status/friend-header.d.ts +8 -1
- package/dist/src/cli/status/friend-header.d.ts.map +1 -1
- package/dist/src/cli/status/friend-header.js +93 -12
- package/dist/src/cli/status/friend-header.js.map +1 -1
- package/dist/src/cli/ui.d.ts +47 -0
- package/dist/src/cli/ui.d.ts.map +1 -0
- package/dist/src/cli/ui.js +166 -0
- package/dist/src/cli/ui.js.map +1 -0
- package/dist/src/diagnostics/doctor.d.ts +106 -0
- package/dist/src/diagnostics/doctor.d.ts.map +1 -0
- package/dist/src/diagnostics/doctor.js +251 -0
- package/dist/src/diagnostics/doctor.js.map +1 -0
- package/dist/src/diagnostics/notify.d.ts +90 -0
- package/dist/src/diagnostics/notify.d.ts.map +1 -0
- package/dist/src/diagnostics/notify.js +177 -0
- package/dist/src/diagnostics/notify.js.map +1 -0
- package/dist/src/diagnostics/repair-prompt.d.ts +49 -0
- package/dist/src/diagnostics/repair-prompt.d.ts.map +1 -0
- package/dist/src/diagnostics/repair-prompt.js +198 -0
- package/dist/src/diagnostics/repair-prompt.js.map +1 -0
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts +25 -2
- package/dist/src/jobs/handlers/dedupe-conversations.d.ts.map +1 -1
- package/dist/src/jobs/handlers/dedupe-conversations.js +48 -9
- package/dist/src/jobs/handlers/dedupe-conversations.js.map +1 -1
- package/dist/src/jobs/handlers/ingest.d.ts.map +1 -1
- package/dist/src/jobs/handlers/ingest.js +8 -2
- package/dist/src/jobs/handlers/ingest.js.map +1 -1
- package/dist/src/main.js +43 -4
- package/dist/src/main.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +43 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/context-pack.js +163 -25
- package/dist/src/mcp/tools/context-pack.js.map +1 -1
- package/dist/src/observability/onboarding-metric.d.ts +115 -0
- package/dist/src/observability/onboarding-metric.d.ts.map +1 -0
- package/dist/src/observability/onboarding-metric.js +344 -0
- package/dist/src/observability/onboarding-metric.js.map +1 -0
- package/dist/src/observability/version-check.d.ts +1 -0
- package/dist/src/observability/version-check.d.ts.map +1 -1
- package/dist/src/observability/version-check.js +2 -1
- package/dist/src/observability/version-check.js.map +1 -1
- package/dist/src/retrieval/context-pack.d.ts +37 -0
- package/dist/src/retrieval/context-pack.d.ts.map +1 -1
- package/dist/src/retrieval/context-pack.js +165 -1
- package/dist/src/retrieval/context-pack.js.map +1 -1
- package/dist/src/retrieval/current-truth.d.ts +326 -0
- package/dist/src/retrieval/current-truth.d.ts.map +1 -0
- package/dist/src/retrieval/current-truth.js +747 -0
- package/dist/src/retrieval/current-truth.js.map +1 -0
- package/dist/src/retrieval/git-state.d.ts +53 -0
- package/dist/src/retrieval/git-state.d.ts.map +1 -0
- package/dist/src/retrieval/git-state.js +174 -0
- package/dist/src/retrieval/git-state.js.map +1 -0
- package/dist/src/server/routes/friend-status.d.ts +63 -0
- package/dist/src/server/routes/friend-status.d.ts.map +1 -1
- package/dist/src/server/routes/friend-status.js +97 -0
- package/dist/src/server/routes/friend-status.js.map +1 -1
- package/operator/swiftbar/render-menu.py +444 -0
- package/operator/swiftbar/rift.10s.sh +147 -0
- package/package.json +4 -1
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phrase triggers for `reasoning_archive`. Multi-word — these must be
|
|
3
|
+
* matched in word order against the lower-cased query. A reasoning
|
|
4
|
+
* question is almost always phrased with possessive ("my take") or
|
|
5
|
+
* historical-tense framing ("why did I", "how did I frame") — single-
|
|
6
|
+
* keyword matching would catch too many false positives.
|
|
7
|
+
*/
|
|
8
|
+
const REASONING_PHRASE_TRIGGERS = [
|
|
9
|
+
"why did i",
|
|
10
|
+
"why did we",
|
|
11
|
+
"what did i decide",
|
|
12
|
+
"what did we decide",
|
|
13
|
+
"how did i frame",
|
|
14
|
+
"how did we frame",
|
|
15
|
+
"my take",
|
|
16
|
+
"my view",
|
|
17
|
+
"my philosophy",
|
|
18
|
+
"relationship between",
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Single-word triggers for `reasoning_archive`. These score the query as
|
|
22
|
+
* a taste/philosophy/origin question — old conversations are the right
|
|
23
|
+
* evidence, not trackers.
|
|
24
|
+
*/
|
|
25
|
+
const REASONING_WORD_TRIGGERS = [
|
|
26
|
+
"favorite",
|
|
27
|
+
"favorites",
|
|
28
|
+
"favourite",
|
|
29
|
+
"favourites",
|
|
30
|
+
"taste",
|
|
31
|
+
"philosophy",
|
|
32
|
+
"origin",
|
|
33
|
+
"lineage",
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Phrase triggers for `current_truth`. Indicates the asker wants the
|
|
37
|
+
* present-tense state, not historical reasoning.
|
|
38
|
+
*/
|
|
39
|
+
const CURRENT_PHRASE_TRIGGERS = [
|
|
40
|
+
"what's next",
|
|
41
|
+
"whats next",
|
|
42
|
+
"what is next",
|
|
43
|
+
"install path",
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Single-word triggers for `current_truth`. Strong signal that the
|
|
47
|
+
* answer should lead with trackers / source / recent state.
|
|
48
|
+
*
|
|
49
|
+
* `customers`/`clients`/`roster`/`freelancer` cover roster-shape
|
|
50
|
+
* questions (Q4: "who are Clément's customers as a freelancer") —
|
|
51
|
+
* roster questions are present-tense state, not historical reasoning,
|
|
52
|
+
* so they belong here even though they don't carry an explicit
|
|
53
|
+
* "current" / "today" marker.
|
|
54
|
+
*/
|
|
55
|
+
const CURRENT_WORD_TRIGGERS = [
|
|
56
|
+
"current",
|
|
57
|
+
"latest",
|
|
58
|
+
"today",
|
|
59
|
+
"now",
|
|
60
|
+
"state",
|
|
61
|
+
"status",
|
|
62
|
+
"next",
|
|
63
|
+
"roadmap",
|
|
64
|
+
"launch",
|
|
65
|
+
"pending",
|
|
66
|
+
"shipped",
|
|
67
|
+
"version",
|
|
68
|
+
"customers",
|
|
69
|
+
"customer",
|
|
70
|
+
"clients",
|
|
71
|
+
"roster",
|
|
72
|
+
"freelancer",
|
|
73
|
+
"freelance",
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Classify a query's evidence intent. Deterministic and transparent so
|
|
77
|
+
* v0 can be debugged by eye. Algorithm:
|
|
78
|
+
*
|
|
79
|
+
* 1. Lower-case the query.
|
|
80
|
+
* 2. Check phrase triggers (multi-word) for each intent.
|
|
81
|
+
* 3. Check single-word triggers (matched as whole words via word
|
|
82
|
+
* boundaries — no partial matches like "currently" → "current").
|
|
83
|
+
* 4. If exactly one intent matches → that intent.
|
|
84
|
+
* 5. If both match → return `blended` (the asker is conflating
|
|
85
|
+
* "what's my taste on the current X").
|
|
86
|
+
* 6. If neither matches → `blended`.
|
|
87
|
+
*
|
|
88
|
+
* Note on Q7 ("difference between second brain and Rift"): only
|
|
89
|
+
* `relationship between` is in the reasoning-archive trigger list, not
|
|
90
|
+
* `difference between`. Q7 fires no trigger and falls to `blended`,
|
|
91
|
+
* which is the deliberate v0 outcome — disambiguation questions surface
|
|
92
|
+
* both the current product framing and the historical context without
|
|
93
|
+
* forcing an order.
|
|
94
|
+
*/
|
|
95
|
+
export function classifyEvidenceIntent(query) {
|
|
96
|
+
const q = (query || "").toLowerCase();
|
|
97
|
+
if (!q.trim())
|
|
98
|
+
return "blended";
|
|
99
|
+
const reasoning = hasAnyPhrase(q, REASONING_PHRASE_TRIGGERS) ||
|
|
100
|
+
hasAnyWord(q, REASONING_WORD_TRIGGERS);
|
|
101
|
+
const current = hasAnyPhrase(q, CURRENT_PHRASE_TRIGGERS) ||
|
|
102
|
+
hasAnyWord(q, CURRENT_WORD_TRIGGERS);
|
|
103
|
+
if (current && !reasoning)
|
|
104
|
+
return "current_truth";
|
|
105
|
+
if (reasoning && !current)
|
|
106
|
+
return "reasoning_archive";
|
|
107
|
+
return "blended";
|
|
108
|
+
}
|
|
109
|
+
const CURRENT_STATE_CLAIM_WORDS = [
|
|
110
|
+
"current",
|
|
111
|
+
"latest",
|
|
112
|
+
"today",
|
|
113
|
+
"now",
|
|
114
|
+
"state",
|
|
115
|
+
"status",
|
|
116
|
+
"next",
|
|
117
|
+
"roadmap",
|
|
118
|
+
"launch",
|
|
119
|
+
"pending",
|
|
120
|
+
"shipped",
|
|
121
|
+
"version",
|
|
122
|
+
"install",
|
|
123
|
+
"onboarding",
|
|
124
|
+
"customer",
|
|
125
|
+
"customers",
|
|
126
|
+
"clients",
|
|
127
|
+
"roster",
|
|
128
|
+
];
|
|
129
|
+
const FRAMING_CLAIM_WORDS = [
|
|
130
|
+
"pricing",
|
|
131
|
+
"price",
|
|
132
|
+
"brand",
|
|
133
|
+
"framing",
|
|
134
|
+
"history",
|
|
135
|
+
"positioning",
|
|
136
|
+
"narrative",
|
|
137
|
+
"messaging",
|
|
138
|
+
];
|
|
139
|
+
/**
|
|
140
|
+
* Unambiguous rationale signals — always a reasoning claim. "decision" /
|
|
141
|
+
* "decide" are deliberately NOT here: they're ambiguous (see
|
|
142
|
+
* {@link DECISION_CLAIM_WORDS}).
|
|
143
|
+
*/
|
|
144
|
+
const REASONING_CLAIM_WORDS = [
|
|
145
|
+
"why",
|
|
146
|
+
"taste",
|
|
147
|
+
"relationship",
|
|
148
|
+
"philosophy",
|
|
149
|
+
"favorite",
|
|
150
|
+
"favourite",
|
|
151
|
+
"origin",
|
|
152
|
+
"rationale",
|
|
153
|
+
];
|
|
154
|
+
/**
|
|
155
|
+
* Rationale-shaped phrases — "why/how did we decide", "reasoning behind".
|
|
156
|
+
* These pin a decision query to reasoning regardless of qualifiers.
|
|
157
|
+
*/
|
|
158
|
+
const REASONING_CLAIM_PHRASES = [
|
|
159
|
+
"why did",
|
|
160
|
+
"why we",
|
|
161
|
+
"how did we decide",
|
|
162
|
+
"how we decided",
|
|
163
|
+
"reasoning behind",
|
|
164
|
+
"rationale behind",
|
|
165
|
+
"thinking behind",
|
|
166
|
+
];
|
|
167
|
+
/**
|
|
168
|
+
* Ambiguous decision words. "What's the latest decision about X" is a
|
|
169
|
+
* current-state question (what the decision *is*); "why did we decide X"
|
|
170
|
+
* is reasoning (caught by {@link REASONING_CLAIM_PHRASES}). So a bare
|
|
171
|
+
* decision word is reasoning ONLY when no current-state qualifier is
|
|
172
|
+
* present — otherwise the qualifier wins.
|
|
173
|
+
*/
|
|
174
|
+
const DECISION_CLAIM_WORDS = ["decision", "decide", "decided"];
|
|
175
|
+
/**
|
|
176
|
+
* Classify the claim-type of a query. Precedence: rationale phrases →
|
|
177
|
+
* strong reasoning words → ambiguous decision-without-current-qualifier →
|
|
178
|
+
* current_state → framing → default current_state. A "why did we decide X"
|
|
179
|
+
* question is reasoning even if it mentions a current-state noun; "latest
|
|
180
|
+
* decision about X" is a current-state question (P2 fix). An ambiguous
|
|
181
|
+
* query defaults to the strictest tier discipline (current_state) so we
|
|
182
|
+
* never under-apply the trust gating.
|
|
183
|
+
*/
|
|
184
|
+
export function classifyClaimType(query) {
|
|
185
|
+
const q = (query || "").toLowerCase();
|
|
186
|
+
if (!q.trim())
|
|
187
|
+
return "current_state";
|
|
188
|
+
if (hasAnyPhrase(q, REASONING_CLAIM_PHRASES))
|
|
189
|
+
return "reasoning";
|
|
190
|
+
if (hasAnyWord(q, REASONING_CLAIM_WORDS))
|
|
191
|
+
return "reasoning";
|
|
192
|
+
const hasCurrentQualifier = hasAnyWord(q, CURRENT_STATE_CLAIM_WORDS);
|
|
193
|
+
if (hasAnyWord(q, DECISION_CLAIM_WORDS) && !hasCurrentQualifier) {
|
|
194
|
+
return "reasoning";
|
|
195
|
+
}
|
|
196
|
+
if (hasCurrentQualifier)
|
|
197
|
+
return "current_state";
|
|
198
|
+
if (hasAnyWord(q, FRAMING_CLAIM_WORDS))
|
|
199
|
+
return "framing";
|
|
200
|
+
return "current_state";
|
|
201
|
+
}
|
|
202
|
+
function hasAnyPhrase(q, phrases) {
|
|
203
|
+
return phrases.some((p) => q.includes(p));
|
|
204
|
+
}
|
|
205
|
+
function hasAnyWord(q, words) {
|
|
206
|
+
for (const w of words) {
|
|
207
|
+
const rx = new RegExp(`\\b${escapeRx(w)}\\b`, "i");
|
|
208
|
+
if (rx.test(q))
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
function escapeRx(s) {
|
|
214
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
215
|
+
}
|
|
216
|
+
// ─── Freshness / canonicalness classification ───────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* Default freshness cutoff: a hit indexed within `RECENT_WINDOW_DAYS` of
|
|
219
|
+
* "now" counts as recent. 21 days is wide enough that a paused-but-
|
|
220
|
+
* active project's tracker still reads as current, and narrow enough
|
|
221
|
+
* that an April-26 LAUNCH.md (now ~3 weeks old by 2026-05-19) starts
|
|
222
|
+
* looking stale relative to a 2026-05-19 PROJECT_STATE.md edit.
|
|
223
|
+
*/
|
|
224
|
+
export const RECENT_WINDOW_DAYS = 21;
|
|
225
|
+
/**
|
|
226
|
+
* Source-path patterns that mark a hit as a current-truth-shaped
|
|
227
|
+
* artifact — the kind of file you'd open by hand to answer "what's the
|
|
228
|
+
* state right now". Tracker files at the project root, source code,
|
|
229
|
+
* install scripts, and the package manifest all qualify. Matched on
|
|
230
|
+
* basename for the manifests/READMEs (so per-project copies count) and
|
|
231
|
+
* on path segments for src/scripts.
|
|
232
|
+
*/
|
|
233
|
+
const TRACKER_PATH_PATTERNS = [
|
|
234
|
+
/(^|\/)PROJECT_STATE\.md$/i,
|
|
235
|
+
/(^|\/)TODO\.md$/i,
|
|
236
|
+
/(^|\/)README\.md$/i,
|
|
237
|
+
/(^|\/)README\.dev\.md$/i,
|
|
238
|
+
/(^|\/)CLAUDE\.md$/i,
|
|
239
|
+
/(^|\/)AGENTS\.md$/i,
|
|
240
|
+
/(^|\/)package\.json$/i,
|
|
241
|
+
/\/src\//,
|
|
242
|
+
/\/scripts\//,
|
|
243
|
+
/(^|\/)config\.json$/i,
|
|
244
|
+
];
|
|
245
|
+
/**
|
|
246
|
+
* Source-path patterns that mark a hit as a *snapshot* — useful as
|
|
247
|
+
* historical context but not as current truth. Reports/insights are the
|
|
248
|
+
* obvious case (Opus overnight passes), plus filename markers like
|
|
249
|
+
* `-snapshot-`, `-overnight-`, and `-pass-N` that consistently denote
|
|
250
|
+
* frozen syntheses.
|
|
251
|
+
*
|
|
252
|
+
* `LAUNCH.md` is intentionally NOT in this list as of v1: the git-state
|
|
253
|
+
* probe ({@link GitState}) is the general signal — an untracked file is
|
|
254
|
+
* discounted regardless of basename, and removing the hardcoded entry
|
|
255
|
+
* means we don't mis-discount a future `LAUNCH.md` that does get
|
|
256
|
+
* committed to a project as durable launch state.
|
|
257
|
+
*/
|
|
258
|
+
const SNAPSHOT_PATH_PATTERNS = [
|
|
259
|
+
/\/reports\/insights\//i,
|
|
260
|
+
/\/reports\/backfill-audit\//i,
|
|
261
|
+
/-snapshot-/i,
|
|
262
|
+
/-overnight-/i,
|
|
263
|
+
/-pass-\d+/i,
|
|
264
|
+
];
|
|
265
|
+
export function isTrackerPath(p) {
|
|
266
|
+
return typeof p === "string" && TRACKER_PATH_PATTERNS.some((rx) => rx.test(p));
|
|
267
|
+
}
|
|
268
|
+
export function isSnapshotPath(p) {
|
|
269
|
+
return typeof p === "string" && SNAPSHOT_PATH_PATTERNS.some((rx) => rx.test(p));
|
|
270
|
+
}
|
|
271
|
+
/** Live/executable state: source, scripts, manifests, config. */
|
|
272
|
+
const LIVE_STATE_PATH_PATTERNS = [
|
|
273
|
+
/\/src\//,
|
|
274
|
+
/\/scripts\//,
|
|
275
|
+
/(^|\/)package\.json$/i,
|
|
276
|
+
/(^|\/)config\.json$/i,
|
|
277
|
+
/(^|\/)install\.sh$/i,
|
|
278
|
+
];
|
|
279
|
+
/** Intentional state trackers. */
|
|
280
|
+
const TRACKER_FILE_PATTERNS = [
|
|
281
|
+
/(^|\/)PROJECT_STATE\.md$/i,
|
|
282
|
+
/(^|\/)TODO\.md$/i,
|
|
283
|
+
];
|
|
284
|
+
/** Durable committed framing docs. */
|
|
285
|
+
const COMMITTED_DOC_PATTERNS = [
|
|
286
|
+
/(^|\/)README(\.[a-z]+)?\.md$/i,
|
|
287
|
+
/(^|\/)CLAUDE\.md$/i,
|
|
288
|
+
/(^|\/)AGENTS\.md$/i,
|
|
289
|
+
/(^|\/)PRD\.md$/i,
|
|
290
|
+
/(^|\/)PLAN\.md$/i,
|
|
291
|
+
];
|
|
292
|
+
function matchesAny(p, patterns) {
|
|
293
|
+
return patterns.some((rx) => rx.test(p));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Assign a {@link TrustTier} to one evidence input. Pure, structural,
|
|
297
|
+
* claim-type-independent: git-state is the strongest signal (an untracked
|
|
298
|
+
* or deleted document is `discounted` no matter how it's named — the v1
|
|
299
|
+
* generalization of the LAUNCH.md hack), then snapshot paths, then the
|
|
300
|
+
* path-pattern ladder. Conversations are always `discussion`.
|
|
301
|
+
*
|
|
302
|
+
* Defaults to `committed_doc` for a tracked/unknown document that matches
|
|
303
|
+
* none of the specific ladders — it's a durable file we just can't place
|
|
304
|
+
* more precisely, which is a safer default than claiming it's live state.
|
|
305
|
+
*/
|
|
306
|
+
export function assignTrustTier(kind, gitState, sourcePath) {
|
|
307
|
+
if (kind === "conversation")
|
|
308
|
+
return "discussion";
|
|
309
|
+
if (gitState === "untracked" || gitState === "deleted")
|
|
310
|
+
return "discounted";
|
|
311
|
+
if (isSnapshotPath(sourcePath))
|
|
312
|
+
return "discounted";
|
|
313
|
+
const p = typeof sourcePath === "string" ? sourcePath : "";
|
|
314
|
+
if (p && matchesAny(p, LIVE_STATE_PATH_PATTERNS))
|
|
315
|
+
return "live_state";
|
|
316
|
+
if (p && matchesAny(p, TRACKER_FILE_PATTERNS))
|
|
317
|
+
return "tracker";
|
|
318
|
+
if (p && matchesAny(p, COMMITTED_DOC_PATTERNS))
|
|
319
|
+
return "committed_doc";
|
|
320
|
+
return "committed_doc";
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Parse a row's `indexed_at` and return whether it falls within the
|
|
324
|
+
* recency window. Robust to missing / unparseable timestamps — we'd
|
|
325
|
+
* rather call a row "unknown-age" (treated as not-recent) than blow up
|
|
326
|
+
* the answer packaging.
|
|
327
|
+
*/
|
|
328
|
+
export function isRecent(indexedAt, now = new Date(), windowDays = RECENT_WINDOW_DAYS) {
|
|
329
|
+
if (!indexedAt)
|
|
330
|
+
return false;
|
|
331
|
+
const t = Date.parse(indexedAt);
|
|
332
|
+
if (Number.isNaN(t))
|
|
333
|
+
return false;
|
|
334
|
+
const ageMs = now.getTime() - t;
|
|
335
|
+
return ageMs <= windowDays * 24 * 60 * 60 * 1000;
|
|
336
|
+
}
|
|
337
|
+
/** Hard rule 3 caveat string — single source so renderer + DTO agree. */
|
|
338
|
+
export const TRACKER_ONLY_CAVEAT = "tracker-backed, not live-verified";
|
|
339
|
+
/**
|
|
340
|
+
* Snippet cap for evidence cards. Mirrors `SNIPPET_MAX` in context-pack,
|
|
341
|
+
* intentionally duplicated here to keep this module decoupled.
|
|
342
|
+
*/
|
|
343
|
+
const EVIDENCE_SNIPPET_MAX = 200;
|
|
344
|
+
export function partitionEvidence(inputs, opts) {
|
|
345
|
+
const cap = opts.maxPerSection;
|
|
346
|
+
const now = opts.now ?? new Date();
|
|
347
|
+
const out = {
|
|
348
|
+
current_truth: [],
|
|
349
|
+
past_reasoning: [],
|
|
350
|
+
older_memory: [],
|
|
351
|
+
discounted: [],
|
|
352
|
+
recommended_live_files: [],
|
|
353
|
+
conflicts: [],
|
|
354
|
+
};
|
|
355
|
+
// Track tracker-paths we encounter so the recommended_live_files fallback
|
|
356
|
+
// can prefer paths the index already knows about. Value is the
|
|
357
|
+
// pre-probed git state (if any) so the recommendation card can echo
|
|
358
|
+
// it back to the asker (e.g. "newer on disk than indexed copy").
|
|
359
|
+
const seenTrackerPaths = new Map();
|
|
360
|
+
// Tracker paths surfaced in hits with a live git state — eligible for
|
|
361
|
+
// promotion to `recommended_live_files`. We exclude `untracked` and
|
|
362
|
+
// `deleted` here: recommending a file the asker can't open or that
|
|
363
|
+
// isn't under version control would defeat the point.
|
|
364
|
+
const recommendableGitStates = new Set([
|
|
365
|
+
"tracked",
|
|
366
|
+
"modified",
|
|
367
|
+
"newer_on_disk",
|
|
368
|
+
undefined,
|
|
369
|
+
]);
|
|
370
|
+
// Basenames of paths the partitioner discounted as untracked or
|
|
371
|
+
// deleted. The canonical fallback list is hand-written basenames
|
|
372
|
+
// (`PROJECT_STATE.md`, `TODO.md`, `package.json`) and was probed
|
|
373
|
+
// alongside the rest of the hits — so if any of those basenames
|
|
374
|
+
// were already discounted, the recommendation must drop them
|
|
375
|
+
// rather than re-suggest them. Tracked via basename because the
|
|
376
|
+
// canonical fallback doesn't have absolute paths to match against.
|
|
377
|
+
const discountedBasenames = new Set();
|
|
378
|
+
for (const input of inputs) {
|
|
379
|
+
const { row, kind, gitState } = input;
|
|
380
|
+
const sourcePath = row?.["source_path"] || undefined;
|
|
381
|
+
const indexedAt = row?.["indexed_at"] || undefined;
|
|
382
|
+
const recent = isRecent(indexedAt, now);
|
|
383
|
+
const tracker = isTrackerPath(sourcePath);
|
|
384
|
+
const snapshot = isSnapshotPath(sourcePath);
|
|
385
|
+
const card = buildEvidenceCard(input);
|
|
386
|
+
if (!card)
|
|
387
|
+
continue;
|
|
388
|
+
if (tracker &&
|
|
389
|
+
sourcePath &&
|
|
390
|
+
recommendableGitStates.has(gitState)) {
|
|
391
|
+
seenTrackerPaths.set(sourcePath, gitState);
|
|
392
|
+
}
|
|
393
|
+
if (opts.intent === "reasoning_archive") {
|
|
394
|
+
if (kind === "conversation") {
|
|
395
|
+
pushIfRoom(out.past_reasoning, card, cap);
|
|
396
|
+
}
|
|
397
|
+
else if (tracker) {
|
|
398
|
+
pushIfRoom(out.current_truth, card, cap);
|
|
399
|
+
}
|
|
400
|
+
else if (snapshot) {
|
|
401
|
+
pushIfRoom(out.older_memory, card, cap);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
pushIfRoom(out.older_memory, card, cap);
|
|
405
|
+
}
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
// v1 git-state discounts. Apply ONLY to documents and ONLY in
|
|
409
|
+
// current-truth-shaped modes (current_truth + blended). Conversations
|
|
410
|
+
// don't have a meaningful git state, and reasoning_archive mode
|
|
411
|
+
// (handled above) cares about framing, not canonicalness.
|
|
412
|
+
if (kind === "document" && gitState === "deleted") {
|
|
413
|
+
pushIfRoom(out.discounted, withReason(card, "deleted from disk"), cap);
|
|
414
|
+
if (sourcePath)
|
|
415
|
+
discountedBasenames.add(basename(sourcePath));
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (kind === "document" && gitState === "untracked") {
|
|
419
|
+
pushIfRoom(out.discounted, withReason(card, "untracked in git (not committed)"), cap);
|
|
420
|
+
if (sourcePath)
|
|
421
|
+
discountedBasenames.add(basename(sourcePath));
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (opts.intent === "current_truth") {
|
|
425
|
+
// v2 hard rule 1: conversations can NEVER enter `current_truth` on
|
|
426
|
+
// recency alone. This is the load-bearing fix — it breaks the
|
|
427
|
+
// `untrusted source → recent conversation → current_truth` laundering
|
|
428
|
+
// loop (and its self-corroboration variant where a captured Rift
|
|
429
|
+
// answer re-enters as evidence). Recent conversations surface as
|
|
430
|
+
// `past_reasoning` ("recent discussion"); older ones as `older_memory`.
|
|
431
|
+
// Demote-by-default: a conversation is never sole authority here.
|
|
432
|
+
if (kind === "conversation") {
|
|
433
|
+
pushIfRoom(recent ? out.past_reasoning : out.older_memory, card, cap);
|
|
434
|
+
}
|
|
435
|
+
else if (tracker) {
|
|
436
|
+
pushIfRoom(out.current_truth, card, cap);
|
|
437
|
+
}
|
|
438
|
+
else if (snapshot) {
|
|
439
|
+
pushIfRoom(out.discounted, withReason(card, "snapshot or insight report"), cap);
|
|
440
|
+
}
|
|
441
|
+
else if (!recent) {
|
|
442
|
+
pushIfRoom(out.discounted, withReason(card, "older document"), cap);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
pushIfRoom(out.current_truth, card, cap);
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// blended
|
|
450
|
+
if (tracker) {
|
|
451
|
+
pushIfRoom(out.current_truth, card, cap);
|
|
452
|
+
}
|
|
453
|
+
else if (kind === "conversation") {
|
|
454
|
+
pushIfRoom(out.past_reasoning, card, cap);
|
|
455
|
+
}
|
|
456
|
+
else if (snapshot) {
|
|
457
|
+
pushIfRoom(out.discounted, withReason(card, "snapshot or insight report"), cap);
|
|
458
|
+
}
|
|
459
|
+
else if (recent) {
|
|
460
|
+
pushIfRoom(out.current_truth, card, cap);
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
pushIfRoom(out.older_memory, card, cap);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// v2 hard rule 5 (staleness demotion): a tracker is *not* absolute. If
|
|
467
|
+
// `current_truth` holds a `live_state`-tier card and a tracker/
|
|
468
|
+
// committed_doc card is older than that live evidence by more than the
|
|
469
|
+
// recency window, the tracker is stale relative to live activity —
|
|
470
|
+
// demote it to `older_memory` (tier label retained so the demotion is
|
|
471
|
+
// visible). Runs before the recommended-live-files check so an emptied
|
|
472
|
+
// `current_truth` correctly triggers the "read the live files" fallback.
|
|
473
|
+
if (opts.intent !== "reasoning_archive") {
|
|
474
|
+
applyStalenessDemotion(out, now, cap);
|
|
475
|
+
}
|
|
476
|
+
// Recommended live files: only for current_truth and blended, and only
|
|
477
|
+
// when `current_truth` is empty or so thin that the asker should read
|
|
478
|
+
// the live files before trusting the answer. The list is sourced from
|
|
479
|
+
// tracker paths we already saw in hits (so we're naming real, indexed
|
|
480
|
+
// files), with a small set of canonical fallbacks if none surfaced.
|
|
481
|
+
if (opts.intent !== "reasoning_archive") {
|
|
482
|
+
const dominatedByOlder = out.current_truth.length === 0 &&
|
|
483
|
+
(out.discounted.length > 0 || out.older_memory.length > 0);
|
|
484
|
+
const veryThin = out.current_truth.length === 0;
|
|
485
|
+
if (dominatedByOlder || veryThin) {
|
|
486
|
+
out.recommended_live_files = recommendLiveFiles(seenTrackerPaths, opts.projectRoot, discountedBasenames);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// v2 hard rule 3 (caveat): `current_truth` is populated but holds no
|
|
490
|
+
// `live_state`-tier evidence — the best we have is an intentional
|
|
491
|
+
// tracker, not executable/observable state. Label it honestly.
|
|
492
|
+
if (opts.intent !== "reasoning_archive" &&
|
|
493
|
+
out.current_truth.length > 0 &&
|
|
494
|
+
!out.current_truth.some((c) => c.tier === "live_state")) {
|
|
495
|
+
out.current_truth_caveat = TRACKER_ONLY_CAVEAT;
|
|
496
|
+
}
|
|
497
|
+
// v2 hard rule 4 (surface conflicts): a discounted current-state-shaped
|
|
498
|
+
// source competing with higher-tier evidence is a disagreement, not a
|
|
499
|
+
// loser to silently drop. Name the tension; do not adjudicate it.
|
|
500
|
+
if (opts.intent !== "reasoning_archive") {
|
|
501
|
+
out.conflicts = buildConflicts(out, opts.claimType ?? "current_state");
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Move stale trackers out of `current_truth` (hard rule 5). A tracker /
|
|
507
|
+
* committed_doc card older than the freshest `live_state` card by more
|
|
508
|
+
* than {@link RECENT_WINDOW_DAYS} is demoted to `older_memory`. No-op
|
|
509
|
+
* when `current_truth` has no live-state anchor to compare against —
|
|
510
|
+
* without live evidence we have no basis to call a tracker stale.
|
|
511
|
+
*
|
|
512
|
+
* P1 fix: the demotable card's age is its *self-declared* freshness
|
|
513
|
+
* (`tracker_date`, e.g. PROJECT_STATE.md's `Last touched:`) when present,
|
|
514
|
+
* not `indexed_at`. A tracker reindexed today but whose own date is weeks
|
|
515
|
+
* old must still be demotable — `indexed_at` would mask that.
|
|
516
|
+
*/
|
|
517
|
+
function applyStalenessDemotion(out, now, cap) {
|
|
518
|
+
void now;
|
|
519
|
+
const liveTimes = out.current_truth
|
|
520
|
+
.filter((c) => c.tier === "live_state" && c.timestamp)
|
|
521
|
+
.map((c) => Date.parse(c.timestamp))
|
|
522
|
+
.filter((t) => !Number.isNaN(t));
|
|
523
|
+
if (liveTimes.length === 0)
|
|
524
|
+
return;
|
|
525
|
+
const freshestLive = Math.max(...liveTimes);
|
|
526
|
+
const windowMs = RECENT_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
527
|
+
const kept = [];
|
|
528
|
+
for (const c of out.current_truth) {
|
|
529
|
+
const demotable = c.tier === "tracker" || c.tier === "committed_doc";
|
|
530
|
+
const anchor = c.tracker_date ?? c.timestamp;
|
|
531
|
+
const t = anchor ? Date.parse(anchor) : NaN;
|
|
532
|
+
if (demotable && !Number.isNaN(t) && freshestLive - t > windowMs) {
|
|
533
|
+
pushIfRoom(out.older_memory, c, cap);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
kept.push(c);
|
|
537
|
+
}
|
|
538
|
+
out.current_truth = kept;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Build at most one conflict card (v2 restraint — surface the strongest
|
|
542
|
+
* tension, not every stale doc). Fires when a discounted source exists
|
|
543
|
+
* and there's higher-trust evidence to supersede it: the top
|
|
544
|
+
* `current_truth` card, else the first recommended live file. Returns []
|
|
545
|
+
* when there's nothing discounted, or nothing trustworthy to supersede
|
|
546
|
+
* it (in which case the honest answer is the recommended-live-files
|
|
547
|
+
* surface, not a manufactured conflict).
|
|
548
|
+
*/
|
|
549
|
+
function buildConflicts(out, claim) {
|
|
550
|
+
const loser = out.discounted[0];
|
|
551
|
+
if (!loser)
|
|
552
|
+
return [];
|
|
553
|
+
const winnerCard = out.current_truth[0];
|
|
554
|
+
if (winnerCard) {
|
|
555
|
+
const superseded = {
|
|
556
|
+
title: winnerCard.title,
|
|
557
|
+
tier: winnerCard.tier ?? "committed_doc",
|
|
558
|
+
};
|
|
559
|
+
if (winnerCard.source_path)
|
|
560
|
+
superseded.source_path = winnerCard.source_path;
|
|
561
|
+
return [conflictCard(claim, loser, superseded)];
|
|
562
|
+
}
|
|
563
|
+
const winnerFile = out.recommended_live_files[0];
|
|
564
|
+
if (winnerFile) {
|
|
565
|
+
const superseded = {
|
|
566
|
+
title: winnerFile.path,
|
|
567
|
+
tier: assignTrustTier("document", winnerFile.git_state, winnerFile.path),
|
|
568
|
+
source_path: winnerFile.path,
|
|
569
|
+
};
|
|
570
|
+
return [conflictCard(claim, loser, superseded)];
|
|
571
|
+
}
|
|
572
|
+
return [];
|
|
573
|
+
}
|
|
574
|
+
function conflictCard(claim, loser, superseded_by) {
|
|
575
|
+
const discounted = {
|
|
576
|
+
title: loser.title,
|
|
577
|
+
reason: loser.reason,
|
|
578
|
+
};
|
|
579
|
+
if (loser.source_path)
|
|
580
|
+
discounted.source_path = loser.source_path;
|
|
581
|
+
return { claim, discounted, superseded_by };
|
|
582
|
+
}
|
|
583
|
+
function pushIfRoom(arr, item, cap) {
|
|
584
|
+
if (arr.length >= cap)
|
|
585
|
+
return;
|
|
586
|
+
arr.push(item);
|
|
587
|
+
}
|
|
588
|
+
function withReason(card, reason) {
|
|
589
|
+
return { ...card, reason };
|
|
590
|
+
}
|
|
591
|
+
function buildEvidenceCard(input) {
|
|
592
|
+
const { ranked, row, kind, gitState } = input;
|
|
593
|
+
const fullContent = row?.["content"] || ranked.content || "";
|
|
594
|
+
const summary = row?.["summary"] ||
|
|
595
|
+
fullContent.split(/\r?\n/).find((l) => l.trim()) ||
|
|
596
|
+
"";
|
|
597
|
+
const snippet = truncate(collapseWhitespace(summary), EVIDENCE_SNIPPET_MAX);
|
|
598
|
+
if (!snippet)
|
|
599
|
+
return null;
|
|
600
|
+
const sourcePath = row?.["source_path"] || undefined;
|
|
601
|
+
const indexedAt = row?.["indexed_at"] || undefined;
|
|
602
|
+
const source = row?.["source"] || (kind === "document" ? "document" : "conversation");
|
|
603
|
+
const title = deriveTitle(row, sourcePath, snippet);
|
|
604
|
+
const card = {
|
|
605
|
+
id: ranked.id,
|
|
606
|
+
title,
|
|
607
|
+
source,
|
|
608
|
+
snippet,
|
|
609
|
+
};
|
|
610
|
+
if (indexedAt)
|
|
611
|
+
card.timestamp = indexedAt;
|
|
612
|
+
if (sourcePath)
|
|
613
|
+
card.source_path = sourcePath;
|
|
614
|
+
if (gitState)
|
|
615
|
+
card.git_state = gitState;
|
|
616
|
+
card.tier = assignTrustTier(kind, gitState, sourcePath);
|
|
617
|
+
const trackerDate = parseTrackerDate(fullContent);
|
|
618
|
+
if (trackerDate)
|
|
619
|
+
card.tracker_date = trackerDate;
|
|
620
|
+
return card;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Best-effort parse of a tracker's self-declared freshness from its
|
|
624
|
+
* content. Currently the `Last touched: YYYY-MM-DD` line that
|
|
625
|
+
* PROJECT_STATE.md carries near the top. Returns the matched ISO date
|
|
626
|
+
* string, or undefined when the chunk doesn't contain the marker (the
|
|
627
|
+
* caller falls back to `indexed_at`). Tolerates optional markdown
|
|
628
|
+
* bold/list decoration around the label.
|
|
629
|
+
*/
|
|
630
|
+
export function parseTrackerDate(content) {
|
|
631
|
+
if (!content)
|
|
632
|
+
return undefined;
|
|
633
|
+
const m = content.match(/(?:^|\n)\s*(?:[-*]\s*)?(?:\*\*)?Last touched:?(?:\*\*)?\s*:?\s*(\d{4}-\d{2}-\d{2})/i);
|
|
634
|
+
return m ? m[1] : undefined;
|
|
635
|
+
}
|
|
636
|
+
function deriveTitle(row, sourcePath, snippet) {
|
|
637
|
+
const meta = row?.["metadata"];
|
|
638
|
+
if (typeof meta === "string" && meta) {
|
|
639
|
+
try {
|
|
640
|
+
const parsed = JSON.parse(meta);
|
|
641
|
+
const t = parsed["title"];
|
|
642
|
+
if (typeof t === "string" && t.trim())
|
|
643
|
+
return truncate(t.trim(), 120);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// ignore
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (sourcePath) {
|
|
650
|
+
const base = sourcePath.split("/").filter(Boolean).pop();
|
|
651
|
+
if (base)
|
|
652
|
+
return truncate(base, 120);
|
|
653
|
+
}
|
|
654
|
+
const topics = row?.["topics"];
|
|
655
|
+
if (typeof topics === "string" && topics) {
|
|
656
|
+
try {
|
|
657
|
+
const arr = JSON.parse(topics);
|
|
658
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
659
|
+
return truncate(arr.filter((x) => typeof x === "string").slice(0, 3).join(", "), 120);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// ignore
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return truncate(firstLine(snippet) || "(item)", 120);
|
|
667
|
+
}
|
|
668
|
+
function firstLine(text) {
|
|
669
|
+
for (const line of text.split(/\r?\n/)) {
|
|
670
|
+
const t = line.trim();
|
|
671
|
+
if (t)
|
|
672
|
+
return t.replace(/^#+\s*/, "").replace(/^[-*]\s+/, "");
|
|
673
|
+
}
|
|
674
|
+
return "";
|
|
675
|
+
}
|
|
676
|
+
function collapseWhitespace(text) {
|
|
677
|
+
return text.replace(/\s+/g, " ").trim();
|
|
678
|
+
}
|
|
679
|
+
function truncate(text, max) {
|
|
680
|
+
if (text.length <= max)
|
|
681
|
+
return text;
|
|
682
|
+
return text.slice(0, max);
|
|
683
|
+
}
|
|
684
|
+
function basename(p) {
|
|
685
|
+
const parts = p.split("/").filter(Boolean);
|
|
686
|
+
return parts[parts.length - 1] ?? p;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Canonical fallback list when the asker is in current_truth/blended
|
|
690
|
+
* mode and we have *no* tracker hits to point at. These are the files a
|
|
691
|
+
* human would open first on this project to check the present-tense
|
|
692
|
+
* state of things — naming them by hand here is honest because the
|
|
693
|
+
* caller hasn't actually read them; the phrasing in the markdown
|
|
694
|
+
* renderer reflects that ("Before trusting this answer, read/check …").
|
|
695
|
+
*/
|
|
696
|
+
const CANONICAL_LIVE_FILES = [
|
|
697
|
+
{ path: "PROJECT_STATE.md", why: "current focus and recent decisions" },
|
|
698
|
+
{ path: "TODO.md", why: "active work" },
|
|
699
|
+
{ path: "package.json", why: "current version" },
|
|
700
|
+
];
|
|
701
|
+
function recommendLiveFiles(seen, projectRoot, discountedBasenames) {
|
|
702
|
+
if (seen.size > 0) {
|
|
703
|
+
const out = [];
|
|
704
|
+
for (const [p, gs] of seen) {
|
|
705
|
+
const why = describeTrackerPath(p);
|
|
706
|
+
const card = { path: displayPath(p, projectRoot), why };
|
|
707
|
+
if (gs)
|
|
708
|
+
card.git_state = gs;
|
|
709
|
+
out.push(card);
|
|
710
|
+
if (out.length >= 3)
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
return out;
|
|
714
|
+
}
|
|
715
|
+
// Canonical fallback. Drop any entry whose basename was already
|
|
716
|
+
// discounted as untracked/deleted in this pack — the partitioner
|
|
717
|
+
// probed it, decided not to trust it, and the fallback would
|
|
718
|
+
// otherwise re-recommend it unprobed.
|
|
719
|
+
return CANONICAL_LIVE_FILES.filter((f) => !discountedBasenames.has(basename(f.path))).map((f) => ({ ...f }));
|
|
720
|
+
}
|
|
721
|
+
function describeTrackerPath(p) {
|
|
722
|
+
if (/PROJECT_STATE\.md$/i.test(p))
|
|
723
|
+
return "current focus and recent decisions";
|
|
724
|
+
if (/TODO\.md$/i.test(p))
|
|
725
|
+
return "active work";
|
|
726
|
+
if (/package\.json$/i.test(p))
|
|
727
|
+
return "current version";
|
|
728
|
+
if (/README/i.test(p))
|
|
729
|
+
return "stated install / usage";
|
|
730
|
+
if (/CLAUDE\.md$/i.test(p))
|
|
731
|
+
return "project conventions";
|
|
732
|
+
if (/AGENTS\.md$/i.test(p))
|
|
733
|
+
return "agent contract";
|
|
734
|
+
if (/\/src\//.test(p))
|
|
735
|
+
return "live source";
|
|
736
|
+
if (/\/scripts\//.test(p))
|
|
737
|
+
return "live script";
|
|
738
|
+
return "live file";
|
|
739
|
+
}
|
|
740
|
+
function displayPath(p, projectRoot) {
|
|
741
|
+
if (projectRoot && p.startsWith(projectRoot)) {
|
|
742
|
+
const rel = p.slice(projectRoot.length).replace(/^\//, "");
|
|
743
|
+
return rel || p;
|
|
744
|
+
}
|
|
745
|
+
return p;
|
|
746
|
+
}
|
|
747
|
+
//# sourceMappingURL=current-truth.js.map
|