@gmickel/gno 2.8.3 → 2.8.5
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 +1 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.8.3.zip → gno-browser-clipper-v2.8.5.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.5.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +24 -7
- package/spec/output-schemas/doctor.schema.json +32 -6
- package/spec/output-schemas/status.schema.json +26 -4
- package/src/cli/commands/doctor-activation.ts +62 -26
- package/src/cli/commands/doctor.ts +4 -1
- package/src/cli/commands/status.ts +23 -6
- package/src/core/activation-connector-health.ts +99 -7
- package/src/core/activation-empty.ts +16 -0
- package/src/core/activation-status.ts +55 -11
- package/src/core/links.ts +53 -15
- package/src/ingestion/strip.ts +69 -27
- package/src/serve/activation-health.ts +52 -24
- package/src/serve/public/components/BootstrapStatus.tsx +17 -4
- package/src/serve/status.ts +27 -8
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.3.zip.sha256 +0 -1
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ See the [guide](docs/COMPILED-CONTEXT.md).
|
|
|
139
139
|
|
|
140
140
|
<!-- public-truth:current-version -->
|
|
141
141
|
|
|
142
|
-
> Current source version: **v2.8.
|
|
142
|
+
> Current source version: **v2.8.5**. See [CHANGELOG.md](./CHANGELOG.md).
|
|
143
143
|
|
|
144
144
|
<!-- /public-truth -->
|
|
145
145
|
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
347695588c724660218a91ae70cd7bd8420445d2f78dd293cf661626380db7f2 gno-browser-clipper-v2.8.5.zip
|
package/package.json
CHANGED
package/spec/cli.md
CHANGED
|
@@ -351,7 +351,12 @@ gno status [--json|--md]
|
|
|
351
351
|
```
|
|
352
352
|
|
|
353
353
|
`activation.usable` means at least one configured collection passed its local
|
|
354
|
-
lexical proof. `activation.healthy` means
|
|
354
|
+
lexical proof. `activation.healthy` means at least one collection is configured
|
|
355
|
+
and none failed its proof. A collection with no documents yet (index stage
|
|
356
|
+
`no_documents`, remediation `gno update`) is informational, not a failure: it
|
|
357
|
+
leaves `healthy` true, projects no connector proofs, and terminal output
|
|
358
|
+
lists it as `no documents yet` (`Lexical activation: NO DOCUMENTS YET` when no
|
|
359
|
+
collection has documents).
|
|
355
360
|
Semantic and connector stages remain independent; passive status never starts a
|
|
356
361
|
model runtime or connector process. `gno status` still exits 0 when activation
|
|
357
362
|
is unhealthy so scripts can inspect the structured state.
|
|
@@ -401,11 +406,19 @@ Passive callers report `semantic_not_checked` when vector runtime availability
|
|
|
401
406
|
is unknown. `vector_unavailable` is reserved for a resident runtime that has
|
|
402
407
|
positively reported vector search unavailable.
|
|
403
408
|
|
|
404
|
-
`connectorProjection.total` counts every
|
|
405
|
-
target pair
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
+
`connectorProjection.total` counts every non-empty collection and connector
|
|
410
|
+
target pair. Every pair is evaluated from cached receipts; the 64-pair cap
|
|
411
|
+
bounds only the listed `connectors`. `projected` equals `connectors.length`,
|
|
412
|
+
and `truncated` is true exactly when `total > projected`. When truncated,
|
|
413
|
+
`omitted` counts the unlisted pairs by outcome (`passed`, `failed`,
|
|
414
|
+
`incomplete`, `notApplicable`; the counts sum to `total - projected`). Connector
|
|
415
|
+
completeness, and so overall health, is decided from the listed pairs plus
|
|
416
|
+
these counts: a truncated projection whose pairs all passed or are not
|
|
417
|
+
applicable is healthy, and a failed or pending pair beyond the list keeps it
|
|
418
|
+
degraded. Human-readable output keeps the `N/M ... shown; K omitted` line and
|
|
419
|
+
summarizes the omitted outcomes, for example `(108 passed, 1 failed)`. A
|
|
420
|
+
truncated projection without `omitted` claims no result for the unlisted pairs
|
|
421
|
+
and is never reported healthy.
|
|
409
422
|
|
|
410
423
|
**Exit Codes:**
|
|
411
424
|
|
|
@@ -3022,7 +3035,11 @@ The additive `activation` object uses the same contract as `gno status` and
|
|
|
3022
3035
|
`GET /api/status`. Doctor performs only the local lexical proof. It never starts
|
|
3023
3036
|
connector children or initializes/downloads models. A failed lexical proof adds
|
|
3024
3037
|
the `retrieval-activation` error check and exits 2 after writing the complete
|
|
3025
|
-
result; no duplicate error is written to stderr.
|
|
3038
|
+
result; no duplicate error is written to stderr. A collection with no
|
|
3039
|
+
documents yet is not a failed proof: when it is the only activation problem,
|
|
3040
|
+
`retrieval-activation` has status `info` (terminal icon `i`) and lists the
|
|
3041
|
+
collection with its remediation (add files, then `gno update`). Check status
|
|
3042
|
+
is one of `ok`, `info`, `warn` or `error`; only `error` fails doctor. Connector failure or projection
|
|
3026
3043
|
truncation adds a warning and makes the structured doctor result non-healthy,
|
|
3027
3044
|
but preserves exit 0 when lexical proof and all other required checks pass. An
|
|
3028
3045
|
omitted target/collection pair has no inferred result.
|
|
@@ -24,8 +24,14 @@
|
|
|
24
24
|
],
|
|
25
25
|
"properties": {
|
|
26
26
|
"schemaVersion": { "const": "1.0" },
|
|
27
|
-
"usable": {
|
|
28
|
-
|
|
27
|
+
"usable": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"description": "At least one collection passed the lexical retrieval proof"
|
|
30
|
+
},
|
|
31
|
+
"healthy": {
|
|
32
|
+
"type": "boolean",
|
|
33
|
+
"description": "At least one collection is configured and none failed its lexical retrieval proof. A collection with no documents yet (remediation code no_documents, command gno update) is informational, not a failure."
|
|
34
|
+
},
|
|
29
35
|
"collections": {
|
|
30
36
|
"type": "array",
|
|
31
37
|
"items": {
|
|
@@ -158,7 +164,7 @@
|
|
|
158
164
|
"total": {
|
|
159
165
|
"type": "integer",
|
|
160
166
|
"minimum": 0,
|
|
161
|
-
"description": "All
|
|
167
|
+
"description": "All non-empty collection and connector target pairs; every pair is evaluated from cached receipts."
|
|
162
168
|
},
|
|
163
169
|
"projected": {
|
|
164
170
|
"type": "integer",
|
|
@@ -168,7 +174,27 @@
|
|
|
168
174
|
},
|
|
169
175
|
"truncated": {
|
|
170
176
|
"type": "boolean",
|
|
171
|
-
"description": "True exactly when total is greater than projected
|
|
177
|
+
"description": "True exactly when total is greater than projected. Omitted pairs are still evaluated; their outcomes are counted in omitted."
|
|
178
|
+
},
|
|
179
|
+
"omitted": {
|
|
180
|
+
"type": "object",
|
|
181
|
+
"additionalProperties": false,
|
|
182
|
+
"required": ["passed", "failed", "incomplete", "notApplicable"],
|
|
183
|
+
"description": "Present when truncated: outcome counts for the evaluated pairs not listed in connectors. The counts sum to total - projected. Connector completeness is decided from connectors plus these counts.",
|
|
184
|
+
"properties": {
|
|
185
|
+
"passed": { "type": "integer", "minimum": 0 },
|
|
186
|
+
"failed": { "type": "integer", "minimum": 0 },
|
|
187
|
+
"incomplete": {
|
|
188
|
+
"type": "integer",
|
|
189
|
+
"minimum": 0,
|
|
190
|
+
"description": "Pending, or skipped for a runtime reason such as lexical retrieval not ready."
|
|
191
|
+
},
|
|
192
|
+
"notApplicable": {
|
|
193
|
+
"type": "integer",
|
|
194
|
+
"minimum": 0,
|
|
195
|
+
"description": "Connector not configured, or a skill runtime that cannot be verified."
|
|
196
|
+
}
|
|
197
|
+
}
|
|
172
198
|
}
|
|
173
199
|
}
|
|
174
200
|
}
|
|
@@ -229,8 +255,8 @@
|
|
|
229
255
|
},
|
|
230
256
|
"status": {
|
|
231
257
|
"type": "string",
|
|
232
|
-
"enum": ["ok", "warn", "error"],
|
|
233
|
-
"description": "Check result status"
|
|
258
|
+
"enum": ["ok", "info", "warn", "error"],
|
|
259
|
+
"description": "Check result status. info reports a state that needs no repair, such as a collection with no documents yet"
|
|
234
260
|
},
|
|
235
261
|
"message": {
|
|
236
262
|
"type": "string",
|
|
@@ -347,10 +347,12 @@
|
|
|
347
347
|
"const": "1.0"
|
|
348
348
|
},
|
|
349
349
|
"usable": {
|
|
350
|
-
"type": "boolean"
|
|
350
|
+
"type": "boolean",
|
|
351
|
+
"description": "At least one collection passed the lexical retrieval proof"
|
|
351
352
|
},
|
|
352
353
|
"healthy": {
|
|
353
|
-
"type": "boolean"
|
|
354
|
+
"type": "boolean",
|
|
355
|
+
"description": "At least one collection is configured and none failed its lexical retrieval proof. A collection with no documents yet (remediation code no_documents, command gno update) is informational, not a failure."
|
|
354
356
|
},
|
|
355
357
|
"collections": {
|
|
356
358
|
"type": "array",
|
|
@@ -512,7 +514,7 @@
|
|
|
512
514
|
"total": {
|
|
513
515
|
"type": "integer",
|
|
514
516
|
"minimum": 0,
|
|
515
|
-
"description": "All
|
|
517
|
+
"description": "All non-empty collection and connector target pairs; every pair is evaluated from cached receipts."
|
|
516
518
|
},
|
|
517
519
|
"projected": {
|
|
518
520
|
"type": "integer",
|
|
@@ -522,7 +524,27 @@
|
|
|
522
524
|
},
|
|
523
525
|
"truncated": {
|
|
524
526
|
"type": "boolean",
|
|
525
|
-
"description": "True exactly when total is greater than projected
|
|
527
|
+
"description": "True exactly when total is greater than projected. Omitted pairs are still evaluated; their outcomes are counted in omitted."
|
|
528
|
+
},
|
|
529
|
+
"omitted": {
|
|
530
|
+
"type": "object",
|
|
531
|
+
"additionalProperties": false,
|
|
532
|
+
"required": ["passed", "failed", "incomplete", "notApplicable"],
|
|
533
|
+
"description": "Present when truncated: outcome counts for the evaluated pairs not listed in connectors. The counts sum to total - projected. Connector completeness is decided from connectors plus these counts.",
|
|
534
|
+
"properties": {
|
|
535
|
+
"passed": { "type": "integer", "minimum": 0 },
|
|
536
|
+
"failed": { "type": "integer", "minimum": 0 },
|
|
537
|
+
"incomplete": {
|
|
538
|
+
"type": "integer",
|
|
539
|
+
"minimum": 0,
|
|
540
|
+
"description": "Pending, or skipped for a runtime reason such as lexical retrieval not ready."
|
|
541
|
+
},
|
|
542
|
+
"notApplicable": {
|
|
543
|
+
"type": "integer",
|
|
544
|
+
"minimum": 0,
|
|
545
|
+
"description": "Connector not configured, or a skill runtime that cannot be verified."
|
|
546
|
+
}
|
|
547
|
+
}
|
|
526
548
|
}
|
|
527
549
|
}
|
|
528
550
|
}
|
|
@@ -6,6 +6,12 @@ import type { StorePort } from "../../store/types";
|
|
|
6
6
|
import type { DoctorCheck } from "./doctor";
|
|
7
7
|
|
|
8
8
|
import { getIndexDbPath, getModelsCachePath } from "../../app/constants";
|
|
9
|
+
import {
|
|
10
|
+
classifyConnectorProof,
|
|
11
|
+
describeOmittedConnectorPairs,
|
|
12
|
+
omittedConnectorSummary,
|
|
13
|
+
} from "../../core/activation-connector-health";
|
|
14
|
+
import { isEmptyActivationCollection } from "../../core/activation-empty";
|
|
9
15
|
import { buildActivationStatus } from "../../core/activation-status";
|
|
10
16
|
import { ModelCache } from "../../llm/cache";
|
|
11
17
|
import { getActivePreset } from "../../llm/registry";
|
|
@@ -75,28 +81,54 @@ export async function buildDoctorActivation(
|
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
|
|
84
|
+
const plural = (count: number, noun: string): string =>
|
|
85
|
+
`${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
86
|
+
|
|
78
87
|
export function checkRetrievalActivation(
|
|
79
88
|
activation: ActivationStatus
|
|
80
89
|
): DoctorCheck {
|
|
90
|
+
const ready = activation.collections.filter((collection) => collection.ready);
|
|
91
|
+
const empty = activation.collections.filter(isEmptyActivationCollection);
|
|
92
|
+
const emptyDetails = empty.flatMap(({ collection, remediation }) => [
|
|
93
|
+
`${collection}: no documents indexed yet (informational)`,
|
|
94
|
+
`Add files, then run: ${remediation?.command ?? "gno update"}`,
|
|
95
|
+
]);
|
|
96
|
+
|
|
81
97
|
if (activation.healthy) {
|
|
82
98
|
const semanticStates = [
|
|
83
99
|
...new Set(
|
|
84
|
-
|
|
85
|
-
({ semanticAvailability }) => semanticAvailability.code
|
|
86
|
-
)
|
|
100
|
+
ready.map(({ semanticAvailability }) => semanticAvailability.code)
|
|
87
101
|
),
|
|
88
102
|
];
|
|
103
|
+
const semanticDetail =
|
|
104
|
+
ready.length > 0
|
|
105
|
+
? [
|
|
106
|
+
`Semantic retrieval remains separate (${semanticStates.join(", ")}).`,
|
|
107
|
+
]
|
|
108
|
+
: [];
|
|
109
|
+
if (empty.length === 0) {
|
|
110
|
+
return {
|
|
111
|
+
name: "retrieval-activation",
|
|
112
|
+
status: "ok",
|
|
113
|
+
message: `${plural(ready.length, "collection")} passed lexical retrieval proof`,
|
|
114
|
+
details: semanticDetail,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
89
117
|
return {
|
|
90
118
|
name: "retrieval-activation",
|
|
91
|
-
status: "
|
|
92
|
-
message:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
status: "info",
|
|
120
|
+
message:
|
|
121
|
+
ready.length > 0
|
|
122
|
+
? `${plural(ready.length, "collection")} passed lexical retrieval proof; ${plural(empty.length, "collection")} with no documents yet`
|
|
123
|
+
: `No documents indexed yet in ${plural(empty.length, "collection")}`,
|
|
124
|
+
details: [...emptyDetails, ...semanticDetail],
|
|
96
125
|
};
|
|
97
126
|
}
|
|
98
127
|
|
|
99
|
-
const failed = activation.collections.filter(
|
|
128
|
+
const failed = activation.collections.filter(
|
|
129
|
+
(collection) =>
|
|
130
|
+
!collection.ready && !isEmptyActivationCollection(collection)
|
|
131
|
+
);
|
|
100
132
|
const details = failed.flatMap(({ collection, remediation }) =>
|
|
101
133
|
remediation
|
|
102
134
|
? [
|
|
@@ -112,9 +144,9 @@ export function checkRetrievalActivation(
|
|
|
112
144
|
activation.collections.length === 0
|
|
113
145
|
? "No collections configured. Run: gno collection add"
|
|
114
146
|
: activation.usable
|
|
115
|
-
? `${failed.length
|
|
147
|
+
? `${plural(failed.length, "collection")} failed lexical retrieval proof`
|
|
116
148
|
: "No configured collection passed lexical retrieval proof",
|
|
117
|
-
details,
|
|
149
|
+
details: [...details, ...emptyDetails],
|
|
118
150
|
};
|
|
119
151
|
}
|
|
120
152
|
|
|
@@ -122,13 +154,14 @@ export function checkConnectorActivation(
|
|
|
122
154
|
activation: ActivationStatus
|
|
123
155
|
): DoctorCheck | null {
|
|
124
156
|
const { projected, total, truncated } = activation.connectorProjection;
|
|
125
|
-
const omitted =
|
|
157
|
+
const omitted = omittedConnectorSummary(activation);
|
|
126
158
|
const observed = activation.connectors.filter(
|
|
127
|
-
(
|
|
128
|
-
code !== "connector_not_configured" &&
|
|
129
|
-
code !== "target_runtime_unverifiable"
|
|
159
|
+
(connector) => classifyConnectorProof(connector) !== "notApplicable"
|
|
130
160
|
);
|
|
131
|
-
|
|
161
|
+
const omittedObserved = omitted
|
|
162
|
+
? omitted.passed + omitted.failed + omitted.incomplete
|
|
163
|
+
: 0;
|
|
164
|
+
if (observed.length === 0 && omittedObserved === 0 && omitted) {
|
|
132
165
|
return null;
|
|
133
166
|
}
|
|
134
167
|
const incomplete = observed.filter(({ status }) => status !== "passed");
|
|
@@ -136,20 +169,23 @@ export function checkConnectorActivation(
|
|
|
136
169
|
({ collection, target, status, code, remediation }) =>
|
|
137
170
|
`${target}/${collection}: ${status}${code ? `/${code}` : ""}${remediation ? `. ${remediation}` : ""}`
|
|
138
171
|
);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
172
|
+
const omittedDetail = describeOmittedConnectorPairs(activation);
|
|
173
|
+
if (omittedDetail) {
|
|
174
|
+
details.unshift(omittedDetail);
|
|
143
175
|
}
|
|
176
|
+
const unresolved =
|
|
177
|
+
incomplete.length + (omitted ? omitted.failed + omitted.incomplete : 0);
|
|
178
|
+
const passed = observed.length - incomplete.length + (omitted?.passed ?? 0);
|
|
179
|
+
const listing = truncated ? ` (${projected} of ${total} checks listed)` : "";
|
|
144
180
|
return {
|
|
145
181
|
name: "connector-activation",
|
|
146
|
-
status:
|
|
182
|
+
status: unresolved > 0 || !omitted ? "warn" : "ok",
|
|
147
183
|
message:
|
|
148
|
-
|
|
149
|
-
? `${
|
|
150
|
-
:
|
|
151
|
-
? `${
|
|
152
|
-
: `${
|
|
184
|
+
unresolved > 0
|
|
185
|
+
? `${unresolved} connector proof${unresolved === 1 ? "" : "s"} pending or failed${listing}`
|
|
186
|
+
: omitted
|
|
187
|
+
? `${passed} connector proof${passed === 1 ? "" : "s"} passed${listing}`
|
|
188
|
+
: `${projected} of ${total} connector target/collection checks projected`,
|
|
153
189
|
details,
|
|
154
190
|
};
|
|
155
191
|
}
|
|
@@ -55,7 +55,8 @@ import {
|
|
|
55
55
|
// Types
|
|
56
56
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
/** `info` reports a state that needs no repair, such as an empty collection. */
|
|
59
|
+
export type DoctorCheckStatus = "ok" | "info" | "warn" | "error";
|
|
59
60
|
|
|
60
61
|
export interface DoctorCheck {
|
|
61
62
|
name: string;
|
|
@@ -754,6 +755,8 @@ function statusIcon(status: DoctorCheckStatus): string {
|
|
|
754
755
|
switch (status) {
|
|
755
756
|
case "ok":
|
|
756
757
|
return "✓";
|
|
758
|
+
case "info":
|
|
759
|
+
return "i";
|
|
757
760
|
case "warn":
|
|
758
761
|
return "!";
|
|
759
762
|
case "error":
|
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
isInitialized,
|
|
19
19
|
loadConfig,
|
|
20
20
|
} from "../../config";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
describeConnectorSummary,
|
|
23
|
+
isConnectorActivationComplete,
|
|
24
|
+
omittedConnectorSummary,
|
|
25
|
+
} from "../../core/activation-connector-health";
|
|
26
|
+
import { isEmptyActivationCollection } from "../../core/activation-empty";
|
|
22
27
|
import { buildActivationStatus } from "../../core/activation-status";
|
|
23
28
|
import { formatChunkingStatus } from "../../core/chunking-status";
|
|
24
29
|
import { formatLinkWorkspace } from "../../core/link-workspace";
|
|
@@ -145,7 +150,8 @@ function connectorProjectionLine(activation: ActivationStatus): string | null {
|
|
|
145
150
|
if (!truncated) {
|
|
146
151
|
return null;
|
|
147
152
|
}
|
|
148
|
-
|
|
153
|
+
const omitted = omittedConnectorSummary(activation);
|
|
154
|
+
return `Connector projection: ${projected}/${total} target/collection checks shown; ${total - projected} omitted${omitted ? ` (${describeConnectorSummary(omitted)})` : ", no result claimed"}`;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
function isStatusHealthy(
|
|
@@ -229,10 +235,21 @@ function formatTerminal(
|
|
|
229
235
|
`Health: ${isStatusHealthy(indexStatus, activation) ? "OK" : "DEGRADED"}`
|
|
230
236
|
);
|
|
231
237
|
lines.push("");
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
const activationState = activation.healthy
|
|
239
|
+
? activation.usable
|
|
240
|
+
? "READY"
|
|
241
|
+
: "NO DOCUMENTS YET"
|
|
242
|
+
: activation.usable
|
|
243
|
+
? "DEGRADED"
|
|
244
|
+
: "BLOCKED";
|
|
245
|
+
lines.push(`Lexical activation: ${activationState}`);
|
|
235
246
|
for (const collection of activation.collections) {
|
|
247
|
+
if (isEmptyActivationCollection(collection)) {
|
|
248
|
+
lines.push(
|
|
249
|
+
` ${collection.collection}: no documents yet (add files, then run ${collection.remediation?.command})`
|
|
250
|
+
);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
236
253
|
const failedStage = collection.remediation?.stage;
|
|
237
254
|
const suffix = failedStage
|
|
238
255
|
? ` (${failedStage}: ${collection.remediation?.code}; ${collection.remediation?.command})`
|
|
@@ -325,7 +342,7 @@ function formatMarkdown(
|
|
|
325
342
|
lines.push(`- **Lexically healthy**: ${activation.healthy}`);
|
|
326
343
|
for (const collection of activation.collections) {
|
|
327
344
|
lines.push(
|
|
328
|
-
`- **${collection.collection}**: ${collection.ready ? "lexical ready" : `${collection.remediation?.stage ?? "index"} ${collection.remediation?.code ?? "index_query_failed"}`}`
|
|
345
|
+
`- **${collection.collection}**: ${collection.ready ? "lexical ready" : isEmptyActivationCollection(collection) ? `no documents yet (${collection.remediation?.command})` : `${collection.remediation?.stage ?? "index"} ${collection.remediation?.code ?? "index_query_failed"}`}`
|
|
329
346
|
);
|
|
330
347
|
}
|
|
331
348
|
for (const connector of activation.connectors) {
|
|
@@ -1,19 +1,111 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ActivationConnectorStatus,
|
|
3
|
+
ActivationStatus,
|
|
4
|
+
ConnectorOmittedSummary,
|
|
5
|
+
} from "./activation-status";
|
|
2
6
|
|
|
3
7
|
const NON_RUNTIME_CODES = new Set([
|
|
4
8
|
"connector_not_configured",
|
|
5
9
|
"target_runtime_unverifiable",
|
|
6
10
|
]);
|
|
7
11
|
|
|
8
|
-
|
|
12
|
+
export type ConnectorProofOutcome = keyof ConnectorOmittedSummary;
|
|
13
|
+
|
|
14
|
+
/** One shared reading of a connector pair, used for listed and unlisted pairs alike. */
|
|
15
|
+
export function classifyConnectorProof({
|
|
16
|
+
code,
|
|
17
|
+
status,
|
|
18
|
+
}: Pick<ActivationConnectorStatus, "code" | "status">): ConnectorProofOutcome {
|
|
19
|
+
if (code !== undefined && NON_RUNTIME_CODES.has(code)) {
|
|
20
|
+
return "notApplicable";
|
|
21
|
+
}
|
|
22
|
+
if (status === "passed" || status === "failed") {
|
|
23
|
+
return status;
|
|
24
|
+
}
|
|
25
|
+
return "incomplete";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function summarizeConnectorProofs(
|
|
29
|
+
connectors: readonly Pick<ActivationConnectorStatus, "code" | "status">[]
|
|
30
|
+
): ConnectorOmittedSummary {
|
|
31
|
+
const summary: ConnectorOmittedSummary = {
|
|
32
|
+
passed: 0,
|
|
33
|
+
failed: 0,
|
|
34
|
+
incomplete: 0,
|
|
35
|
+
notApplicable: 0,
|
|
36
|
+
};
|
|
37
|
+
for (const connector of connectors) {
|
|
38
|
+
summary[classifyConnectorProof(connector)] += 1;
|
|
39
|
+
}
|
|
40
|
+
return summary;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Summary of the pairs past the display cap, or null when some were omitted
|
|
45
|
+
* without an evaluated result (fail closed).
|
|
46
|
+
*/
|
|
47
|
+
export function omittedConnectorSummary(
|
|
48
|
+
activation: ActivationStatus
|
|
49
|
+
): ConnectorOmittedSummary | null {
|
|
50
|
+
const { omitted, projected, total, truncated } =
|
|
51
|
+
activation.connectorProjection;
|
|
52
|
+
if (!truncated) {
|
|
53
|
+
return { passed: 0, failed: 0, incomplete: 0, notApplicable: 0 };
|
|
54
|
+
}
|
|
55
|
+
if (!omitted) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const counted =
|
|
59
|
+
omitted.passed +
|
|
60
|
+
omitted.failed +
|
|
61
|
+
omitted.incomplete +
|
|
62
|
+
omitted.notApplicable;
|
|
63
|
+
return counted === total - projected ? omitted : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Human summary of unlisted pairs, e.g. "108 passed, 1 failed". */
|
|
67
|
+
export function describeConnectorSummary(
|
|
68
|
+
summary: ConnectorOmittedSummary
|
|
69
|
+
): string {
|
|
70
|
+
const parts = [
|
|
71
|
+
summary.passed > 0 ? `${summary.passed} passed` : null,
|
|
72
|
+
summary.failed > 0 ? `${summary.failed} failed` : null,
|
|
73
|
+
summary.incomplete > 0 ? `${summary.incomplete} pending` : null,
|
|
74
|
+
summary.notApplicable > 0
|
|
75
|
+
? `${summary.notApplicable} not configured or not verifiable`
|
|
76
|
+
: null,
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Sentence describing the pairs past the display cap, or null when none were omitted. */
|
|
82
|
+
export function describeOmittedConnectorPairs(
|
|
83
|
+
activation: ActivationStatus
|
|
84
|
+
): string | null {
|
|
85
|
+
const { projected, total, truncated } = activation.connectorProjection;
|
|
86
|
+
if (!truncated) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const omitted = total - projected;
|
|
90
|
+
const summary = omittedConnectorSummary(activation);
|
|
91
|
+
return summary
|
|
92
|
+
? `${omitted} more target/collection checks are not listed: ${describeConnectorSummary(summary)}.`
|
|
93
|
+
: `${omitted} target/collection checks were omitted by the bounded status projection; no result is claimed for them.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* True when every collection/target pair, listed or not, passed or has a
|
|
98
|
+
* non-runtime code. Pairs omitted without an evaluated result fail closed.
|
|
99
|
+
*/
|
|
9
100
|
export function isConnectorActivationComplete(
|
|
10
101
|
activation: ActivationStatus
|
|
11
102
|
): boolean {
|
|
12
|
-
|
|
103
|
+
const omitted = omittedConnectorSummary(activation);
|
|
104
|
+
if (!omitted || omitted.failed > 0 || omitted.incomplete > 0) {
|
|
13
105
|
return false;
|
|
14
106
|
}
|
|
15
|
-
return activation.connectors.every(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
107
|
+
return activation.connectors.every((connector) => {
|
|
108
|
+
const outcome = classifyConnectorProof(connector);
|
|
109
|
+
return outcome === "passed" || outcome === "notApplicable";
|
|
110
|
+
});
|
|
19
111
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty-collection rule shared by server surfaces and the Web UI bundle, so
|
|
3
|
+
* it carries type imports only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ActivationCollectionStatus } from "./activation-status";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A collection with nothing indexed yet: its only activation problem is
|
|
10
|
+
* `no_documents`. Surfaces show it as informational, never as a failure.
|
|
11
|
+
*/
|
|
12
|
+
export function isEmptyActivationCollection(
|
|
13
|
+
collection: Pick<ActivationCollectionStatus, "ready" | "remediation">
|
|
14
|
+
): boolean {
|
|
15
|
+
return !collection.ready && collection.remediation?.code === "no_documents";
|
|
16
|
+
}
|
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
ConnectorVerificationTarget,
|
|
15
15
|
} from "./connector-verifier";
|
|
16
16
|
|
|
17
|
+
import { summarizeConnectorProofs } from "./activation-connector-health";
|
|
18
|
+
import { isEmptyActivationCollection } from "./activation-empty";
|
|
17
19
|
import { createEphemeralActivationProbePlan } from "./activation-probe-plan";
|
|
18
20
|
import { verifyLexicalActivation } from "./activation-verifier";
|
|
19
21
|
import {
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
} from "./connector-verifier";
|
|
23
25
|
|
|
24
26
|
const DEFAULT_CONCURRENCY = 4;
|
|
25
|
-
|
|
27
|
+
/** Bounds the listed pairs only; completeness is decided from every pair. */
|
|
26
28
|
const MAX_CONNECTOR_PROJECTIONS = 64;
|
|
27
29
|
const CONNECTOR_CODES = new Set<ActivationVerificationCode>([
|
|
28
30
|
"connector_not_configured",
|
|
@@ -73,7 +75,13 @@ export interface ActivationConnectorStatus {
|
|
|
73
75
|
|
|
74
76
|
export interface ActivationStatus {
|
|
75
77
|
schemaVersion: "1.0";
|
|
78
|
+
/** At least one collection passed the lexical proof. */
|
|
76
79
|
usable: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* At least one collection is configured and none failed its proof. A
|
|
82
|
+
* collection with no documents yet is not a failure (see
|
|
83
|
+
* `isEmptyActivationCollection`), so it does not clear this flag.
|
|
84
|
+
*/
|
|
77
85
|
healthy: boolean;
|
|
78
86
|
collections: ActivationCollectionStatus[];
|
|
79
87
|
/** Only fingerprint-current persisted connector receipts may appear here. */
|
|
@@ -82,9 +90,21 @@ export interface ActivationStatus {
|
|
|
82
90
|
total: number;
|
|
83
91
|
projected: number;
|
|
84
92
|
truncated: boolean;
|
|
93
|
+
/** Outcome counts for the evaluated pairs past the display cap; present when truncated. */
|
|
94
|
+
omitted?: ConnectorOmittedSummary;
|
|
85
95
|
};
|
|
86
96
|
}
|
|
87
97
|
|
|
98
|
+
/** Per-outcome counts of evaluated connector pairs that are not listed. */
|
|
99
|
+
export interface ConnectorOmittedSummary {
|
|
100
|
+
passed: number;
|
|
101
|
+
failed: number;
|
|
102
|
+
/** Pending, or skipped for a runtime reason (for example lexical not ready). */
|
|
103
|
+
incomplete: number;
|
|
104
|
+
/** Not configured, or a skill runtime that cannot be verified. */
|
|
105
|
+
notApplicable: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
export interface ActivationStatusOptions {
|
|
89
109
|
concurrency?: number;
|
|
90
110
|
semantic?: {
|
|
@@ -178,10 +198,13 @@ function remediationFor(
|
|
|
178
198
|
stage: ActivationStageName,
|
|
179
199
|
code: ActivationVerificationCode
|
|
180
200
|
): ActivationRemediation {
|
|
181
|
-
const command =
|
|
201
|
+
const command =
|
|
202
|
+
code === "no_documents"
|
|
203
|
+
? "gno update"
|
|
204
|
+
: `gno index ${collection} --no-embed`;
|
|
182
205
|
const messages: Partial<Record<ActivationVerificationCode, string>> = {
|
|
183
206
|
no_documents:
|
|
184
|
-
"
|
|
207
|
+
"No documents are indexed yet. Add supported files to this collection's folder, then run gno update.",
|
|
185
208
|
no_probe_term:
|
|
186
209
|
"Add searchable text or adjust the collection filters, then reindex.",
|
|
187
210
|
index_query_failed:
|
|
@@ -319,14 +342,22 @@ async function buildConnectorStatuses(
|
|
|
319
342
|
store: StorePort,
|
|
320
343
|
collections: VerifiedCollection[],
|
|
321
344
|
targets: readonly ConnectorVerificationTarget[]
|
|
322
|
-
): Promise<{
|
|
345
|
+
): Promise<{
|
|
346
|
+
items: ActivationConnectorStatus[];
|
|
347
|
+
omitted: ConnectorOmittedSummary;
|
|
348
|
+
total: number;
|
|
349
|
+
}> {
|
|
323
350
|
const sortedTargets = [...targets].sort((a, b) => a.id.localeCompare(b.id));
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
351
|
+
// A collection with no documents gives a connector nothing to prove.
|
|
352
|
+
const provable = collections.filter(
|
|
353
|
+
({ projected }) => !isEmptyActivationCollection(projected)
|
|
354
|
+
);
|
|
355
|
+
const allPairs = provable.flatMap((collection) =>
|
|
356
|
+
sortedTargets.map((target) => ({ collection, target }))
|
|
327
357
|
);
|
|
328
|
-
|
|
329
|
-
|
|
358
|
+
// Every pair is read from cached receipts only; the cap bounds the listing.
|
|
359
|
+
const evaluated = await mapBounded(
|
|
360
|
+
allPairs,
|
|
330
361
|
DEFAULT_CONCURRENCY,
|
|
331
362
|
async ({ collection, target }) => {
|
|
332
363
|
const fallback = connectorFallback(
|
|
@@ -363,7 +394,13 @@ async function buildConnectorStatuses(
|
|
|
363
394
|
};
|
|
364
395
|
}
|
|
365
396
|
);
|
|
366
|
-
return {
|
|
397
|
+
return {
|
|
398
|
+
items: evaluated.slice(0, MAX_CONNECTOR_PROJECTIONS),
|
|
399
|
+
omitted: summarizeConnectorProofs(
|
|
400
|
+
evaluated.slice(MAX_CONNECTOR_PROJECTIONS)
|
|
401
|
+
),
|
|
402
|
+
total: evaluated.length,
|
|
403
|
+
};
|
|
367
404
|
}
|
|
368
405
|
|
|
369
406
|
async function verifyCoalesced(
|
|
@@ -501,13 +538,20 @@ export async function buildActivationStatus(
|
|
|
501
538
|
schemaVersion: "1.0",
|
|
502
539
|
usable: projected.some((collection) => collection.ready),
|
|
503
540
|
healthy:
|
|
504
|
-
projected.length > 0 &&
|
|
541
|
+
projected.length > 0 &&
|
|
542
|
+
projected.every(
|
|
543
|
+
(collection) =>
|
|
544
|
+
collection.ready || isEmptyActivationCollection(collection)
|
|
545
|
+
),
|
|
505
546
|
collections: projected,
|
|
506
547
|
connectors: connectorStatuses.items,
|
|
507
548
|
connectorProjection: {
|
|
508
549
|
total: connectorStatuses.total,
|
|
509
550
|
projected: connectorStatuses.items.length,
|
|
510
551
|
truncated: connectorStatuses.total > connectorStatuses.items.length,
|
|
552
|
+
...(connectorStatuses.total > connectorStatuses.items.length
|
|
553
|
+
? { omitted: connectorStatuses.omitted }
|
|
554
|
+
: {}),
|
|
511
555
|
},
|
|
512
556
|
};
|
|
513
557
|
}
|
package/src/core/links.ts
CHANGED
|
@@ -80,11 +80,53 @@ const UNSAFE_PERCENT_CODES = new Set(["%2F", "%5C", "%00", "%2f", "%5c"]);
|
|
|
80
80
|
// Regex Patterns
|
|
81
81
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
82
|
|
|
83
|
+
interface WikiLinkMatch {
|
|
84
|
+
index: number;
|
|
85
|
+
raw: string;
|
|
86
|
+
content: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
/**
|
|
84
|
-
* Wiki
|
|
85
|
-
*
|
|
90
|
+
* Wiki links: [[target]], [[target|alias]], [[target#anchor]] or
|
|
91
|
+
* [[collection:target]]. Matches exactly what
|
|
92
|
+
* `/\[\[([^\]|]+(?:\|[^\]]+)?)\]\]/g` matches, in one pass: the content runs
|
|
93
|
+
* to the first `]`, which must be doubled; it must not start with `|`, and a
|
|
94
|
+
* `|` in it needs text after it. The regex rescanned to that `]` from every
|
|
95
|
+
* unclosed `[[`, so a note with thousands of them took minutes.
|
|
86
96
|
*/
|
|
87
|
-
|
|
97
|
+
export function* findWikiLinks(
|
|
98
|
+
markdown: string
|
|
99
|
+
): Generator<WikiLinkMatch, void, undefined> {
|
|
100
|
+
let nextClose = -2;
|
|
101
|
+
let nextPipe = -2;
|
|
102
|
+
let start = markdown.indexOf("[[");
|
|
103
|
+
while (start !== -1) {
|
|
104
|
+
const contentStart = start + 2;
|
|
105
|
+
if (nextClose < contentStart) {
|
|
106
|
+
nextClose = markdown.indexOf("]", contentStart);
|
|
107
|
+
if (nextClose === -1) return;
|
|
108
|
+
}
|
|
109
|
+
if (nextPipe !== -1 && nextPipe < contentStart) {
|
|
110
|
+
nextPipe = markdown.indexOf("|", contentStart);
|
|
111
|
+
}
|
|
112
|
+
const pipeInContent = nextPipe !== -1 && nextPipe < nextClose;
|
|
113
|
+
const matched =
|
|
114
|
+
markdown.charCodeAt(nextClose + 1) === 93 /* ] */ &&
|
|
115
|
+
nextClose > contentStart &&
|
|
116
|
+
nextPipe !== contentStart &&
|
|
117
|
+
(!pipeInContent || nextClose > nextPipe + 1);
|
|
118
|
+
if (matched) {
|
|
119
|
+
yield {
|
|
120
|
+
index: start,
|
|
121
|
+
raw: markdown.slice(start, nextClose + 2),
|
|
122
|
+
content: markdown.slice(contentStart, nextClose),
|
|
123
|
+
};
|
|
124
|
+
start = markdown.indexOf("[[", nextClose + 2);
|
|
125
|
+
} else {
|
|
126
|
+
start = markdown.indexOf("[[", start + 1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
88
130
|
|
|
89
131
|
/** Logseq embed: {{embed [[Page]]}} or {{embed ((block-id))}} */
|
|
90
132
|
const LOGSEQ_EMBED_REGEX =
|
|
@@ -385,12 +427,9 @@ export function parseLinks(
|
|
|
385
427
|
};
|
|
386
428
|
|
|
387
429
|
// Parse wiki links
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
while ((match = WIKI_LINK_REGEX.exec(markdown)) !== null) {
|
|
392
|
-
const startOffset = match.index;
|
|
393
|
-
const endOffset = startOffset + match[0].length;
|
|
430
|
+
for (const wiki of findWikiLinks(markdown)) {
|
|
431
|
+
const startOffset = wiki.index;
|
|
432
|
+
const endOffset = startOffset + wiki.raw.length;
|
|
394
433
|
|
|
395
434
|
// Skip [[target]] nested in Logseq alias syntax: [Display]([[target]])
|
|
396
435
|
if (
|
|
@@ -405,12 +444,10 @@ export function parseLinks(
|
|
|
405
444
|
continue;
|
|
406
445
|
}
|
|
407
446
|
|
|
408
|
-
const content = match[1];
|
|
409
|
-
if (!content) continue;
|
|
410
|
-
|
|
411
447
|
// Parse [[target|alias]] (and table-escaped [[target\|alias]]) format
|
|
412
|
-
const { target: targetPart, alias: aliasText } =
|
|
413
|
-
|
|
448
|
+
const { target: targetPart, alias: aliasText } = splitWikiLinkContent(
|
|
449
|
+
wiki.content
|
|
450
|
+
);
|
|
414
451
|
// Only set displayText if different from target
|
|
415
452
|
const displayText =
|
|
416
453
|
aliasText !== undefined && aliasText !== targetPart
|
|
@@ -421,11 +458,12 @@ export function parseLinks(
|
|
|
421
458
|
if (!trimmedTarget) {
|
|
422
459
|
continue;
|
|
423
460
|
}
|
|
424
|
-
pushWikiLink(
|
|
461
|
+
pushWikiLink(wiki.raw, trimmedTarget, startOffset, endOffset, displayText);
|
|
425
462
|
}
|
|
426
463
|
|
|
427
464
|
// Parse Logseq embeds as links
|
|
428
465
|
LOGSEQ_EMBED_REGEX.lastIndex = 0;
|
|
466
|
+
let match: RegExpExecArray | null;
|
|
429
467
|
|
|
430
468
|
while ((match = LOGSEQ_EMBED_REGEX.exec(markdown)) !== null) {
|
|
431
469
|
const startOffset = match.index;
|
package/src/ingestion/strip.ts
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
* Code regions come from the CommonMark + GFM parser. GFM table parsing is
|
|
15
15
|
* quadratic in the cells of one table (a 20,000-row table took minutes), so
|
|
16
16
|
* text with a table over the cell budget is parsed without the table
|
|
17
|
-
* extension
|
|
18
|
-
*
|
|
17
|
+
* extension and without inline links and inline HTML, which rescan a
|
|
18
|
+
* table-sized paragraph once per row: code spans, fences, indented code,
|
|
19
|
+
* blockquotes and lists keep their CommonMark meaning; table-cell
|
|
20
|
+
* boundaries, link destinations and HTML tags no longer hide a backtick.
|
|
19
21
|
*
|
|
20
22
|
* @module src/ingestion/strip
|
|
21
23
|
*/
|
|
@@ -59,9 +61,6 @@ export interface ExcludedRange {
|
|
|
59
61
|
/** Frontmatter at start of file (YAML between --- delimiters) */
|
|
60
62
|
const FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?(?:\r?\n)?---(?:\r?\n|$)/;
|
|
61
63
|
|
|
62
|
-
/** HTML comments */
|
|
63
|
-
const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g;
|
|
64
|
-
|
|
65
64
|
/**
|
|
66
65
|
* Most `|` characters one blank-line-delimited block may hold before the
|
|
67
66
|
* GFM table extension is left out of the parse. A GFM table only ends at a
|
|
@@ -132,13 +131,33 @@ const hasOversizedTable = (markdown: string): boolean => {
|
|
|
132
131
|
return false;
|
|
133
132
|
};
|
|
134
133
|
|
|
135
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Inline constructs left out with the table extension. Without table rules a
|
|
136
|
+
* large table is one paragraph, and these rescan it once per row: on every
|
|
137
|
+
* `]` link and image ends walk back through all the paragraph's earlier
|
|
138
|
+
* events looking for an opening `[` (an unmatched `]` per row made 8,000
|
|
139
|
+
* rows take 66 s), and inline HTML reads forward from every unclosed `<!--`
|
|
140
|
+
* to the paragraph's end. They only matter to code detection through a
|
|
141
|
+
* backtick inside a link destination or title or an inline HTML tag, which
|
|
142
|
+
* then reads as a code-span delimiter.
|
|
143
|
+
*/
|
|
144
|
+
const PARAGRAPH_RESCANNING_CONSTRUCTS = [
|
|
145
|
+
"labelEnd",
|
|
146
|
+
"gfmPotentialFootnoteCall",
|
|
147
|
+
"htmlText",
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* GFM without tables: every GFM construct except table rows and cells, and
|
|
152
|
+
* without the constructs in `PARAGRAPH_RESCANNING_CONSTRUCTS`.
|
|
153
|
+
*/
|
|
136
154
|
const GFM_WITHOUT_TABLES = {
|
|
137
155
|
extensions: [
|
|
138
156
|
gfmAutolinkLiteral(),
|
|
139
157
|
gfmFootnote(),
|
|
140
158
|
gfmStrikethrough(),
|
|
141
159
|
gfmTaskListItem(),
|
|
160
|
+
{ disable: { null: PARAGRAPH_RESCANNING_CONSTRUCTS } },
|
|
142
161
|
],
|
|
143
162
|
mdastExtensions: [
|
|
144
163
|
gfmAutolinkLiteralFromMarkdown(),
|
|
@@ -226,15 +245,15 @@ export function getExcludedRanges(markdown: string): ExcludedRange[] {
|
|
|
226
245
|
});
|
|
227
246
|
}
|
|
228
247
|
|
|
229
|
-
// 2. HTML comments
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
// 2. HTML comments: each `<!--` to the first `-->` after it. Once no
|
|
249
|
+
// `-->` follows, no later comment can close either, so the scan stops;
|
|
250
|
+
// the regex /<!--[\s\S]*?-->/g rescanned to the end from every opener.
|
|
251
|
+
let open = markdown.indexOf("<!--");
|
|
252
|
+
while (open !== -1) {
|
|
253
|
+
const close = markdown.indexOf("-->", open + 4);
|
|
254
|
+
if (close === -1) break;
|
|
255
|
+
ranges.push({ start: open, end: close + 3, kind: "html_comment" });
|
|
256
|
+
open = markdown.indexOf("<!--", close + 3);
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
// 3. Code spans and fenced or indented code blocks, from the parser.
|
|
@@ -281,24 +300,47 @@ export function isExcluded(
|
|
|
281
300
|
return false;
|
|
282
301
|
}
|
|
283
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Largest `end` among each prefix of a start-sorted range list, built once
|
|
305
|
+
* per list. Ranges can overlap (a comment around a code span), so a range
|
|
306
|
+
* list's ends are not sorted and the prefix maximum stands in for them.
|
|
307
|
+
*/
|
|
308
|
+
const prefixMaxEnds = new WeakMap<readonly ExcludedRange[], Int32Array>();
|
|
309
|
+
|
|
310
|
+
const getPrefixMaxEnds = (ranges: readonly ExcludedRange[]): Int32Array => {
|
|
311
|
+
let maxEnds = prefixMaxEnds.get(ranges);
|
|
312
|
+
if (maxEnds?.length !== ranges.length) {
|
|
313
|
+
maxEnds = new Int32Array(ranges.length);
|
|
314
|
+
let maxEnd = -1;
|
|
315
|
+
for (const [index, range] of ranges.entries()) {
|
|
316
|
+
maxEnd = Math.max(maxEnd, range.end);
|
|
317
|
+
maxEnds[index] = maxEnd;
|
|
318
|
+
}
|
|
319
|
+
prefixMaxEnds.set(ranges, maxEnds);
|
|
320
|
+
}
|
|
321
|
+
return maxEnds;
|
|
322
|
+
};
|
|
323
|
+
|
|
284
324
|
/**
|
|
285
325
|
* Check if a range [start, end) intersects any excluded range.
|
|
286
|
-
* More precise than isExcluded for multi-character matches.
|
|
326
|
+
* More precise than isExcluded for multi-character matches. Ranges must be
|
|
327
|
+
* sorted by start, as getExcludedRanges returns them; one lookup is a binary
|
|
328
|
+
* search, so checking every link of a large note stays linear.
|
|
287
329
|
*/
|
|
288
330
|
export function rangeIntersectsExcluded(
|
|
289
331
|
start: number,
|
|
290
332
|
end: number,
|
|
291
|
-
excludedRanges: ExcludedRange[]
|
|
333
|
+
excludedRanges: readonly ExcludedRange[]
|
|
292
334
|
): boolean {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
}
|
|
335
|
+
// Ranges starting before `end` form a prefix; one of them intersects
|
|
336
|
+
// [start, end) exactly when the largest end among them passes `start`.
|
|
337
|
+
let low = 0;
|
|
338
|
+
let high = excludedRanges.length;
|
|
339
|
+
while (low < high) {
|
|
340
|
+
const mid = (low + high) >>> 1;
|
|
341
|
+
if ((excludedRanges[mid]?.start ?? end) < end) low = mid + 1;
|
|
342
|
+
else high = mid;
|
|
302
343
|
}
|
|
303
|
-
return false;
|
|
344
|
+
if (low === 0) return false;
|
|
345
|
+
return (getPrefixMaxEnds(excludedRanges)[low - 1] ?? -1) > start;
|
|
304
346
|
}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
import type { ActivationStatus } from "../core/activation-status";
|
|
4
4
|
import type { HealthCheck } from "./status-model";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
classifyConnectorProof,
|
|
8
|
+
describeOmittedConnectorPairs,
|
|
9
|
+
omittedConnectorSummary,
|
|
10
|
+
} from "../core/activation-connector-health";
|
|
11
|
+
import { isEmptyActivationCollection } from "../core/activation-empty";
|
|
12
|
+
|
|
6
13
|
function countLabel(count: number, singular: string): string {
|
|
7
14
|
return `${count} ${count === 1 ? singular : `${singular}s`}`;
|
|
8
15
|
}
|
|
@@ -10,26 +17,42 @@ function countLabel(count: number, singular: string): string {
|
|
|
10
17
|
export function buildActivationCheck(
|
|
11
18
|
activation: ActivationStatus
|
|
12
19
|
): HealthCheck {
|
|
20
|
+
const ready = activation.collections.filter((collection) => collection.ready);
|
|
21
|
+
const empty = activation.collections.filter(isEmptyActivationCollection);
|
|
22
|
+
const emptyDetail =
|
|
23
|
+
empty.length > 0
|
|
24
|
+
? `${countLabel(empty.length, "folder")} ${empty.length === 1 ? "has" : "have"} no documents yet (${empty.map(({ collection }) => collection).join(", ")}): add files, then run update.`
|
|
25
|
+
: null;
|
|
13
26
|
if (activation.healthy) {
|
|
14
27
|
const semanticReasons = [
|
|
15
28
|
...new Set(
|
|
16
|
-
|
|
17
|
-
({ semanticAvailability }) => semanticAvailability.code
|
|
18
|
-
)
|
|
29
|
+
ready.map(({ semanticAvailability }) => semanticAvailability.code)
|
|
19
30
|
),
|
|
20
31
|
];
|
|
32
|
+
const semanticDetail =
|
|
33
|
+
ready.length > 0
|
|
34
|
+
? `Lexical search is proven. Semantic availability is separate (${semanticReasons.join(", ")}).`
|
|
35
|
+
: null;
|
|
21
36
|
return {
|
|
22
37
|
id: "retrieval-activation",
|
|
23
38
|
title: "Retrieval proof",
|
|
24
39
|
status: "ok",
|
|
25
|
-
summary:
|
|
26
|
-
|
|
40
|
+
summary:
|
|
41
|
+
ready.length === 0
|
|
42
|
+
? `No documents indexed yet in ${countLabel(empty.length, "folder")}`
|
|
43
|
+
: empty.length > 0
|
|
44
|
+
? `${countLabel(ready.length, "folder")} passed lexical retrieval; ${countLabel(empty.length, "folder")} with no documents yet`
|
|
45
|
+
: `${countLabel(ready.length, "folder")} passed lexical retrieval`,
|
|
46
|
+
detail: [semanticDetail, emptyDetail].filter(Boolean).join(" "),
|
|
27
47
|
actionLabel: "Run update",
|
|
28
48
|
actionKind: "sync",
|
|
29
49
|
};
|
|
30
50
|
}
|
|
31
51
|
|
|
32
|
-
const failed = activation.collections.filter(
|
|
52
|
+
const failed = activation.collections.filter(
|
|
53
|
+
(collection) =>
|
|
54
|
+
!collection.ready && !isEmptyActivationCollection(collection)
|
|
55
|
+
);
|
|
33
56
|
const first = failed[0];
|
|
34
57
|
const detail = first?.remediation
|
|
35
58
|
? `${first.collection}: ${first.remediation.stage}/${first.remediation.code}. Run: ${first.remediation.command}`
|
|
@@ -41,7 +64,7 @@ export function buildActivationCheck(
|
|
|
41
64
|
summary: activation.usable
|
|
42
65
|
? `${countLabel(failed.length, "folder")} failed lexical retrieval`
|
|
43
66
|
: "No folder passed lexical retrieval",
|
|
44
|
-
detail,
|
|
67
|
+
detail: [detail, emptyDetail].filter(Boolean).join(" "),
|
|
45
68
|
actionLabel: "Run update",
|
|
46
69
|
actionKind: "sync",
|
|
47
70
|
};
|
|
@@ -51,41 +74,46 @@ export function buildConnectorActivationCheck(
|
|
|
51
74
|
activation: ActivationStatus
|
|
52
75
|
): HealthCheck | null {
|
|
53
76
|
const { projected, total, truncated } = activation.connectorProjection;
|
|
54
|
-
const omitted =
|
|
77
|
+
const omitted = omittedConnectorSummary(activation);
|
|
55
78
|
const observed = activation.connectors.filter(
|
|
56
|
-
(
|
|
57
|
-
code !== "connector_not_configured" &&
|
|
58
|
-
code !== "target_runtime_unverifiable"
|
|
79
|
+
(connector) => classifyConnectorProof(connector) !== "notApplicable"
|
|
59
80
|
);
|
|
60
|
-
|
|
81
|
+
const omittedObserved = omitted
|
|
82
|
+
? omitted.passed + omitted.failed + omitted.incomplete
|
|
83
|
+
: 0;
|
|
84
|
+
if (observed.length === 0 && omittedObserved === 0 && omitted) {
|
|
61
85
|
return null;
|
|
62
86
|
}
|
|
63
87
|
const failed = observed.filter(({ status }) => status === "failed");
|
|
64
88
|
const incomplete = observed.filter(({ status }) => status !== "passed");
|
|
89
|
+
const failedCount = failed.length + (omitted?.failed ?? 0);
|
|
90
|
+
const incompleteCount =
|
|
91
|
+
incomplete.length + (omitted?.incomplete ?? 0) + (omitted?.failed ?? 0);
|
|
92
|
+
const passedCount =
|
|
93
|
+
observed.length - incomplete.length + (omitted?.passed ?? 0);
|
|
65
94
|
const first = failed[0] ?? incomplete[0] ?? observed[0];
|
|
66
95
|
const firstDetail = first
|
|
67
96
|
? `${first.target} / ${first.collection}: ${first.status}${first.code ? `/${first.code}` : ""}${first.remediation ? `. ${first.remediation}` : ""}`
|
|
68
97
|
: null;
|
|
69
|
-
const projectionDetail =
|
|
70
|
-
|
|
71
|
-
: null;
|
|
98
|
+
const projectionDetail = describeOmittedConnectorPairs(activation);
|
|
99
|
+
const listing = truncated ? ` (${projected} of ${total} checks listed)` : "";
|
|
72
100
|
return {
|
|
73
101
|
id: "connector-activation",
|
|
74
102
|
title: "Connector proof",
|
|
75
103
|
status:
|
|
76
|
-
|
|
104
|
+
failedCount > 0
|
|
77
105
|
? "error"
|
|
78
|
-
:
|
|
106
|
+
: incompleteCount > 0 || !omitted
|
|
79
107
|
? "warn"
|
|
80
108
|
: "ok",
|
|
81
109
|
summary:
|
|
82
|
-
|
|
83
|
-
? `${countLabel(
|
|
84
|
-
:
|
|
85
|
-
? `${countLabel(
|
|
86
|
-
:
|
|
87
|
-
? `${
|
|
88
|
-
: `${
|
|
110
|
+
failedCount > 0
|
|
111
|
+
? `${countLabel(failedCount, "connector proof")} failed${listing}`
|
|
112
|
+
: incompleteCount > 0
|
|
113
|
+
? `${countLabel(incompleteCount, "connector proof")} incomplete${listing}`
|
|
114
|
+
: omitted
|
|
115
|
+
? `${countLabel(passedCount, "connector proof")} passed${listing}`
|
|
116
|
+
: `${projected} of ${total} connector target/collection checks projected`,
|
|
89
117
|
detail: [projectionDetail, firstDetail].filter(Boolean).join(" "),
|
|
90
118
|
};
|
|
91
119
|
}
|
|
@@ -9,6 +9,11 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import type { AppStatusResponse } from "../../status-model";
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
describeConnectorSummary,
|
|
14
|
+
omittedConnectorSummary as summarizeOmittedConnectors,
|
|
15
|
+
} from "../../../core/activation-connector-health";
|
|
16
|
+
import { isEmptyActivationCollection } from "../../../core/activation-empty";
|
|
12
17
|
import { buildConnectorActivationCheck } from "../../activation-health";
|
|
13
18
|
import { Button } from "./ui/button";
|
|
14
19
|
import {
|
|
@@ -53,6 +58,7 @@ export function BootstrapStatus({
|
|
|
53
58
|
const omittedConnectorCount =
|
|
54
59
|
activation.connectorProjection.total -
|
|
55
60
|
activation.connectorProjection.projected;
|
|
61
|
+
const omittedConnectorSummary = summarizeOmittedConnectors(activation);
|
|
56
62
|
const connectorHealth = buildConnectorActivationCheck(activation);
|
|
57
63
|
const connectorsHealthy =
|
|
58
64
|
connectorHealth === null || connectorHealth.status === "ok";
|
|
@@ -147,9 +153,9 @@ export function BootstrapStatus({
|
|
|
147
153
|
<Card className="border-border/60 bg-card/70">
|
|
148
154
|
<CardHeader className="pb-3">
|
|
149
155
|
<div className="flex items-center gap-2">
|
|
150
|
-
{activation.healthy && connectorsHealthy ? (
|
|
156
|
+
{activation.healthy && activation.usable && connectorsHealthy ? (
|
|
151
157
|
<CheckCircle2Icon className="size-4 text-emerald-500" />
|
|
152
|
-
) : activation.usable ? (
|
|
158
|
+
) : activation.usable || activation.healthy ? (
|
|
153
159
|
<ServerCogIcon className="size-4 text-amber-500" />
|
|
154
160
|
) : (
|
|
155
161
|
<AlertCircleIcon className="size-4 text-destructive" />
|
|
@@ -158,7 +164,9 @@ export function BootstrapStatus({
|
|
|
158
164
|
</div>
|
|
159
165
|
<CardDescription>
|
|
160
166
|
{activation.healthy
|
|
161
|
-
?
|
|
167
|
+
? activation.usable
|
|
168
|
+
? "Lexical retrieval proven"
|
|
169
|
+
: "No documents indexed yet"
|
|
162
170
|
: activation.usable
|
|
163
171
|
? `Search usable in ${activation.collections.filter(({ ready }) => ready).length}/${activation.collections.length} folders`
|
|
164
172
|
: "Retrieval proof failed"}
|
|
@@ -179,7 +187,9 @@ export function BootstrapStatus({
|
|
|
179
187
|
<p className="text-muted-foreground text-xs">
|
|
180
188
|
{collection.ready
|
|
181
189
|
? `Lexical passed; semantic ${collection.semanticAvailability.code}`
|
|
182
|
-
:
|
|
190
|
+
: isEmptyActivationCollection(collection)
|
|
191
|
+
? "No documents yet. Add files, then run update."
|
|
192
|
+
: `${collection.remediation?.stage ?? "index"}/${collection.remediation?.code ?? "index_query_failed"}`}
|
|
183
193
|
</p>
|
|
184
194
|
{collection.remediation && (
|
|
185
195
|
<p className="mt-1 font-mono text-muted-foreground text-xs">
|
|
@@ -216,6 +226,9 @@ export function BootstrapStatus({
|
|
|
216
226
|
<p className="text-muted-foreground text-xs">
|
|
217
227
|
{omittedConnectorCount} additional target/collection checks
|
|
218
228
|
omitted from this bounded status view
|
|
229
|
+
{omittedConnectorSummary
|
|
230
|
+
? ` (${describeConnectorSummary(omittedConnectorSummary)})`
|
|
231
|
+
: ""}
|
|
219
232
|
</p>
|
|
220
233
|
)}
|
|
221
234
|
</div>
|
package/src/serve/status.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
|
|
15
15
|
import { getModelsCachePath } from "../app/constants";
|
|
16
16
|
import { buildContentTypeBoostStatus } from "../config/content-types";
|
|
17
|
+
import { isEmptyActivationCollection } from "../core/activation-empty";
|
|
17
18
|
import {
|
|
18
19
|
type ActivationStatus,
|
|
19
20
|
buildActivationStatus,
|
|
@@ -555,7 +556,13 @@ function buildOnboarding(
|
|
|
555
556
|
): AppStatusResponse["onboarding"] {
|
|
556
557
|
const foldersReady = activation.collections.length > 0;
|
|
557
558
|
const modelsReady = modelCheck.status === "ok";
|
|
558
|
-
|
|
559
|
+
// An empty folder is not a failure, but a workspace with no documents yet
|
|
560
|
+
// has not finished its first proof.
|
|
561
|
+
const indexedReady = activation.healthy && activation.usable;
|
|
562
|
+
const readyCount = activation.collections.filter(({ ready }) => ready).length;
|
|
563
|
+
const emptyCount = activation.collections.filter(
|
|
564
|
+
isEmptyActivationCollection
|
|
565
|
+
).length;
|
|
559
566
|
const semanticStates = [
|
|
560
567
|
...new Set(
|
|
561
568
|
activation.collections.map(
|
|
@@ -563,10 +570,22 @@ function buildOnboarding(
|
|
|
563
570
|
)
|
|
564
571
|
),
|
|
565
572
|
];
|
|
566
|
-
const failedActivation =
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
573
|
+
const failedActivation =
|
|
574
|
+
activation.collections.find(
|
|
575
|
+
(collection) =>
|
|
576
|
+
!collection.ready && !isEmptyActivationCollection(collection)
|
|
577
|
+
) ?? activation.collections.find(({ ready }) => !ready);
|
|
578
|
+
const activationDetail = !failedActivation?.remediation
|
|
579
|
+
? "Run the first sync to populate a searchable lexical index."
|
|
580
|
+
: isEmptyActivationCollection(failedActivation)
|
|
581
|
+
? `${failedActivation.collection} has no documents yet. Add files, then run: ${failedActivation.remediation.command}`
|
|
582
|
+
: `${failedActivation.collection}: ${failedActivation.remediation.stage}/${failedActivation.remediation.code}. Run: ${failedActivation.remediation.command}`;
|
|
583
|
+
const emptyNote =
|
|
584
|
+
emptyCount > 0
|
|
585
|
+
? ` ${summarizeCount(emptyCount, "folder")} with no documents yet.`
|
|
586
|
+
: "";
|
|
587
|
+
const passedFolders =
|
|
588
|
+
emptyCount > 0 ? "Every folder with documents" : "Every folder";
|
|
570
589
|
|
|
571
590
|
const steps = [
|
|
572
591
|
{
|
|
@@ -600,7 +619,7 @@ function buildOnboarding(
|
|
|
600
619
|
title: "Prove lexical retrieval",
|
|
601
620
|
status: indexedReady ? "complete" : foldersReady ? "current" : "upcoming",
|
|
602
621
|
detail: indexedReady
|
|
603
|
-
? `${summarizeCount(
|
|
622
|
+
? `${summarizeCount(readyCount, "folder")} passed a corpus-derived lexical proof.${emptyNote}`
|
|
604
623
|
: activationDetail,
|
|
605
624
|
action: "sync",
|
|
606
625
|
},
|
|
@@ -636,8 +655,8 @@ function buildOnboarding(
|
|
|
636
655
|
stage: "ready",
|
|
637
656
|
headline: "Workspace ready",
|
|
638
657
|
detail: modelsReady
|
|
639
|
-
?
|
|
640
|
-
:
|
|
658
|
+
? `${passedFolders} passed lexical retrieval.${emptyNote} Local model files are cached; semantic retrieval remains separate (${semanticStates.join(", ")}).`
|
|
659
|
+
: `${passedFolders} passed lexical retrieval.${emptyNote} Semantic retrieval remains an optional next step (${semanticStates.join(", ")}).`,
|
|
641
660
|
suggestedCollections: suggestions,
|
|
642
661
|
steps,
|
|
643
662
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
7fe15ce08a24f4dae4ac6694f62bb26d94bbc47d6abc09826f29267ace10558d gno-browser-clipper-v2.8.3.zip
|