@loomwork/knowledge 0.0.0-stage → 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAAmE;AAA1D,8GAAA,aAAa,OAAA;AAAE,kHAAA,iBAAiB,OAAA;AAUzC,iCAAyC;AAAhC,uGAAA,cAAc,OAAA;AACvB,yCAAsF;AAA7E,2GAAA,cAAc,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,oHAAA,uBAAuB,OAAA;AAEhE,mCAA4D;AAAnD,yGAAA,eAAe,OAAA;AAAE,yGAAA,eAAe,OAAA"}
@@ -0,0 +1,41 @@
1
+ import type { LlmMessageInput, LlmRequestInput } from "@loomwork/llm";
2
+ import type { Passage } from "./types";
3
+ /**
4
+ * Retrieved passages as model messages: one `user` message per passage, each still
5
+ * carrying the taint it was ingested with.
6
+ *
7
+ * This shape is the whole safety argument, and it is the shape almost nobody writes first.
8
+ * The natural thing — join the passages into one string under a "here is the relevant
9
+ * documentation" header, and put it in the SYSTEM prompt — is refused by `guardRequest`
10
+ * with an `UntrustedContentError`, because the system prompt is the operator's voice and a
11
+ * corpus anyone can edit is not the operator. Two properties make this function the fix
12
+ * rather than a wrapper around the same mistake:
13
+ *
14
+ * - ONE message per passage, never a join. Taint is attached to a VALUE, so joining means
15
+ * unwrapping first, and provenance stops travelling exactly where it starts mattering.
16
+ * - the `user` role, which is the only position `guardRequest` fences rather than refuses.
17
+ *
18
+ * `guardRequest` then labels each fenced block with the passage's origin before any
19
+ * provider sees it, so the model is told which passages are data and where each came from.
20
+ */
21
+ export declare function passageMessages(passages: readonly Passage[]): LlmMessageInput[];
22
+ export interface GroundedRequestOptions {
23
+ /** The operator's instructions. Trusted content — a plain string, never a passage. */
24
+ instructions: string;
25
+ /** The question, appended after the passages so it reads last. */
26
+ question: string;
27
+ passages: readonly Passage[];
28
+ maxTokens?: number;
29
+ temperature?: number;
30
+ }
31
+ /**
32
+ * A complete grounded-answer request: operator instructions plus the untrusted-content
33
+ * note in `system`, the retrieved passages as fenced `user` messages, the question last.
34
+ *
35
+ * `instructions` is typed `string` rather than `LlmContent` on purpose. The system slot is
36
+ * where a passage must never land, and a signature that accepts a tainted value there
37
+ * would move the refusal from compile time to run time — the check exists either way, but
38
+ * only one of the two tells you before you ship.
39
+ */
40
+ export declare function groundedRequest(opts: GroundedRequestOptions): LlmRequestInput;
41
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,eAAe,EAAE,CAE/E;AAED,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,eAAe,CAU7E"}
package/dist/prompt.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.passageMessages = passageMessages;
4
+ exports.groundedRequest = groundedRequest;
5
+ const llm_1 = require("@loomwork/llm");
6
+ /**
7
+ * Retrieved passages as model messages: one `user` message per passage, each still
8
+ * carrying the taint it was ingested with.
9
+ *
10
+ * This shape is the whole safety argument, and it is the shape almost nobody writes first.
11
+ * The natural thing — join the passages into one string under a "here is the relevant
12
+ * documentation" header, and put it in the SYSTEM prompt — is refused by `guardRequest`
13
+ * with an `UntrustedContentError`, because the system prompt is the operator's voice and a
14
+ * corpus anyone can edit is not the operator. Two properties make this function the fix
15
+ * rather than a wrapper around the same mistake:
16
+ *
17
+ * - ONE message per passage, never a join. Taint is attached to a VALUE, so joining means
18
+ * unwrapping first, and provenance stops travelling exactly where it starts mattering.
19
+ * - the `user` role, which is the only position `guardRequest` fences rather than refuses.
20
+ *
21
+ * `guardRequest` then labels each fenced block with the passage's origin before any
22
+ * provider sees it, so the model is told which passages are data and where each came from.
23
+ */
24
+ function passageMessages(passages) {
25
+ return passages.map((passage) => ({ role: "user", content: passage.text }));
26
+ }
27
+ /**
28
+ * A complete grounded-answer request: operator instructions plus the untrusted-content
29
+ * note in `system`, the retrieved passages as fenced `user` messages, the question last.
30
+ *
31
+ * `instructions` is typed `string` rather than `LlmContent` on purpose. The system slot is
32
+ * where a passage must never land, and a signature that accepts a tainted value there
33
+ * would move the refusal from compile time to run time — the check exists either way, but
34
+ * only one of the two tells you before you ship.
35
+ */
36
+ function groundedRequest(opts) {
37
+ return {
38
+ system: `${opts.instructions} ${llm_1.UNTRUSTED_CONTENT_SYSTEM_NOTE}`,
39
+ messages: [
40
+ ...passageMessages(opts.passages),
41
+ { role: "user", content: opts.question },
42
+ ],
43
+ ...(opts.maxTokens !== undefined && { maxTokens: opts.maxTokens }),
44
+ ...(opts.temperature !== undefined && { temperature: opts.temperature }),
45
+ };
46
+ }
47
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":";;AAsBA,0CAEC;AAqBD,0CAUC;AAvDD,uCAA8D;AAI9D;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,eAAe,CAAC,QAA4B;IAC1D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACvF,CAAC;AAYD;;;;;;;;GAQG;AACH,SAAgB,eAAe,CAAC,IAA4B;IAC1D,OAAO;QACL,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,IAAI,mCAA6B,EAAE;QAC/D,QAAQ,EAAE;YACR,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;YACjC,EAAE,IAAI,EAAE,MAAe,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE;SAClD;QACD,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QAClE,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;KACzE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,94 @@
1
+ import type { TaintedValue } from "@loomwork/classify";
2
+ /**
3
+ * A source document, as the application already has it: an id it can name the document
4
+ * by again tomorrow, its text, and whatever metadata it wants carried onto every passage.
5
+ *
6
+ * There is deliberately no `source`, `url`, `author`, `section` or `updatedAt` here. Those
7
+ * are a vocabulary, they differ per corpus, and a framework that named them would be
8
+ * asking every app to translate into its dialect — put them in `meta`, which travels
9
+ * verbatim and is indexed like any other content.
10
+ */
11
+ export interface KnowledgeDoc {
12
+ /** Stable across revisions. This is what a re-ingest supersedes and what a citation names. */
13
+ id: string;
14
+ /** Optional label, carried onto every passage so a citation can read as a title. */
15
+ title?: string;
16
+ text: string;
17
+ /** Carried onto every passage verbatim. Must be JSON — it is stored in a MemoryEntry. */
18
+ meta?: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * One retrieved (or just-ingested) piece of a document.
22
+ *
23
+ * `text` is TAINTED (ADR 0085) and stays tainted all the way into the model request. A
24
+ * corpus is edited by whoever can edit the source, so a passage is third-party content by
25
+ * definition — the same class as an MCP tool result — and `guardRequest` refuses it in
26
+ * every instruction position. Tainting at INGEST rather than at the prompt is what makes
27
+ * that hold on every path: the wrapper is plain JSON, so it survives the store, the
28
+ * recorded retrieval, and a replay that rebuilds the prompt from the log.
29
+ */
30
+ export interface Passage {
31
+ docId: string;
32
+ title?: string;
33
+ /** 0-based position within the document, in source order. */
34
+ index: number;
35
+ text: TaintedValue<string>;
36
+ meta?: Record<string, unknown>;
37
+ }
38
+ /**
39
+ * Splits a document into passage texts, in source order.
40
+ *
41
+ * Injected, never inferred. How a document splits is a property of the corpus — a runbook
42
+ * splits on paragraphs, a legal contract on numbered clauses, a chat export on turns — and
43
+ * the retrieval quality of a knowledge base is mostly decided here. {@link packParagraphs}
44
+ * is a default worth having, not a default worth hiding.
45
+ */
46
+ export type Chunker = (doc: KnowledgeDoc) => string[];
47
+ /**
48
+ * The manifest row for one document: what a corpus knows about a document without
49
+ * reading its passages.
50
+ *
51
+ * `chunkCount` is what makes removal possible at all — passage ids are derived
52
+ * (`docId#0…docId#n`), so the count IS the id list, in one small row instead of n.
53
+ *
54
+ * `fingerprint` is over the document's content, so re-ingesting an unchanged document is
55
+ * free: that is what turns {@link KnowledgeBase.sync} from "rebuild the corpus" into "write
56
+ * the documents that actually changed".
57
+ *
58
+ * `committed` is the crash-safety flag, and the reason ingest writes a manifest row TWICE.
59
+ * The dangerous interleaving is not a lost write, it is a manifest that claims a revision
60
+ * whose passages were never appended: the next sync would compare fingerprints, see a
61
+ * match, and skip the document forever — a corpus silently missing a document that every
62
+ * status view calls present. Writing `committed: false` first, and flipping it only after
63
+ * the passages land, makes the crash window converge on a re-run instead.
64
+ */
65
+ export interface DocRecord {
66
+ docId: string;
67
+ chunkCount: number;
68
+ fingerprint: string;
69
+ committed: boolean;
70
+ /**
71
+ * WHO manages this document — the writer that may revise or delete it. `undefined` is a
72
+ * source of its own (the corpus's default writer), not a wildcard.
73
+ *
74
+ * It exists because `sync` is a destructive reconcile over everything the manifest holds,
75
+ * and a corpus is not always written by one thing. An agent that writes down what it
76
+ * learned from feedback puts documents in the same scope as an authored corpus — and the
77
+ * nightly sync of a wiki export would then delete every one of them, because they are not
78
+ * in the set the wiki produced. Stamping each document with its writer makes that
79
+ * impossible instead of documented: a `sync` only ever reconciles its own documents.
80
+ */
81
+ source?: string;
82
+ /**
83
+ * Generation of this manifest ROW, not a version of the document.
84
+ *
85
+ * Manifest rows are only ever APPENDED — `docId#0`, `docId#1`, … — and the highest one
86
+ * wins, because the alternative (delete the old row, append the new one) has a crash
87
+ * window in which the document has no manifest row at all. That window is the worst of
88
+ * the three: passages nothing counts are passages nothing can remove. Superseded rows
89
+ * are pruned after the new one is durable, so a crash costs a stale row and never a
90
+ * missing one.
91
+ */
92
+ revision: number;
93
+ }
94
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,8FAA8F;IAC9F,EAAE,EAAE,MAAM,CAAC;IACX,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,EAAE,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;OASG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@loomwork/knowledge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Document corpora over memory — ingest, revise, retrieve with provenance",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@loomwork/classify": "^0.1.0",
16
+ "@loomwork/core": "^0.1.0",
17
+ "@loomwork/llm": "^0.1.0",
18
+ "@loomwork/memory": "^0.1.0",
19
+ "@loomwork/verify": "^0.1.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.0.0",
23
+ "typescript": "^5.6.3",
24
+ "vitest": "^2.1.8",
25
+ "@loomwork/plugin-sqlite": "0.1.0"
26
+ },
27
+ "license": "Apache-2.0",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/chhetrisushil/loom.git",
31
+ "directory": "packages/knowledge"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src",
36
+ "!src/**/*.test.ts",
37
+ "!src/**/*.test.tsx",
38
+ "!src/**/__tests__",
39
+ "!src/**/__fixtures__",
40
+ "!src/**/__snapshots__",
41
+ "!src/**/__golden__",
42
+ "LICENSE",
43
+ "NOTICE"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "tag": "next"
48
+ },
49
+ "engines": {
50
+ "node": ">=20.0.0"
51
+ },
52
+ "scripts": {
53
+ "build": "tsc --build",
54
+ "dev": "tsc --build --watch",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
58
+ "typecheck:tests": "tsc -p tsconfig.test.json"
59
+ }
6
60
  }