@hraness/wordcell 0.24.0 → 0.25.0

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.
Files changed (49) hide show
  1. package/README.md +14 -12
  2. package/dist/authoring.js +2 -1
  3. package/dist/capture.js +2 -2
  4. package/dist/cli.js +12 -11
  5. package/dist/clip/cli.js +2 -2
  6. package/dist/clip/defuddle-worker.js +87 -2
  7. package/dist/clip/jobs.js +5 -1
  8. package/dist/evaluation-builder.js +5 -4
  9. package/dist/evaluation-kb.js +5 -4
  10. package/dist/graph-authority.js +2 -1
  11. package/dist/graph-percolation.js +3 -2
  12. package/dist/{index-44kw9tr8.js → index-1wccy765.js} +18 -12
  13. package/dist/{index-1er88ckw.js → index-3gc2yk4k.js} +10 -5
  14. package/dist/{index-0eacgvpv.js → index-47893rgw.js} +22 -8
  15. package/dist/{index-mvs6f7ne.js → index-59bcxqee.js} +1 -1
  16. package/dist/{index-1w0sp1q7.js → index-7drercgk.js} +1 -1
  17. package/dist/{index-tcaq1c7f.js → index-e26mdwxz.js} +1 -1
  18. package/dist/{index-6jcz0m1c.js → index-hxyjer3x.js} +52 -1
  19. package/dist/{index-f184qyh1.js → index-hz2wyggk.js} +1 -1
  20. package/dist/{index-5d1rd5v9.js → index-mcpppwbq.js} +2 -2
  21. package/dist/index-qkesh4c6.js +7 -0
  22. package/dist/{index-b0khj0h5.js → index-qt2zwza8.js} +6 -1
  23. package/dist/{index-2fmnttca.js → index-t3abkwk4.js} +1 -1
  24. package/dist/index.js +6 -5
  25. package/dist/portfolio.js +5 -4
  26. package/dist/sdk.js +4 -3
  27. package/dist/{support-1yhv6vm6.js → support-adj7bbge.js} +11 -10
  28. package/dist/url-intelligence.js +3 -2
  29. package/dist/workflows/decision-context.js +5 -4
  30. package/dist/workflows/index.js +5 -4
  31. package/package.json +13 -8
  32. package/skills/wordcell/SKILL.md +1 -1
  33. package/skills/wordcell/references/publish.md +2 -1
  34. package/src/authoring-platform.ts +3 -0
  35. package/src/cli-program.ts +3 -3
  36. package/src/clip/jobs.ts +2 -1
  37. package/src/clip/metadata-search-tool/Cargo.lock +12 -0
  38. package/src/clip/metadata-search-tool/Cargo.toml +3 -0
  39. package/src/clip/metadata-search-tool/runner.ts +4 -1
  40. package/src/clip/metadata-search-tool/src/bing.rs +539 -0
  41. package/src/clip/metadata-search-tool/src/duckduckgo.rs +562 -0
  42. package/src/clip/metadata-search-tool/src/main.rs +263 -24
  43. package/src/clip/metadata-search-tool/src/openalex.rs +557 -0
  44. package/src/clip/metadata-search.ts +22 -4
  45. package/src/clip/url-intelligence.ts +54 -1
  46. package/src/clip/url-metadata.ts +2 -1
  47. package/src/directory-sync.ts +9 -0
  48. package/src/graph-authority.ts +5 -2
  49. package/src/url-intelligence.ts +2 -0
@@ -35,12 +35,33 @@ export type MetadataSearchResult = {
35
35
  readonly score: number;
36
36
  };
37
37
 
