@wrenautomation/falcon 0.0.0-stage → 0.3.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/LICENSE.md +91 -0
- package/README.md +50 -2
- package/dist/app/cli.d.ts +2 -0
- package/dist/app/cli.js +77 -0
- package/dist/app/cli.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/study/claims.d.ts +50 -0
- package/dist/study/claims.js +123 -0
- package/dist/study/claims.js.map +1 -0
- package/dist/study/drafts.d.ts +43 -0
- package/dist/study/drafts.js +93 -0
- package/dist/study/drafts.js.map +1 -0
- package/dist/study/evidence.d.ts +31 -0
- package/dist/study/evidence.js +123 -0
- package/dist/study/evidence.js.map +1 -0
- package/dist/study/grounding.d.ts +29 -0
- package/dist/study/grounding.js +58 -0
- package/dist/study/grounding.js.map +1 -0
- package/dist/study/index.d.ts +10 -0
- package/dist/study/index.js +11 -0
- package/dist/study/index.js.map +1 -0
- package/dist/study/report.d.ts +55 -0
- package/dist/study/report.js +56 -0
- package/dist/study/report.js.map +1 -0
- package/dist/text/entities.d.ts +2 -0
- package/dist/text/entities.js +4 -0
- package/dist/text/entities.js.map +1 -0
- package/dist/text/htmltext.d.ts +30 -0
- package/dist/text/htmltext.js +208 -0
- package/dist/text/htmltext.js.map +1 -0
- package/dist/text/index.d.ts +7 -0
- package/dist/text/index.js +8 -0
- package/dist/text/index.js.map +1 -0
- package/dist/text/text.d.ts +8 -0
- package/dist/text/text.js +38 -0
- package/dist/text/text.js.map +1 -0
- package/dist/video/gemini.d.ts +35 -0
- package/dist/video/gemini.js +103 -0
- package/dist/video/gemini.js.map +1 -0
- package/dist/video/index.d.ts +34 -0
- package/dist/video/index.js +11 -0
- package/dist/video/index.js.map +1 -0
- package/dist/video/read.d.ts +52 -0
- package/dist/video/read.js +99 -0
- package/dist/video/read.js.map +1 -0
- package/dist/video/shots.d.ts +33 -0
- package/dist/video/shots.js +151 -0
- package/dist/video/shots.js.map +1 -0
- package/dist/video/tools.d.ts +16 -0
- package/dist/video/tools.js +29 -0
- package/dist/video/tools.js.map +1 -0
- package/dist/video/watch.d.ts +101 -0
- package/dist/video/watch.js +420 -0
- package/dist/video/watch.js.map +1 -0
- package/package.json +66 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# PolyForm Shield License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/shield/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
8
|
+
|
|
9
|
+
## Copyright License
|
|
10
|
+
|
|
11
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
12
|
+
|
|
13
|
+
## Distribution License
|
|
14
|
+
|
|
15
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
16
|
+
|
|
17
|
+
## Notices
|
|
18
|
+
|
|
19
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
20
|
+
|
|
21
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
22
|
+
|
|
23
|
+
## Changes and New Works License
|
|
24
|
+
|
|
25
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
26
|
+
|
|
27
|
+
## Patent License
|
|
28
|
+
|
|
29
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
30
|
+
|
|
31
|
+
## Noncompete
|
|
32
|
+
|
|
33
|
+
Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
|
|
34
|
+
|
|
35
|
+
## Competition
|
|
36
|
+
|
|
37
|
+
Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
|
|
38
|
+
|
|
39
|
+
## New Products
|
|
40
|
+
|
|
41
|
+
If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
|
|
42
|
+
|
|
43
|
+
## Discontinued Products
|
|
44
|
+
|
|
45
|
+
You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
|
|
46
|
+
|
|
47
|
+
> Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
|
|
48
|
+
|
|
49
|
+
## Sales of Business
|
|
50
|
+
|
|
51
|
+
If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
|
|
52
|
+
|
|
53
|
+
## Fair Use
|
|
54
|
+
|
|
55
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
56
|
+
|
|
57
|
+
## No Other Rights
|
|
58
|
+
|
|
59
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
60
|
+
|
|
61
|
+
## Patent Defense
|
|
62
|
+
|
|
63
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
64
|
+
|
|
65
|
+
## Violations
|
|
66
|
+
|
|
67
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
68
|
+
|
|
69
|
+
## No Liability
|
|
70
|
+
|
|
71
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
72
|
+
|
|
73
|
+
## Definitions
|
|
74
|
+
|
|
75
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
76
|
+
|
|
77
|
+
A **product** can be a good or service, or a combination of them.
|
|
78
|
+
|
|
79
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
80
|
+
|
|
81
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
|
|
82
|
+
|
|
83
|
+
**Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
|
|
84
|
+
|
|
85
|
+
**Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
86
|
+
|
|
87
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
88
|
+
|
|
89
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
90
|
+
|
|
91
|
+
Required Notice: Copyright 2026 William Jin (https://wrenautomation.com)
|
package/README.md
CHANGED
|
@@ -1,3 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# falcon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Files, markup and media in. Text and quoted facts out. No database, no
|
|
4
|
+
accounts, no business rules: a library that reads, and a small CLI over it.
|
|
5
|
+
Was `@wrenautomation/scribe` until 0.3.0, when fetching moved to
|
|
6
|
+
[weaver](https://github.com/wrenautomation/weaver) and the rest took this name.
|
|
7
|
+
|
|
8
|
+
Three doors:
|
|
9
|
+
|
|
10
|
+
| Import | Does |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `@wrenautomation/falcon/text` | `htmlText`, `pdfText`, `decodeHtml`; `readPage` (HTML to title, text, links, mailtos, tels) |
|
|
13
|
+
| `@wrenautomation/falcon/video` | `watch` a video file: ffmpeg cuts and frames, Cohere speech and on-screen text, Gemini's read of the screen when a key works; shot labels. `captionsBody` renders a caption track per chapter, `hearAudio` transcribes an audio file by clips; both feed the one `VideoRead` shape weaver's readers return |
|
|
14
|
+
| `@wrenautomation/falcon/study` | a study whose every claim quotes a page: query planning, the claim gate, which pages to read, drafts that cite claims, the markdown report |
|
|
15
|
+
|
|
16
|
+
Everything is a pure function of what you pass: bytes, text, a file path, keys.
|
|
17
|
+
What fetches a page or a video is weaver's (`@wrenautomation/weaver`: polite HTTP, robots, the
|
|
18
|
+
fetch ladder, yt-dlp). What searches, stores, schedules or knows a customer belongs to the caller.
|
|
19
|
+
|
|
20
|
+
## CLI
|
|
21
|
+
|
|
22
|
+
Needs Node 24+. `watch` needs ffmpeg.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm i @wrenautomation/falcon
|
|
26
|
+
npx falcon text report.pdf # a PDF as text
|
|
27
|
+
npx falcon page saved.html --url https://example.com/report # title, text, links as JSON
|
|
28
|
+
COHERE_API_KEY=... npx falcon watch talk.mp4 # speech, on-screen text, cuts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Library
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { htmlText, readPage } from "@wrenautomation/falcon/text";
|
|
35
|
+
import { gateClaims } from "@wrenautomation/falcon/study";
|
|
36
|
+
|
|
37
|
+
const page = readPage(html, url);
|
|
38
|
+
// Ask a model for claims about the page, then keep only the ones whose quote is on it.
|
|
39
|
+
const { kept, dropped } = gateClaims(proposals, { url, text: page.text }, angles.length);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Develop
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pnpm install
|
|
46
|
+
pnpm lint && pnpm typecheck && pnpm test
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A `v*` tag publishes to npm (`.github/workflows/release.yml`, trusted publishing: no stored token).
|
|
50
|
+
|
|
51
|
+
License: PolyForm Shield 1.0.0 (`LICENSE.md`).
|
package/dist/app/cli.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `falcon`: the library from a shell. `text <file>` prints an HTML or PDF file as text,
|
|
4
|
+
* `page <file>` an HTML file's title, text and links, `watch <video>` what a video file shows
|
|
5
|
+
* and says. A URL is weaver's (`npx weaver fetch|read|list`). Keys come from the environment:
|
|
6
|
+
* COHERE_API_KEY (or COHERE_API_KEY_1..n), GEMINI_API_KEY.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import { readPage } from "../text/htmltext.js";
|
|
11
|
+
import { htmlText, pdfText } from "../text/text.js";
|
|
12
|
+
import { frameCounts, renderWatched, watch } from "../video/watch.js";
|
|
13
|
+
/** `NAME`, then `NAME_1`, `NAME_2`, ... in order: a fleet of keys for one vendor. */
|
|
14
|
+
function keys(env, name) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const one = env[name];
|
|
17
|
+
if (one)
|
|
18
|
+
out.push(one);
|
|
19
|
+
for (let i = 1;; i++) {
|
|
20
|
+
const k = env[`${name}_${i}`];
|
|
21
|
+
if (!k)
|
|
22
|
+
break;
|
|
23
|
+
out.push(k);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
const program = new Command().name("falcon").description("files and videos as text");
|
|
28
|
+
program
|
|
29
|
+
.command("text <file>")
|
|
30
|
+
.description("an HTML or PDF file as text")
|
|
31
|
+
.action(async (file) => {
|
|
32
|
+
const bytes = await readFile(file);
|
|
33
|
+
const out = /\.pdf$/i.test(file) ? await pdfText(bytes) : htmlText(bytes.toString("utf8"));
|
|
34
|
+
if (out === null)
|
|
35
|
+
throw new Error(`${file}: no text (scanned, encrypted or broken)`);
|
|
36
|
+
console.log(out);
|
|
37
|
+
});
|
|
38
|
+
program
|
|
39
|
+
.command("page <file>")
|
|
40
|
+
.description("an HTML file's title, text, links, mailtos and tels as JSON")
|
|
41
|
+
.option("--url <url>", "the page's address, to resolve its links")
|
|
42
|
+
.action(async (file, o) => {
|
|
43
|
+
const html = await readFile(file, "utf8");
|
|
44
|
+
console.log(JSON.stringify(readPage(html, o.url ?? `file://${file}`), null, 2));
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command("watch <video>")
|
|
48
|
+
.description("a video file's speech, on-screen text and cuts")
|
|
49
|
+
.option("--json", "the result as JSON")
|
|
50
|
+
.option("--no-speech", "skip the transcription")
|
|
51
|
+
.option("--language <code>", "spoken language, ISO 639-1", "en")
|
|
52
|
+
.option("--every <s>", "seconds between frames", Number, 2)
|
|
53
|
+
.option("--ffmpeg <path>", "the ffmpeg binary", "ffmpeg")
|
|
54
|
+
.action(async (input, o) => {
|
|
55
|
+
const cohereKeys = keys(process.env, "COHERE_API_KEY");
|
|
56
|
+
if (!cohereKeys.length)
|
|
57
|
+
throw new Error("watch needs COHERE_API_KEY");
|
|
58
|
+
const watched = await watch(input, {
|
|
59
|
+
cohereKeys,
|
|
60
|
+
geminiKeys: keys(process.env, "GEMINI_API_KEY"),
|
|
61
|
+
ffmpeg: o.ffmpeg,
|
|
62
|
+
speech: o.speech,
|
|
63
|
+
language: o.language,
|
|
64
|
+
everyS: o.every,
|
|
65
|
+
});
|
|
66
|
+
if (o.json)
|
|
67
|
+
console.log(JSON.stringify(watched, null, 2));
|
|
68
|
+
else
|
|
69
|
+
console.log(renderWatched(watched));
|
|
70
|
+
const { read, unread } = frameCounts(watched);
|
|
71
|
+
console.error(`${watched.durationS}s, ${read} frames read, ${unread} not read, ${watched.cuts.length} cuts, ${watched.speech.length} chars of speech`);
|
|
72
|
+
});
|
|
73
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
74
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/app/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* falcon: files, markup and media in; text and quoted facts out. Three doors: `./text` (HTML and
|
|
3
|
+
* PDF to text, a page to title, text and links), `./video` (watch a video file with ffmpeg, Cohere
|
|
4
|
+
* and Gemini; captions and audio to markdown) and `./study` (claims that must quote a page, drafts
|
|
5
|
+
* that must cite a claim, the report). Fetching a page or a video is weaver's
|
|
6
|
+
* (`@wrenautomation/weaver`).
|
|
7
|
+
*/
|
|
8
|
+
export * from "./study/index.js";
|
|
9
|
+
export * from "./text/index.js";
|
|
10
|
+
export * from "./video/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* falcon: files, markup and media in; text and quoted facts out. Three doors: `./text` (HTML and
|
|
3
|
+
* PDF to text, a page to title, text and links), `./video` (watch a video file with ffmpeg, Cohere
|
|
4
|
+
* and Gemini; captions and audio to markdown) and `./study` (claims that must quote a page, drafts
|
|
5
|
+
* that must cite a claim, the report). Fetching a page or a video is weaver's
|
|
6
|
+
* (`@wrenautomation/weaver`).
|
|
7
|
+
*/
|
|
8
|
+
export * from "./study/index.js";
|
|
9
|
+
export * from "./text/index.js";
|
|
10
|
+
export * from "./video/index.js";
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model's side of a study: plan an angle into searches, read one page into
|
|
3
|
+
* claims on every angle. Prompts, answer shapes and the claim gate; pure.
|
|
4
|
+
*
|
|
5
|
+
* A claim is kept only when its quote is on a page it was shown, word for
|
|
6
|
+
* word, and every number in the claim is in the quote (a year may come from
|
|
7
|
+
* the page). What the model knows without a page never reaches the report.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { type EvidencePage } from "./grounding.js";
|
|
11
|
+
export declare const STUDY_VERSION = "v2";
|
|
12
|
+
export declare const QueryPlan: z.ZodObject<{
|
|
13
|
+
queries: z.ZodArray<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export type QueryPlan = z.infer<typeof QueryPlan>;
|
|
16
|
+
/**
|
|
17
|
+
* `{name}` marks filled in one pass: a "{angle}" or "$&" inside a question or
|
|
18
|
+
* a page stays text.
|
|
19
|
+
*/
|
|
20
|
+
export declare const fill: (template: string, values: Readonly<Record<string, string>>) => string;
|
|
21
|
+
export declare function buildPlanPrompt(question: string, angle: string, year: number): string;
|
|
22
|
+
/** The plan's queries, trimmed, each once, at most QUERIES_PER_ANGLE. */
|
|
23
|
+
export declare function planQueries(plan: QueryPlan): string[];
|
|
24
|
+
export declare const ClaimProposals: z.ZodObject<{
|
|
25
|
+
claims: z.ZodArray<z.ZodObject<{
|
|
26
|
+
angle: z.ZodOptional<z.ZodNullable<z.ZodCoercedNumber<unknown>>>;
|
|
27
|
+
claim: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
28
|
+
quote: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
export type ClaimProposals = z.infer<typeof ClaimProposals>;
|
|
32
|
+
export declare function buildClaimsPrompt(question: string, angles: readonly string[], page: EvidencePage): string;
|
|
33
|
+
export interface Claim {
|
|
34
|
+
/** The angle it answers: its index in the study's angles. */
|
|
35
|
+
angle: number;
|
|
36
|
+
claim: string;
|
|
37
|
+
quote: string;
|
|
38
|
+
source_url: string;
|
|
39
|
+
}
|
|
40
|
+
type ClaimRejection = "no_angle" | "too_long" | "no_quote" | "quote_too_long" | "quote_not_found" | "number_not_in_quote";
|
|
41
|
+
export interface DroppedClaim {
|
|
42
|
+
claim: string;
|
|
43
|
+
why: ClaimRejection;
|
|
44
|
+
}
|
|
45
|
+
/** Every proposal through the gate: kept (each quote once), dropped with why. */
|
|
46
|
+
export declare function gateClaims(proposals: ClaimProposals, page: EvidencePage, angles: number): {
|
|
47
|
+
kept: Claim[];
|
|
48
|
+
dropped: DroppedClaim[];
|
|
49
|
+
};
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model's side of a study: plan an angle into searches, read one page into
|
|
3
|
+
* claims on every angle. Prompts, answer shapes and the claim gate; pure.
|
|
4
|
+
*
|
|
5
|
+
* A claim is kept only when its quote is on a page it was shown, word for
|
|
6
|
+
* word, and every number in the claim is in the quote (a year may come from
|
|
7
|
+
* the page). What the model knows without a page never reaches the report.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { numbersQuoted, pageQuoted, words } from "./grounding.js";
|
|
11
|
+
export const STUDY_VERSION = "v2";
|
|
12
|
+
const QUERIES_PER_ANGLE = 3;
|
|
13
|
+
const CLAIMS_PER_PAGE = 8;
|
|
14
|
+
const CLAIM_MAX_WORDS = 60;
|
|
15
|
+
const QUOTE_MIN_WORDS = 5;
|
|
16
|
+
/** The prompt asks for 60; a quote past this is a pasted section, not the words a claim rests on. */
|
|
17
|
+
const QUOTE_MAX_WORDS = 80;
|
|
18
|
+
const PLAN = `You plan web searches for one angle of a research study.
|
|
19
|
+
|
|
20
|
+
Return ONLY a JSON object, no prose, exactly this shape:
|
|
21
|
+
{"queries": ["...", "..."]}
|
|
22
|
+
|
|
23
|
+
Rules:
|
|
24
|
+
- At most {n} queries, each 3 to 10 words, as a person types into a search engine.
|
|
25
|
+
- Each query finds a different kind of source: a survey or benchmark with numbers, a trade publication or industry association, people in the field talking (forums, Reddit, reviews), a government statistic.
|
|
26
|
+
- Put a year in a query only when fresh numbers matter. This year is {year}.
|
|
27
|
+
- No site: operators. Quotes only around a phrase that must match exactly.
|
|
28
|
+
|
|
29
|
+
Study: {question}
|
|
30
|
+
Angle: {angle}
|
|
31
|
+
`;
|
|
32
|
+
export const QueryPlan = z.object({ queries: z.array(z.string()) });
|
|
33
|
+
/**
|
|
34
|
+
* `{name}` marks filled in one pass: a "{angle}" or "$&" inside a question or
|
|
35
|
+
* a page stays text.
|
|
36
|
+
*/
|
|
37
|
+
export const fill = (template, values) => template.replace(/\{(\w+)\}/g, (mark, name) => values[name] ?? mark);
|
|
38
|
+
export function buildPlanPrompt(question, angle, year) {
|
|
39
|
+
return fill(PLAN, { n: String(QUERIES_PER_ANGLE), year: String(year), question, angle });
|
|
40
|
+
}
|
|
41
|
+
/** The plan's queries, trimmed, each once, at most QUERIES_PER_ANGLE. */
|
|
42
|
+
export function planQueries(plan) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const q of plan.queries.map((s) => s.replace(/\s+/g, " ").trim())) {
|
|
45
|
+
if (q && !out.some((o) => o.toLowerCase() === q.toLowerCase()))
|
|
46
|
+
out.push(q);
|
|
47
|
+
}
|
|
48
|
+
return out.slice(0, QUERIES_PER_ANGLE);
|
|
49
|
+
}
|
|
50
|
+
const CLAIMS = `You pull facts out of one web page for a research report. The study has numbered angles.
|
|
51
|
+
|
|
52
|
+
Return ONLY a JSON object, no prose, exactly this shape:
|
|
53
|
+
{"claims": [{"angle": 1, "claim": "...", "quote": "..."}]}
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
- At most {max} claims, the most useful first. Only claims that answer an angle.
|
|
57
|
+
- angle: the number of the angle the claim answers.
|
|
58
|
+
- claim: one plain sentence a person would say out loud, at most {words} words. Specific: a number, a named practice, a stated cause. Say who says it ("A 2025 survey of 400 firms found ...", "Recruiters on Reddit say ...").
|
|
59
|
+
- quote: the exact words from the page that the claim rests on, copied character for character, 5 to 60 words.
|
|
60
|
+
- Every number in the claim must appear in the quote.
|
|
61
|
+
- A company's marketing about itself is not a fact about the market.
|
|
62
|
+
- Nothing on the page answers an angle: return {"claims": []}.
|
|
63
|
+
|
|
64
|
+
Study: {question}
|
|
65
|
+
|
|
66
|
+
ANGLES:
|
|
67
|
+
{angles}
|
|
68
|
+
|
|
69
|
+
PAGE: {url}
|
|
70
|
+
{page}
|
|
71
|
+
`;
|
|
72
|
+
export const ClaimProposals = z.object({
|
|
73
|
+
claims: z.array(z.object({
|
|
74
|
+
angle: z.coerce.number().nullable().optional(),
|
|
75
|
+
claim: z.string().nullable().optional(),
|
|
76
|
+
quote: z.string().nullable().optional(),
|
|
77
|
+
})),
|
|
78
|
+
});
|
|
79
|
+
export function buildClaimsPrompt(question, angles, page) {
|
|
80
|
+
return fill(CLAIMS, {
|
|
81
|
+
max: String(CLAIMS_PER_PAGE),
|
|
82
|
+
words: String(CLAIM_MAX_WORDS),
|
|
83
|
+
question,
|
|
84
|
+
angles: angles.map((a, i) => `${i + 1}. ${a}`).join("\n"),
|
|
85
|
+
url: page.url,
|
|
86
|
+
page: page.text,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** Keep the claim only when it answers an angle and stands on a quote from the page. */
|
|
90
|
+
function groundClaim(p, page, angles) {
|
|
91
|
+
const claim = (p.claim ?? "").replace(/\s+/g, " ").trim();
|
|
92
|
+
if (!claim)
|
|
93
|
+
return { claim: null, rejected: null };
|
|
94
|
+
const angle = Number(p.angle) - 1;
|
|
95
|
+
if (!Number.isInteger(angle) || angle < 0 || angle >= angles)
|
|
96
|
+
return { claim: null, rejected: "no_angle" };
|
|
97
|
+
if (words(claim) > CLAIM_MAX_WORDS)
|
|
98
|
+
return { claim: null, rejected: "too_long" };
|
|
99
|
+
const quote = (p.quote ?? "").trim();
|
|
100
|
+
if (words(quote) < QUOTE_MIN_WORDS)
|
|
101
|
+
return { claim: null, rejected: "no_quote" };
|
|
102
|
+
if (words(quote) > QUOTE_MAX_WORDS)
|
|
103
|
+
return { claim: null, rejected: "quote_too_long" };
|
|
104
|
+
if (!pageQuoted(quote, [page], page.url))
|
|
105
|
+
return { claim: null, rejected: "quote_not_found" };
|
|
106
|
+
if (!numbersQuoted(claim, quote, page))
|
|
107
|
+
return { claim: null, rejected: "number_not_in_quote" };
|
|
108
|
+
return { claim: { angle, claim, quote, source_url: page.url }, rejected: null };
|
|
109
|
+
}
|
|
110
|
+
/** Every proposal through the gate: kept (each quote once), dropped with why. */
|
|
111
|
+
export function gateClaims(proposals, page, angles) {
|
|
112
|
+
const kept = [];
|
|
113
|
+
const dropped = [];
|
|
114
|
+
for (const p of proposals.claims.slice(0, CLAIMS_PER_PAGE)) {
|
|
115
|
+
const { claim, rejected } = groundClaim(p, page, angles);
|
|
116
|
+
if (rejected)
|
|
117
|
+
dropped.push({ claim: (p.claim ?? "").trim(), why: rejected });
|
|
118
|
+
else if (claim && !kept.some((k) => k.quote === claim.quote))
|
|
119
|
+
kept.push(claim);
|
|
120
|
+
}
|
|
121
|
+
return { kept, dropped };
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=claims.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims.js","sourceRoot":"","sources":["../../src/study/claims.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A study's claims turned into something to use: offer candidates, cold
|
|
3
|
+
* emails, a brief. The model sees only the numbered claims, never the pages,
|
|
4
|
+
* so a draft is two steps from a quote: draft cites claim, claim quotes page.
|
|
5
|
+
* An item that cites nothing, cites a claim that isn't there, or has a number
|
|
6
|
+
* no cited claim (or the task) gave it, is dropped.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { type Claim } from "./claims.js";
|
|
10
|
+
/** One draft a study writes from its claims: its key in the report and what to ask for. */
|
|
11
|
+
export interface StudyDraftSpec {
|
|
12
|
+
key: string;
|
|
13
|
+
ask: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const DraftProposals: z.ZodObject<{
|
|
16
|
+
items: z.ZodArray<z.ZodObject<{
|
|
17
|
+
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
18
|
+
body: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
|
+
cites: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodCoercedNumber<unknown>>>>;
|
|
20
|
+
}, z.core.$strip>>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export type DraftProposals = z.infer<typeof DraftProposals>;
|
|
23
|
+
/** A claim as drafts see it: its report number. */
|
|
24
|
+
export interface NumberedClaim extends Claim {
|
|
25
|
+
n: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function buildDraftPrompt(question: string, spec: StudyDraftSpec, claims: readonly NumberedClaim[]): string;
|
|
28
|
+
export interface DraftItem {
|
|
29
|
+
title: string;
|
|
30
|
+
body: string;
|
|
31
|
+
cites: number[];
|
|
32
|
+
}
|
|
33
|
+
type DraftRejection = "empty" | "no_cites" | "unknown_cite" | "number_not_cited";
|
|
34
|
+
export interface DroppedDraft {
|
|
35
|
+
title: string;
|
|
36
|
+
why: DraftRejection;
|
|
37
|
+
}
|
|
38
|
+
/** Every item through the gate: kept (citation marks stripped), or dropped with why. */
|
|
39
|
+
export declare function gateDrafts(proposals: DraftProposals, spec: StudyDraftSpec, claims: readonly NumberedClaim[]): {
|
|
40
|
+
kept: DraftItem[];
|
|
41
|
+
dropped: DroppedDraft[];
|
|
42
|
+
};
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A study's claims turned into something to use: offer candidates, cold
|
|
3
|
+
* emails, a brief. The model sees only the numbered claims, never the pages,
|
|
4
|
+
* so a draft is two steps from a quote: draft cites claim, claim quotes page.
|
|
5
|
+
* An item that cites nothing, cites a claim that isn't there, or has a number
|
|
6
|
+
* no cited claim (or the task) gave it, is dropped.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { fill } from "./claims.js";
|
|
10
|
+
import { numbers } from "./grounding.js";
|
|
11
|
+
const DRAFT_ITEMS_MAX = 8;
|
|
12
|
+
const DRAFT = `You turn research into something a small business can use. Use ONLY the numbered claims below: they are everything we know.
|
|
13
|
+
|
|
14
|
+
Return ONLY a JSON object, no prose, exactly this shape:
|
|
15
|
+
{"items": [{"title": "...", "body": "...", "cites": [1, 4]}]}
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- cites: the numbers of the claims the item rests on. At least one.
|
|
19
|
+
- No citation marks like [1] or (claim 1) in the title or body: cites carries them.
|
|
20
|
+
- Every number in the title or body must appear in a claim it cites, or in the task.
|
|
21
|
+
- Never invent a statistic, a client, a result, a quote or a price. We have no clients yet: never say what clients or owners "tell us".
|
|
22
|
+
- Plain words a person says out loud. No dashes as punctuation.
|
|
23
|
+
|
|
24
|
+
TASK:
|
|
25
|
+
{ask}
|
|
26
|
+
|
|
27
|
+
Study: {question}
|
|
28
|
+
|
|
29
|
+
CLAIMS:
|
|
30
|
+
{claims}
|
|
31
|
+
`;
|
|
32
|
+
export const DraftProposals = z.object({
|
|
33
|
+
items: z.array(z.object({
|
|
34
|
+
title: z.string().nullable().optional(),
|
|
35
|
+
body: z.string().nullable().optional(),
|
|
36
|
+
cites: z.array(z.coerce.number()).nullable().optional(),
|
|
37
|
+
})),
|
|
38
|
+
});
|
|
39
|
+
const hostOf = (url) => {
|
|
40
|
+
try {
|
|
41
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
export function buildDraftPrompt(question, spec, claims) {
|
|
48
|
+
return fill(DRAFT, {
|
|
49
|
+
ask: spec.ask,
|
|
50
|
+
question,
|
|
51
|
+
claims: claims.map((c) => `[${c.n}] ${c.claim} (${hostOf(c.source_url)})`).join("\n"),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** "[3]", "[1, 4]", "(claim 2)", "(claims 1 and 5)": the model's citation marks, not text. */
|
|
55
|
+
const CITE_MARK = /\s*(?:\[\d+(?:\s*,\s*\d+)*\]|\((?:claims?|sources?)\s*\d+(?:\s*(?:,|and|&)\s*\d+)*\))/gi;
|
|
56
|
+
const stripCiteMarks = (s) => s
|
|
57
|
+
.replace(CITE_MARK, "")
|
|
58
|
+
.replace(/[ \t]+([.,;:!?])/g, "$1")
|
|
59
|
+
.trim();
|
|
60
|
+
/** Every item through the gate: kept (citation marks stripped), or dropped with why. */
|
|
61
|
+
export function gateDrafts(proposals, spec, claims) {
|
|
62
|
+
const byN = new Map(claims.map((c) => [c.n, c]));
|
|
63
|
+
const fromTask = new Set(numbers(spec.ask));
|
|
64
|
+
const kept = [];
|
|
65
|
+
const dropped = [];
|
|
66
|
+
for (const p of proposals.items.slice(0, DRAFT_ITEMS_MAX)) {
|
|
67
|
+
const title = stripCiteMarks(p.title ?? "");
|
|
68
|
+
const body = stripCiteMarks(p.body ?? "");
|
|
69
|
+
const cites = [...new Set(p.cites ?? [])];
|
|
70
|
+
const why = !body
|
|
71
|
+
? "empty"
|
|
72
|
+
: !cites.length
|
|
73
|
+
? "no_cites"
|
|
74
|
+
: cites.some((n) => !byN.has(n))
|
|
75
|
+
? "unknown_cite"
|
|
76
|
+
: null;
|
|
77
|
+
if (why) {
|
|
78
|
+
dropped.push({ title, why });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const cited = new Set(cites.flatMap((n) => {
|
|
82
|
+
const c = byN.get(n);
|
|
83
|
+
return c ? numbers(`${c.claim} ${c.quote}`) : [];
|
|
84
|
+
}));
|
|
85
|
+
if (numbers(`${title} ${body}`).some((n) => !cited.has(n) && !fromTask.has(n))) {
|
|
86
|
+
dropped.push({ title, why: "number_not_cited" });
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
kept.push({ title, body, cites: cites.sort((a, b) => a - b) });
|
|
90
|
+
}
|
|
91
|
+
return { kept, dropped };
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=drafts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drafts.js","sourceRoot":"","sources":["../../src/study/drafts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a study reads and what the model is shown. Pure: the run stores the
|
|
3
|
+
* hits and pages, these decide which pages and which parts of them.
|
|
4
|
+
*/
|
|
5
|
+
import { type EvidencePage } from "./grounding.js";
|
|
6
|
+
export interface Hit {
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
snippet: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** One page, one read: no fragment, no trailing slash. */
|
|
12
|
+
export declare const pageKey: (url: string) => string;
|
|
13
|
+
/**
|
|
14
|
+
* The pages to read for one angle: each list's best hit, then each list's
|
|
15
|
+
* second, until `limit`. One list per query (and one for an answer engine's
|
|
16
|
+
* sources), so no single query fills the angle. Social and video pages are
|
|
17
|
+
* skipped; they don't read as text.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pickReads(lists: readonly (readonly Hit[])[], limit: number): string[];
|
|
20
|
+
/** The words worth matching in a question: four letters or more, not filler. */
|
|
21
|
+
export declare function terms(...texts: string[]): Set<string>;
|
|
22
|
+
/**
|
|
23
|
+
* The parts of a page worth showing for these terms, in page order, within
|
|
24
|
+
* `budget` characters. A chunk scores a point per term it has, and one more
|
|
25
|
+
* when it has a number (a study wants figures). A short page is shown whole.
|
|
26
|
+
*/
|
|
27
|
+
export declare function passages(text: string, want: ReadonlySet<string>, budget: number): string;
|
|
28
|
+
/** How much of one page the model is shown: its best passages for every angle. */
|
|
29
|
+
export declare const PAGE_CHARS = 12000;
|
|
30
|
+
/** The part of one page its claims are drawn from. */
|
|
31
|
+
export declare const pageEvidence: (page: EvidencePage, want: ReadonlySet<string>) => EvidencePage;
|