@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import {
|
|
2
|
+
discoverArchiveTodaySnapshot,
|
|
3
|
+
type ArchiveTodayDependencies,
|
|
4
|
+
type ArchiveTodayDiscovery,
|
|
5
|
+
} from "./archive-today.js";
|
|
6
|
+
import {
|
|
7
|
+
createExactUrlSearchQuery,
|
|
8
|
+
type SearchProvider,
|
|
9
|
+
} from "./metadata-search.js";
|
|
10
|
+
import { assertSafeNetworkUrl } from "./network.js";
|
|
11
|
+
import {
|
|
12
|
+
parseArchiveTodayMementoUrl,
|
|
13
|
+
rankMetadataSearchResults,
|
|
14
|
+
type ArchiveTodayMemento,
|
|
15
|
+
type MetadataSearchResponse,
|
|
16
|
+
} from "./url-intelligence.js";
|
|
17
|
+
import {
|
|
18
|
+
createUrlMetadataDocument,
|
|
19
|
+
discoverSavedUrlRecords,
|
|
20
|
+
readUrlMetadataDocument,
|
|
21
|
+
writeUrlMetadataDocument,
|
|
22
|
+
type SavedUrlRecord,
|
|
23
|
+
type UrlMetadataArchive,
|
|
24
|
+
type UrlMetadataAttempt,
|
|
25
|
+
type UrlMetadataCandidate,
|
|
26
|
+
type UrlMetadataDocument,
|
|
27
|
+
} from "./url-metadata.js";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MAX_RESULTS = 20;
|
|
30
|
+
const DEFAULT_SEARCH_TIMEOUT_MS = 15_000;
|
|
31
|
+
const DEFAULT_NETWORK_VALIDATION_TIMEOUT_MS = 5_000;
|
|
32
|
+
const MAX_INTER_REQUEST_DELAY_MS = 60_000;
|
|
33
|
+
|
|
34
|
+
export type UrlMetadataBackfillOptions = {
|
|
35
|
+
readonly vaultRoot: string;
|
|
36
|
+
/** Re-run compatible existing sidecars. The default is a resumable skip. */
|
|
37
|
+
readonly refresh?: boolean;
|
|
38
|
+
/** Also ask Archive.today for its newest existing snapshot. This never creates a snapshot. */
|
|
39
|
+
readonly discoverArchives?: boolean;
|
|
40
|
+
readonly interRequestDelayMs?: number;
|
|
41
|
+
readonly maxResults?: number;
|
|
42
|
+
readonly searchTimeoutMs?: number;
|
|
43
|
+
readonly networkValidationTimeoutMs?: number;
|
|
44
|
+
readonly signal?: AbortSignal;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type UrlMetadataNetworkAssertion = (
|
|
48
|
+
url: URL,
|
|
49
|
+
allowPrivateNetwork: boolean,
|
|
50
|
+
timeoutMs?: number,
|
|
51
|
+
) => Promise<void>;
|
|
52
|
+
|
|
53
|
+
export type ArchiveTodayDiscoveryProvider = (
|
|
54
|
+
sourceUrl: string | URL,
|
|
55
|
+
dependencies?: ArchiveTodayDependencies,
|
|
56
|
+
) => Promise<ArchiveTodayDiscovery>;
|
|
57
|
+
|
|
58
|
+
export type UrlMetadataBackfillDependencies = {
|
|
59
|
+
readonly searchProvider: SearchProvider;
|
|
60
|
+
readonly discoverRecords?: typeof discoverSavedUrlRecords;
|
|
61
|
+
readonly readMetadata?: typeof readUrlMetadataDocument;
|
|
62
|
+
readonly writeMetadata?: typeof writeUrlMetadataDocument;
|
|
63
|
+
readonly assertNetworkUrl?: UrlMetadataNetworkAssertion;
|
|
64
|
+
readonly discoverArchive?: ArchiveTodayDiscoveryProvider;
|
|
65
|
+
readonly now?: () => Date;
|
|
66
|
+
readonly sleep?: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type UrlMetadataBackfillAction = "skipped" | "written" | "unchanged";
|
|
70
|
+
|
|
71
|
+
export type UrlMetadataBackfillItem = {
|
|
72
|
+
readonly articleId: string;
|
|
73
|
+
readonly subjectUrl: string;
|
|
74
|
+
readonly sidecarPath: string;
|
|
75
|
+
readonly action: UrlMetadataBackfillAction;
|
|
76
|
+
readonly status: UrlMetadataDocument["status"];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type UrlMetadataBackfillStatusCounts = {
|
|
80
|
+
readonly matched: number;
|
|
81
|
+
readonly notFound: number;
|
|
82
|
+
readonly partial: number;
|
|
83
|
+
readonly unavailable: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type UrlMetadataBackfillReport = {
|
|
87
|
+
readonly generatedAt: string;
|
|
88
|
+
readonly totalRecords: number;
|
|
89
|
+
readonly processedRecords: number;
|
|
90
|
+
readonly skippedRecords: number;
|
|
91
|
+
readonly writtenRecords: number;
|
|
92
|
+
readonly unchangedRecords: number;
|
|
93
|
+
readonly remainingRecords: number;
|
|
94
|
+
readonly aborted: boolean;
|
|
95
|
+
readonly statusCounts: UrlMetadataBackfillStatusCounts;
|
|
96
|
+
readonly items: readonly UrlMetadataBackfillItem[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type ResolvedOptions = {
|
|
100
|
+
readonly vaultRoot: string;
|
|
101
|
+
readonly refresh: boolean;
|
|
102
|
+
readonly discoverArchives: boolean;
|
|
103
|
+
readonly interRequestDelayMs: number;
|
|
104
|
+
readonly maxResults: number;
|
|
105
|
+
readonly searchTimeoutMs: number;
|
|
106
|
+
readonly networkValidationTimeoutMs: number;
|
|
107
|
+
readonly signal?: AbortSignal;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type ResolvedDependencies = {
|
|
111
|
+
readonly searchProvider: SearchProvider;
|
|
112
|
+
readonly discoverRecords: typeof discoverSavedUrlRecords;
|
|
113
|
+
readonly readMetadata: typeof readUrlMetadataDocument;
|
|
114
|
+
readonly writeMetadata: typeof writeUrlMetadataDocument;
|
|
115
|
+
readonly assertNetworkUrl: UrlMetadataNetworkAssertion;
|
|
116
|
+
readonly discoverArchive: ArchiveTodayDiscoveryProvider;
|
|
117
|
+
readonly now: () => Date;
|
|
118
|
+
readonly sleep: (milliseconds: number, signal?: AbortSignal) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
type RecordMetadata = {
|
|
122
|
+
readonly attempts: UrlMetadataAttempt[];
|
|
123
|
+
readonly candidates: UrlMetadataCandidate[];
|
|
124
|
+
readonly archives: Map<string, UrlMetadataArchive>;
|
|
125
|
+
readonly warnings: string[];
|
|
126
|
+
enginesQueried: readonly string[];
|
|
127
|
+
enginesFailed: readonly string[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function compareText(left: string, right: string): number {
|
|
131
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function boundedInteger(
|
|
135
|
+
value: number | undefined,
|
|
136
|
+
fallback: number,
|
|
137
|
+
minimum: number,
|
|
138
|
+
maximum: number,
|
|
139
|
+
label: string,
|
|
140
|
+
): number {
|
|
141
|
+
const selected = value ?? fallback;
|
|
142
|
+
if (!Number.isSafeInteger(selected) || selected < minimum || selected > maximum) {
|
|
143
|
+
throw new TypeError(`${label} must be an integer from ${minimum} through ${maximum}.`);
|
|
144
|
+
}
|
|
145
|
+
return selected;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveOptions(options: UrlMetadataBackfillOptions): ResolvedOptions {
|
|
149
|
+
if (typeof options.vaultRoot !== "string" || options.vaultRoot.trim() === "") {
|
|
150
|
+
throw new TypeError("The URL metadata backfill vault root must be a non-empty path.");
|
|
151
|
+
}
|
|
152
|
+
return Object.freeze({
|
|
153
|
+
vaultRoot: options.vaultRoot,
|
|
154
|
+
refresh: options.refresh ?? false,
|
|
155
|
+
discoverArchives: options.discoverArchives ?? false,
|
|
156
|
+
interRequestDelayMs: boundedInteger(
|
|
157
|
+
options.interRequestDelayMs,
|
|
158
|
+
0,
|
|
159
|
+
0,
|
|
160
|
+
MAX_INTER_REQUEST_DELAY_MS,
|
|
161
|
+
"URL metadata backfill inter-request delay",
|
|
162
|
+
),
|
|
163
|
+
maxResults: boundedInteger(options.maxResults, DEFAULT_MAX_RESULTS, 1, 20, "URL metadata backfill result limit"),
|
|
164
|
+
searchTimeoutMs: boundedInteger(
|
|
165
|
+
options.searchTimeoutMs,
|
|
166
|
+
DEFAULT_SEARCH_TIMEOUT_MS,
|
|
167
|
+
500,
|
|
168
|
+
15_000,
|
|
169
|
+
"URL metadata backfill search timeout",
|
|
170
|
+
),
|
|
171
|
+
networkValidationTimeoutMs: boundedInteger(
|
|
172
|
+
options.networkValidationTimeoutMs,
|
|
173
|
+
DEFAULT_NETWORK_VALIDATION_TIMEOUT_MS,
|
|
174
|
+
250,
|
|
175
|
+
60_000,
|
|
176
|
+
"URL metadata backfill network validation timeout",
|
|
177
|
+
),
|
|
178
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function abortAwareSleep(milliseconds: number, signal?: AbortSignal): Promise<void> {
|
|
183
|
+
if (milliseconds === 0) return Promise.resolve();
|
|
184
|
+
const abortError = (): Error => signal?.reason instanceof Error
|
|
185
|
+
? signal.reason
|
|
186
|
+
: new Error("URL metadata backfill aborted.");
|
|
187
|
+
if (signal?.aborted === true) return Promise.reject(abortError());
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const timeout = setTimeout(finish, milliseconds);
|
|
190
|
+
const abort = (): void => finish(abortError());
|
|
191
|
+
function finish(error?: Error): void {
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
signal?.removeEventListener("abort", abort);
|
|
194
|
+
if (error === undefined) resolve();
|
|
195
|
+
else reject(error);
|
|
196
|
+
}
|
|
197
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveDependencies(dependencies: UrlMetadataBackfillDependencies): ResolvedDependencies {
|
|
202
|
+
if (typeof dependencies.searchProvider !== "function") {
|
|
203
|
+
throw new TypeError("URL metadata backfill requires a search provider.");
|
|
204
|
+
}
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
searchProvider: dependencies.searchProvider,
|
|
207
|
+
discoverRecords: dependencies.discoverRecords ?? discoverSavedUrlRecords,
|
|
208
|
+
readMetadata: dependencies.readMetadata ?? readUrlMetadataDocument,
|
|
209
|
+
writeMetadata: dependencies.writeMetadata ?? writeUrlMetadataDocument,
|
|
210
|
+
assertNetworkUrl: dependencies.assertNetworkUrl ?? assertSafeNetworkUrl,
|
|
211
|
+
discoverArchive: dependencies.discoverArchive ?? discoverArchiveTodaySnapshot,
|
|
212
|
+
now: dependencies.now ?? (() => new Date()),
|
|
213
|
+
sleep: dependencies.sleep ?? abortAwareSleep,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function canonicalNow(now: () => Date): Date {
|
|
218
|
+
let value: Date;
|
|
219
|
+
try {
|
|
220
|
+
value = now();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new Error("URL metadata backfill could not read its clock.", { cause: error });
|
|
223
|
+
}
|
|
224
|
+
if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
|
|
225
|
+
throw new TypeError("URL metadata backfill now must return a valid Date.");
|
|
226
|
+
}
|
|
227
|
+
return new Date(value.getTime());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sortedInventory(records: readonly SavedUrlRecord[]): readonly SavedUrlRecord[] {
|
|
231
|
+
const sorted = [...records].sort((left, right) =>
|
|
232
|
+
compareText(left.articleId, right.articleId)
|
|
233
|
+
|| compareText(left.subjectUrl, right.subjectUrl)
|
|
234
|
+
|| compareText(left.markdownPath, right.markdownPath));
|
|
235
|
+
const sidecars = new Set<string>();
|
|
236
|
+
for (const record of sorted) {
|
|
237
|
+
if (sidecars.has(record.sidecarPath)) {
|
|
238
|
+
throw new Error(`URL metadata inventory repeats a sidecar path: ${record.sidecarPath}`);
|
|
239
|
+
}
|
|
240
|
+
sidecars.add(record.sidecarPath);
|
|
241
|
+
}
|
|
242
|
+
return Object.freeze(sorted);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isMissingFile(error: unknown): boolean {
|
|
246
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
247
|
+
&& (error as { readonly code?: unknown }).code === "ENOENT";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isAborted(signal: AbortSignal | undefined): boolean {
|
|
251
|
+
return signal?.aborted === true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function readExisting(
|
|
255
|
+
saved: SavedUrlRecord,
|
|
256
|
+
readMetadata: typeof readUrlMetadataDocument,
|
|
257
|
+
): UrlMetadataDocument | null {
|
|
258
|
+
try {
|
|
259
|
+
return readMetadata(saved);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (isMissingFile(error)) return null;
|
|
262
|
+
// A malformed, mismatched, linked, or otherwise unsafe sidecar is never
|
|
263
|
+
// treated as absent, even during an explicit refresh.
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function sortedStrings(values: readonly string[]): readonly string[] {
|
|
269
|
+
return Object.freeze([...values].sort(compareText));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function addArchive(
|
|
273
|
+
archives: Map<string, UrlMetadataArchive>,
|
|
274
|
+
memento: ArchiveTodayMemento,
|
|
275
|
+
discovery: UrlMetadataArchive["discovery"],
|
|
276
|
+
): void {
|
|
277
|
+
// A timestamp is Archive.today's capture identity for one already-bound
|
|
278
|
+
// original. This also deduplicates aliases for the same snapshot.
|
|
279
|
+
const candidate: UrlMetadataArchive = Object.freeze({
|
|
280
|
+
url: memento.url,
|
|
281
|
+
capturedAt: memento.capturedAt,
|
|
282
|
+
discovery,
|
|
283
|
+
});
|
|
284
|
+
const previous = archives.get(memento.timestamp);
|
|
285
|
+
if (
|
|
286
|
+
previous === undefined
|
|
287
|
+
|| (previous.discovery === "metadata-search" && discovery === "newest")
|
|
288
|
+
|| (previous.discovery === discovery && compareText(candidate.url, previous.url) < 0)
|
|
289
|
+
) archives.set(memento.timestamp, candidate);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function collectSuccessfulSearch(
|
|
293
|
+
subjectUrl: string,
|
|
294
|
+
query: string,
|
|
295
|
+
response: MetadataSearchResponse,
|
|
296
|
+
now: Date,
|
|
297
|
+
metadata: RecordMetadata,
|
|
298
|
+
resultLimit: number,
|
|
299
|
+
): void {
|
|
300
|
+
if (response.query !== query) {
|
|
301
|
+
metadata.attempts.push(Object.freeze({
|
|
302
|
+
provider: "metadata-search-engine-rs",
|
|
303
|
+
outcome: "failed",
|
|
304
|
+
message: "Metadata search returned a response for a different query.",
|
|
305
|
+
}));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
metadata.enginesQueried = sortedStrings(response.enginesQueried);
|
|
310
|
+
metadata.enginesFailed = sortedStrings(response.enginesFailed);
|
|
311
|
+
const ranked = rankMetadataSearchResults(response.results, { targetUrl: subjectUrl, limit: resultLimit });
|
|
312
|
+
let archiveMatches = 0;
|
|
313
|
+
let discarded = 0;
|
|
314
|
+
for (const result of ranked) {
|
|
315
|
+
if (result.exactTarget) {
|
|
316
|
+
metadata.candidates.push(Object.freeze({
|
|
317
|
+
title: result.title,
|
|
318
|
+
url: result.sourceIdentity,
|
|
319
|
+
snippet: result.snippet,
|
|
320
|
+
engines: sortedStrings(result.engines),
|
|
321
|
+
score: result.score,
|
|
322
|
+
}));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const memento = parseArchiveTodayMementoUrl(result.url, { originalUrl: subjectUrl, now });
|
|
327
|
+
addArchive(metadata.archives, memento, "metadata-search");
|
|
328
|
+
archiveMatches += 1;
|
|
329
|
+
} catch {
|
|
330
|
+
discarded += 1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (discarded > 0) {
|
|
335
|
+
metadata.warnings.push(`Discarded ${discarded} search result${discarded === 1 ? "" : "s"} without an exact source binding.`);
|
|
336
|
+
}
|
|
337
|
+
const unavailable = response.engineStatus === "unavailable"
|
|
338
|
+
|| response.enginesFailed.length === response.enginesQueried.length;
|
|
339
|
+
const partial = !unavailable && (response.engineStatus === "partial" || response.enginesFailed.length > 0);
|
|
340
|
+
const usefulMatches = metadata.candidates.length + archiveMatches;
|
|
341
|
+
metadata.attempts.push(Object.freeze({
|
|
342
|
+
provider: "metadata-search-engine-rs",
|
|
343
|
+
outcome: unavailable ? "failed" : partial ? "partial" : usefulMatches > 0 ? "succeeded" : "not-found",
|
|
344
|
+
message: unavailable
|
|
345
|
+
? "Metadata search failed because all queried engines were unavailable."
|
|
346
|
+
: partial
|
|
347
|
+
? `Metadata search completed with ${response.enginesFailed.length} failed engine${response.enginesFailed.length === 1 ? "" : "s"}.`
|
|
348
|
+
: usefulMatches > 0
|
|
349
|
+
? `Metadata search returned ${metadata.candidates.length} exact source match${metadata.candidates.length === 1 ? "" : "es"} and ${archiveMatches} bound archive match${archiveMatches === 1 ? "" : "es"}.`
|
|
350
|
+
: "Metadata search returned no exact source or bound archive matches.",
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function addSearchFailure(metadata: RecordMetadata, category: string): void {
|
|
355
|
+
metadata.attempts.push(Object.freeze({
|
|
356
|
+
provider: "metadata-search-engine-rs",
|
|
357
|
+
outcome: "failed",
|
|
358
|
+
message: `Metadata search failed (${category}).`,
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function runArchiveDiscovery(
|
|
363
|
+
saved: SavedUrlRecord,
|
|
364
|
+
now: Date,
|
|
365
|
+
discoverArchive: ArchiveTodayDiscoveryProvider,
|
|
366
|
+
metadata: RecordMetadata,
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
let outcome: ArchiveTodayDiscovery;
|
|
369
|
+
try {
|
|
370
|
+
outcome = await discoverArchive(saved.subjectUrl, { now: () => new Date(now.getTime()) });
|
|
371
|
+
} catch {
|
|
372
|
+
metadata.attempts.push(Object.freeze({
|
|
373
|
+
provider: "archive-today",
|
|
374
|
+
outcome: "failed",
|
|
375
|
+
message: "Archive.today discovery failed.",
|
|
376
|
+
}));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (outcome.status === "found") {
|
|
380
|
+
try {
|
|
381
|
+
const memento = parseArchiveTodayMementoUrl(outcome.snapshot.url, {
|
|
382
|
+
originalUrl: saved.subjectUrl,
|
|
383
|
+
now,
|
|
384
|
+
});
|
|
385
|
+
addArchive(metadata.archives, memento, "newest");
|
|
386
|
+
metadata.attempts.push(Object.freeze({
|
|
387
|
+
provider: "archive-today",
|
|
388
|
+
outcome: "succeeded",
|
|
389
|
+
message: "Archive.today returned a validated newest snapshot.",
|
|
390
|
+
}));
|
|
391
|
+
} catch {
|
|
392
|
+
metadata.attempts.push(Object.freeze({
|
|
393
|
+
provider: "archive-today",
|
|
394
|
+
outcome: "failed",
|
|
395
|
+
message: "Archive.today returned an invalid newest snapshot.",
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (outcome.status === "not-found") {
|
|
401
|
+
metadata.attempts.push(Object.freeze({
|
|
402
|
+
provider: "archive-today",
|
|
403
|
+
outcome: "not-found",
|
|
404
|
+
message: "Archive.today has no discoverable snapshot for this source.",
|
|
405
|
+
}));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (outcome.status === "throttled") {
|
|
409
|
+
metadata.attempts.push(Object.freeze({
|
|
410
|
+
provider: "archive-today",
|
|
411
|
+
outcome: "failed",
|
|
412
|
+
message: "Archive.today throttled snapshot discovery.",
|
|
413
|
+
}));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
metadata.attempts.push(Object.freeze({
|
|
417
|
+
provider: "archive-today",
|
|
418
|
+
outcome: "failed",
|
|
419
|
+
message: `Archive.today snapshot discovery was unavailable (${outcome.reason}).`,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function newRecordMetadata(): RecordMetadata {
|
|
424
|
+
return {
|
|
425
|
+
attempts: [],
|
|
426
|
+
candidates: [],
|
|
427
|
+
archives: new Map(),
|
|
428
|
+
warnings: [],
|
|
429
|
+
enginesQueried: Object.freeze([]),
|
|
430
|
+
enginesFailed: Object.freeze([]),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function skipUnsafeProviders(metadata: RecordMetadata, includeArchive: boolean, reason: "network" | "query"): void {
|
|
435
|
+
const explanation = reason === "network"
|
|
436
|
+
? "the source failed network-safety validation"
|
|
437
|
+
: "the source could not form a disclosure-safe exact query";
|
|
438
|
+
metadata.attempts.push(Object.freeze({
|
|
439
|
+
provider: "metadata-search-engine-rs",
|
|
440
|
+
outcome: "skipped",
|
|
441
|
+
message: `Metadata search was skipped because ${explanation}.`,
|
|
442
|
+
}));
|
|
443
|
+
if (includeArchive) {
|
|
444
|
+
metadata.attempts.push(Object.freeze({
|
|
445
|
+
provider: "archive-today",
|
|
446
|
+
outcome: "skipped",
|
|
447
|
+
message: `Archive.today discovery was skipped because ${explanation}.`,
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
metadata.warnings.push(`No URL was disclosed because ${explanation}.`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function item(saved: SavedUrlRecord, action: UrlMetadataBackfillAction, document: UrlMetadataDocument): UrlMetadataBackfillItem {
|
|
454
|
+
return Object.freeze({
|
|
455
|
+
articleId: saved.articleId,
|
|
456
|
+
subjectUrl: saved.subjectUrl,
|
|
457
|
+
sidecarPath: saved.sidecarPath,
|
|
458
|
+
action,
|
|
459
|
+
status: document.status,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function report(
|
|
464
|
+
generatedAt: string,
|
|
465
|
+
totalRecords: number,
|
|
466
|
+
items: readonly UrlMetadataBackfillItem[],
|
|
467
|
+
interrupted: boolean,
|
|
468
|
+
): UrlMetadataBackfillReport {
|
|
469
|
+
const skippedRecords = items.filter(({ action }) => action === "skipped").length;
|
|
470
|
+
const writtenRecords = items.filter(({ action }) => action === "written").length;
|
|
471
|
+
const unchangedRecords = items.filter(({ action }) => action === "unchanged").length;
|
|
472
|
+
const counts: UrlMetadataBackfillStatusCounts = Object.freeze({
|
|
473
|
+
matched: items.filter(({ status }) => status === "matched").length,
|
|
474
|
+
notFound: items.filter(({ status }) => status === "not-found").length,
|
|
475
|
+
partial: items.filter(({ status }) => status === "partial").length,
|
|
476
|
+
unavailable: items.filter(({ status }) => status === "unavailable").length,
|
|
477
|
+
});
|
|
478
|
+
return Object.freeze({
|
|
479
|
+
generatedAt,
|
|
480
|
+
totalRecords,
|
|
481
|
+
processedRecords: writtenRecords + unchangedRecords,
|
|
482
|
+
skippedRecords,
|
|
483
|
+
writtenRecords,
|
|
484
|
+
unchangedRecords,
|
|
485
|
+
remainingRecords: totalRecords - items.length,
|
|
486
|
+
aborted: interrupted,
|
|
487
|
+
statusCounts: counts,
|
|
488
|
+
items: Object.freeze([...items]),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Backfill tool-owned URL metadata sidecars one record and one outbound request
|
|
494
|
+
* at a time. Provider failures are recorded; filesystem-integrity failures are
|
|
495
|
+
* deliberately allowed to stop the run.
|
|
496
|
+
*/
|
|
497
|
+
export async function backfillSavedUrlMetadata(
|
|
498
|
+
options: UrlMetadataBackfillOptions,
|
|
499
|
+
dependencies: UrlMetadataBackfillDependencies,
|
|
500
|
+
): Promise<UrlMetadataBackfillReport> {
|
|
501
|
+
const resolvedOptions = resolveOptions(options);
|
|
502
|
+
const resolved = resolveDependencies(dependencies);
|
|
503
|
+
const now = canonicalNow(resolved.now);
|
|
504
|
+
const generatedAt = now.toISOString();
|
|
505
|
+
const records = sortedInventory(resolved.discoverRecords(resolvedOptions.vaultRoot));
|
|
506
|
+
const items: UrlMetadataBackfillItem[] = [];
|
|
507
|
+
let interrupted = isAborted(resolvedOptions.signal);
|
|
508
|
+
let outboundRequestStarted = false;
|
|
509
|
+
|
|
510
|
+
const waitForRequestSlot = async (): Promise<boolean> => {
|
|
511
|
+
if (isAborted(resolvedOptions.signal)) return false;
|
|
512
|
+
if (outboundRequestStarted && resolvedOptions.interRequestDelayMs > 0) {
|
|
513
|
+
try {
|
|
514
|
+
await resolved.sleep(resolvedOptions.interRequestDelayMs, resolvedOptions.signal);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (isAborted(resolvedOptions.signal)) return false;
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
if (isAborted(resolvedOptions.signal)) return false;
|
|
520
|
+
}
|
|
521
|
+
outboundRequestStarted = true;
|
|
522
|
+
return true;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
for (const saved of records) {
|
|
526
|
+
if (interrupted || isAborted(resolvedOptions.signal)) {
|
|
527
|
+
interrupted = true;
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
const existing = readExisting(saved, resolved.readMetadata);
|
|
531
|
+
if (existing !== null && !resolvedOptions.refresh) {
|
|
532
|
+
items.push(item(saved, "skipped", existing));
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const metadata = newRecordMetadata();
|
|
537
|
+
let safeForDisclosure = true;
|
|
538
|
+
try {
|
|
539
|
+
await resolved.assertNetworkUrl(
|
|
540
|
+
new URL(saved.subjectUrl),
|
|
541
|
+
false,
|
|
542
|
+
resolvedOptions.networkValidationTimeoutMs,
|
|
543
|
+
);
|
|
544
|
+
} catch {
|
|
545
|
+
safeForDisclosure = false;
|
|
546
|
+
skipUnsafeProviders(metadata, resolvedOptions.discoverArchives, "network");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const query = safeForDisclosure ? createExactUrlSearchQuery(saved.subjectUrl) : null;
|
|
550
|
+
if (safeForDisclosure && query === null) {
|
|
551
|
+
safeForDisclosure = false;
|
|
552
|
+
skipUnsafeProviders(metadata, resolvedOptions.discoverArchives, "query");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (safeForDisclosure && query !== null) {
|
|
556
|
+
if (!await waitForRequestSlot()) {
|
|
557
|
+
interrupted = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
const outcome = await resolved.searchProvider({
|
|
562
|
+
query,
|
|
563
|
+
maxResults: resolvedOptions.maxResults,
|
|
564
|
+
timeoutMs: resolvedOptions.searchTimeoutMs,
|
|
565
|
+
...(resolvedOptions.signal === undefined ? {} : { signal: resolvedOptions.signal }),
|
|
566
|
+
});
|
|
567
|
+
if (isAborted(resolvedOptions.signal)) {
|
|
568
|
+
interrupted = true;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
if (outcome.status === "failure") addSearchFailure(metadata, outcome.category);
|
|
572
|
+
else collectSuccessfulSearch(
|
|
573
|
+
saved.subjectUrl,
|
|
574
|
+
query,
|
|
575
|
+
outcome.response,
|
|
576
|
+
now,
|
|
577
|
+
metadata,
|
|
578
|
+
resolvedOptions.maxResults,
|
|
579
|
+
);
|
|
580
|
+
} catch {
|
|
581
|
+
if (isAborted(resolvedOptions.signal)) {
|
|
582
|
+
interrupted = true;
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
addSearchFailure(metadata, "provider-threw");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (resolvedOptions.discoverArchives) {
|
|
589
|
+
if (!await waitForRequestSlot()) {
|
|
590
|
+
interrupted = true;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
await runArchiveDiscovery(saved, now, resolved.discoverArchive, metadata);
|
|
594
|
+
if (isAborted(resolvedOptions.signal)) {
|
|
595
|
+
interrupted = true;
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const document = createUrlMetadataDocument({
|
|
602
|
+
subjectUrl: saved.subjectUrl,
|
|
603
|
+
generatedAt,
|
|
604
|
+
enginesQueried: metadata.enginesQueried,
|
|
605
|
+
enginesFailed: metadata.enginesFailed,
|
|
606
|
+
attempts: metadata.attempts,
|
|
607
|
+
candidates: metadata.candidates,
|
|
608
|
+
archives: [...metadata.archives.values()],
|
|
609
|
+
warnings: metadata.warnings,
|
|
610
|
+
});
|
|
611
|
+
const write = resolved.writeMetadata(saved, document);
|
|
612
|
+
items.push(item(saved, write.changed ? "written" : "unchanged", document));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return report(generatedAt, records.length, items, interrupted);
|
|
616
|
+
}
|