38
+ export const METADATA_SEARCH_FAILURE_CODES = [
39
+ "timeout", "http", "http-body", "http-status", "http-forbidden", "http-rate-limited", "http-redirect",
40
+ "parse", "unverified-empty", "challenge", "content-type", "content-encoding", "body-limit",
41
+ "body-encoding", "result-limit", "unrecognized-html", "query-mismatch",
42
+ ] as const;
43
+ export type MetadataSearchFailureCode = (typeof METADATA_SEARCH_FAILURE_CODES)[number];
44
+ export type MetadataSearchEngineFailure = {
45
+ readonly engine: string;
46
+ readonly code: MetadataSearchFailureCode;
47
+ };
48
+
49
+ export type MetadataSearchEngineUsage = {
50
+ readonly engine: "openalex";
51
+ readonly reportedCostUsd: number;
52
+ readonly billingStatus: "not-established";
53
+ };
54
+
38
55
  export type MetadataSearchResponse = {
39
56
  readonly query: string;
40
57
  readonly results: readonly MetadataSearchResult[];
41
58
  readonly enginesQueried: readonly string[];
42
59
  readonly enginesFailed: readonly string[];
43
60
  readonly engineStatus: MetadataSearchEngineStatus;
61
+ /** Absent for older providers; present entries cover every failed engine exactly once. */
62
+ readonly engineFailures?: readonly MetadataSearchEngineFailure[];
63
+ /** Provider-reported telemetry; this does not establish a user charge. */
64
+ readonly engineUsage?: readonly MetadataSearchEngineUsage[];
44
65
  };
45
66
 
46
67
  export type RankMetadataSearchOptions = {
@@ -246,7 +267,7 @@ function parseMetadataResult(
246
267
  /** Strictly parse one successful metadata-search-engine-rs `GET /search` body. */
247
268
  export function parseMetadataSearchResponse(value: unknown): MetadataSearchResponse {
248
269
  const input = record(value, "Metadata search response");
249
- strictKeys(input, ["query", "results", "engines_queried", "engines_failed"], "Metadata search response");
270
+ strictKeys(input, ["query", "results", "engines_queried", "engines_failed", "engine_failures", "engine_usage"], "Metadata search response");
250
271
  const budget: TextBudget = { bytes: 0 };
251
272
  const query = boundedString(input.query, "Metadata search response.query", MAX_METADATA_SEARCH_QUERY_UTF8_BYTES, budget);
252
273
  if (query === "" || query !== query.trim() || hasUnsafeControls(query)) {
@@ -262,13 +283,45 @@ export function parseMetadataSearchResponse(value: unknown): MetadataSearchRespo
262
283
  fail("Metadata search response.results", `must be an array with at most ${MAX_METADATA_SEARCH_RESULTS} entries.`);
263
284
  }
264
285
  const failed = new Set(enginesFailed);
286
+ let engineFailures: readonly MetadataSearchEngineFailure[] | undefined;
287
+ if (Object.hasOwn(input, "engine_failures")) {
288
+ if (!Array.isArray(input.engine_failures) || input.engine_failures.length !== enginesFailed.length) {
289
+ fail("Metadata search response.engine_failures", "must cover every failed engine exactly once.");
290
+ }
291
+ const seen = new Set<string>();
292
+ engineFailures = Object.freeze(input.engine_failures.map((value, index) => {
293
+ const label = `Metadata search response.engine_failures[${index}]`;
294
+ const entry = record(value, label);
295
+ strictKeys(entry, ["engine", "code"], label);
296
+ if (typeof entry.engine !== "string" || !failed.has(entry.engine) || seen.has(entry.engine)) {
297
+ fail(label, "must name a unique failed, queried engine.");
298
+ }
299
+ if (typeof entry.code !== "string" || !METADATA_SEARCH_FAILURE_CODES.some(code => code === entry.code)) {
300
+ fail(label, "must contain a known categorical failure code.");
301
+ }
302
+ seen.add(entry.engine);
303
+ return Object.freeze({ engine: entry.engine, code: entry.code as MetadataSearchFailureCode });
304
+ }));
305
+ }
265
306
  const results = Object.freeze(input.results.map((result, index) =>
266
307
  parseMetadataResult(result, index, queried, failed, budget)));
308
+ let engineUsage: readonly MetadataSearchEngineUsage[] | undefined;
309
+ if (Object.hasOwn(input, "engine_usage")) {
310
+ if (!Array.isArray(input.engine_usage) || input.engine_usage.length !== 1) fail("Metadata search response.engine_usage", "must contain one bounded OpenAlex usage record.");
311
+ const entry = record(input.engine_usage[0], "engine usage");
312
+ strictKeys(entry, ["engine", "reported_cost_usd", "billing_status"], "engine usage");
313
+ if (entry.engine !== "openalex" || !enginesQueried.includes("openalex") || failed.has("openalex")
314
+ || typeof entry.reported_cost_usd !== "number" || !Number.isFinite(entry.reported_cost_usd)
315
+ || entry.reported_cost_usd < 0 || entry.reported_cost_usd > 1_000_000 || entry.billing_status !== "not-established") fail("engine usage", "must preserve bounded provider-reported usage without asserting billing.");
316
+ engineUsage = Object.freeze([Object.freeze({ engine: "openalex" as const, reportedCostUsd: entry.reported_cost_usd, billingStatus: "not-established" as const })]);
317
+ }
267
318
  return Object.freeze({
268
319
  query,
269
320
  results,
270
321
  enginesQueried,
271
322
  enginesFailed,
323
+ ...(engineFailures === undefined ? {} : { engineFailures }),
324
+ ...(engineUsage === undefined ? {} : { engineUsage }),
272
325
  engineStatus: enginesFailed.length === 0
273
326
  ? "complete"
274
327
  : enginesFailed.length === enginesQueried.length ? "unavailable" : "partial",
@@ -1,4 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { syncsDirectories } from "../directory-sync.js";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import {
4
5
  closeSync,
@@ -1371,7 +1372,7 @@ export function writeUrlMetadataDocument(
1371
1372
  throw new Error(`URL metadata sidecar failed post-install verification: ${saved.sidecarPath}`);
1372
1373
  }
1373
1374
  assertHeldUrlMetadataWriteLockCurrent(heldLock);
1374
- fsyncSync(directoryDescriptor);
1375
+ if (syncsDirectories()) fsyncSync(directoryDescriptor);
1375
1376
  return { changed: true, path: saved.sidecarPath };
1376
1377
  } catch (error) {
1377
1378
  operationError = error;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Whether this platform can fsync a directory handle to persist a rename or
3
+ * link. POSIX filesystems need it. Windows refuses (fsync on a directory
4
+ * handle fails with EPERM), and NTFS journals the directory metadata itself,
5
+ * so callers skip the directory sync there and keep every file fsync.
6
+ */
7
+ export function syncsDirectories(platform: NodeJS.Platform = process.platform): boolean {
8
+ return platform !== "win32";
9
+ }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { syncsDirectories } from "./directory-sync.js";
2
3
  import { constants } from "node:fs";
3
4
  import { mkdir, mkdtemp, realpath, rename, rm } from "node:fs/promises";
4
5
  import { join, resolve } from "node:path";
@@ -197,8 +198,10 @@ export async function rebuildGraph(rootInput: string, options: GraphRebuildOptio
197
198
  const handle = await fs.open(stagedPath, constants.O_RDONLY | constants.O_NOFOLLOW);
198
199
  try { await handle.sync(); } finally { await handle.close(); }
199
200
  await rename(stagedPath, path);
200
- const directory = await fs.open(cache, constants.O_RDONLY);
201
- try { await directory.sync(); } finally { await directory.close(); }
201
+ if (syncsDirectories()) {
202
+ const directory = await fs.open(cache, constants.O_RDONLY);
203
+ try { await directory.sync(); } finally { await directory.close(); }
204
+ }
202
205
  return result;
203
206
  } finally {
204
207
  try {
@@ -86,6 +86,8 @@ export type {
86
86
  ArchiveTodayMemento,
87
87
  ArchiveTodayTimeMap,
88
88
  MetadataSearchResponse,
89
+ MetadataSearchEngineFailure,
90
+ MetadataSearchFailureCode,
89
91
  MetadataSearchResult,
90
92
  RankedMetadataSearchResult,
91
93
  } from "./clip/url-intelligence.js";