@isparling/engram-cli 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.
- package/LICENSE +187 -0
- package/README.md +41 -0
- package/bin/engram +14 -0
- package/package.json +32 -0
- package/release/engram-release.ts +1493 -0
- package/src/atomicWrite.ts +80 -0
- package/src/candidate.ts +229 -0
- package/src/classify.ts +275 -0
- package/src/cli.ts +709 -0
- package/src/contentHash.ts +54 -0
- package/src/deepFreeze.ts +13 -0
- package/src/diff.ts +81 -0
- package/src/guardedRetrieval.ts +128 -0
- package/src/guardedRetrievalInternal.ts +321 -0
- package/src/knowledgeRecord.ts +256 -0
- package/src/knowledgeRetrieval.ts +564 -0
- package/src/knowledgeRollup.ts +479 -0
- package/src/knowledgeTransaction.ts +683 -0
- package/src/knowledgeTypes.ts +249 -0
- package/src/knowledgeValidation.ts +269 -0
- package/src/markdownRecord.ts +265 -0
- package/src/packLoader.ts +188 -0
- package/src/packTypes.ts +12 -0
- package/src/presentation.ts +673 -0
- package/src/qmdConfigGuard.ts +245 -0
- package/src/qmdRunner.ts +392 -0
- package/src/realPath.ts +47 -0
- package/src/spaceBinding.ts +37 -0
- package/src/spaceRegistry.ts +1139 -0
- package/src/submit.ts +228 -0
- package/src/symlinkGuard.ts +71 -0
- package/src/transactionLock.ts +188 -0
- package/src/types.ts +38 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Hand-written parser/serializer for the Markdown record shape:
|
|
2
|
+
//
|
|
3
|
+
// ---
|
|
4
|
+
// id: <id>
|
|
5
|
+
// title: <title>
|
|
6
|
+
// updated: <YYYY-MM-DD>
|
|
7
|
+
// ---
|
|
8
|
+
//
|
|
9
|
+
// ## Active claims
|
|
10
|
+
//
|
|
11
|
+
// - claim text [source: tag]
|
|
12
|
+
//
|
|
13
|
+
// ## Evidence log
|
|
14
|
+
//
|
|
15
|
+
// - YYYY-MM-DD — entry text [source: tag]
|
|
16
|
+
//
|
|
17
|
+
// `## Active claims` and `## Evidence log` are the two sections this
|
|
18
|
+
// module understands and can mutate. Any OTHER `## ...` section in the
|
|
19
|
+
// body is preserved verbatim, in its original position, through parse and
|
|
20
|
+
// serialize — a record is free to carry a `## Notes` section (or anything
|
|
21
|
+
// else) that is never touched. So is any PREAMBLE: free-form content
|
|
22
|
+
// between the frontmatter's closing `---` and the first `##` heading.
|
|
23
|
+
// Losing either on write would be a silent, ungated destructive edit —
|
|
24
|
+
// exactly what the additive/non-additive gate exists to prevent, and
|
|
25
|
+
// exactly what happened here before preambleLines existed: content before
|
|
26
|
+
// the first heading was parsed and then never re-emitted.
|
|
27
|
+
|
|
28
|
+
import { err, ok, requireDefined, type Result } from "./types.ts";
|
|
29
|
+
|
|
30
|
+
export type RecordFrontmatter = {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
updated: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type KnownSection =
|
|
37
|
+
| { kind: "active-claims"; bullets: string[] }
|
|
38
|
+
| { kind: "evidence-log"; bullets: string[] };
|
|
39
|
+
|
|
40
|
+
export type OtherSection = { kind: "other"; heading: string; lines: string[] };
|
|
41
|
+
|
|
42
|
+
export type RecordSection = KnownSection | OtherSection;
|
|
43
|
+
|
|
44
|
+
export type ParsedRecord = {
|
|
45
|
+
frontmatter: RecordFrontmatter;
|
|
46
|
+
/** Raw body lines before the first `## ...` heading, preserved verbatim
|
|
47
|
+
* (including the mandatory blank separator line after the frontmatter
|
|
48
|
+
* delimiter, for a record with no real preamble). A record is free to
|
|
49
|
+
* carry introductory prose here; losing it on the first write would be
|
|
50
|
+
* a silent, ungated destructive edit. */
|
|
51
|
+
preambleLines: string[];
|
|
52
|
+
sections: RecordSection[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ACTIVE_CLAIMS_HEADING = "## Active claims";
|
|
56
|
+
const EVIDENCE_LOG_HEADING = "## Evidence log";
|
|
57
|
+
|
|
58
|
+
export function parseRecord(text: string): Result<ParsedRecord> {
|
|
59
|
+
const errors: string[] = [];
|
|
60
|
+
const lines = text.split("\n").map((line) => line.replace(/\r$/, ""));
|
|
61
|
+
|
|
62
|
+
if (lines[0] !== "---") {
|
|
63
|
+
return err(["record must begin with a '---' frontmatter delimiter"]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const closingIndex = lines.indexOf("---", 1);
|
|
67
|
+
if (closingIndex === -1) {
|
|
68
|
+
return err(["record frontmatter is missing its closing '---' delimiter"]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const frontmatterLines = lines.slice(1, closingIndex);
|
|
72
|
+
const frontmatterFields = new Map<string, string>();
|
|
73
|
+
for (const line of frontmatterLines) {
|
|
74
|
+
if (line.trim() === "") continue;
|
|
75
|
+
const match = /^([a-zA-Z_][a-zA-Z0-9_]*):\s?(.*)$/.exec(line);
|
|
76
|
+
if (!match) {
|
|
77
|
+
errors.push(`unrecognized frontmatter line: ${JSON.stringify(line)}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const key = requireDefined(match[1], "frontmatter regex capture group 1");
|
|
81
|
+
const value = requireDefined(match[2], "frontmatter regex capture group 2");
|
|
82
|
+
frontmatterFields.set(key, value.trim());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const required of ["id", "title", "updated"] as const) {
|
|
86
|
+
if (!frontmatterFields.has(required)) {
|
|
87
|
+
errors.push(`frontmatter is missing required field: ${required}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const knownFrontmatterKeys = new Set(["id", "title", "updated"]);
|
|
91
|
+
for (const key of frontmatterFields.keys()) {
|
|
92
|
+
if (!knownFrontmatterKeys.has(key)) {
|
|
93
|
+
errors.push(`frontmatter has unexpected field: ${key}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (errors.length > 0) {
|
|
98
|
+
return err(errors);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const frontmatter: RecordFrontmatter = {
|
|
102
|
+
id: requireDefined(frontmatterFields.get("id"), "id present after validation"),
|
|
103
|
+
title: requireDefined(frontmatterFields.get("title"), "title present after validation"),
|
|
104
|
+
updated: requireDefined(frontmatterFields.get("updated"), "updated present after validation"),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const bodyLines = lines.slice(closingIndex + 1);
|
|
108
|
+
const firstHeadingIndex = bodyLines.findIndex((line) => line.startsWith("## "));
|
|
109
|
+
const preambleLines = firstHeadingIndex === -1 ? bodyLines.slice() : bodyLines.slice(0, firstHeadingIndex);
|
|
110
|
+
const sectionLines = firstHeadingIndex === -1 ? [] : bodyLines.slice(firstHeadingIndex);
|
|
111
|
+
const rawSections = splitIntoSections(sectionLines);
|
|
112
|
+
|
|
113
|
+
const sections: RecordSection[] = [];
|
|
114
|
+
let activeClaimsIndex = -1;
|
|
115
|
+
let evidenceLogIndex = -1;
|
|
116
|
+
|
|
117
|
+
for (const raw of rawSections) {
|
|
118
|
+
if (raw.heading === ACTIVE_CLAIMS_HEADING) {
|
|
119
|
+
if (activeClaimsIndex !== -1) {
|
|
120
|
+
errors.push(`record has more than one "${ACTIVE_CLAIMS_HEADING}" section`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const bulletsResult = parseBullets(raw.lines, ACTIVE_CLAIMS_HEADING);
|
|
124
|
+
if (!bulletsResult.ok) {
|
|
125
|
+
errors.push(...bulletsResult.errors);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
activeClaimsIndex = sections.length;
|
|
129
|
+
sections.push({ kind: "active-claims", bullets: bulletsResult.value });
|
|
130
|
+
} else if (raw.heading === EVIDENCE_LOG_HEADING) {
|
|
131
|
+
if (evidenceLogIndex !== -1) {
|
|
132
|
+
errors.push(`record has more than one "${EVIDENCE_LOG_HEADING}" section`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const bulletsResult = parseBullets(raw.lines, EVIDENCE_LOG_HEADING);
|
|
136
|
+
if (!bulletsResult.ok) {
|
|
137
|
+
errors.push(...bulletsResult.errors);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
evidenceLogIndex = sections.length;
|
|
141
|
+
sections.push({ kind: "evidence-log", bullets: bulletsResult.value });
|
|
142
|
+
} else {
|
|
143
|
+
sections.push({ kind: "other", heading: raw.heading, lines: raw.lines.slice() });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (activeClaimsIndex === -1) {
|
|
148
|
+
errors.push(`record body is missing the "${ACTIVE_CLAIMS_HEADING}" section`);
|
|
149
|
+
}
|
|
150
|
+
if (evidenceLogIndex === -1) {
|
|
151
|
+
errors.push(`record body is missing the "${EVIDENCE_LOG_HEADING}" section`);
|
|
152
|
+
}
|
|
153
|
+
if (activeClaimsIndex !== -1 && evidenceLogIndex !== -1 && activeClaimsIndex > evidenceLogIndex) {
|
|
154
|
+
errors.push(`"${ACTIVE_CLAIMS_HEADING}" must come before "${EVIDENCE_LOG_HEADING}"`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (errors.length > 0) {
|
|
158
|
+
return err(errors);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return ok({ frontmatter, preambleLines, sections });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type RawSection = { heading: string; lines: string[] };
|
|
165
|
+
|
|
166
|
+
function splitIntoSections(bodyLines: string[]): RawSection[] {
|
|
167
|
+
const sections: RawSection[] = [];
|
|
168
|
+
let current: RawSection | null = null;
|
|
169
|
+
for (const line of bodyLines) {
|
|
170
|
+
if (line.startsWith("## ")) {
|
|
171
|
+
current = { heading: line.trimEnd(), lines: [] };
|
|
172
|
+
sections.push(current);
|
|
173
|
+
} else if (current) {
|
|
174
|
+
current.lines.push(line);
|
|
175
|
+
}
|
|
176
|
+
// lines before the first heading (blank separator lines) are ignored
|
|
177
|
+
}
|
|
178
|
+
return sections;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseBullets(lines: string[], sectionName: string): Result<string[]> {
|
|
182
|
+
const bullets: string[] = [];
|
|
183
|
+
const errors: string[] = [];
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
if (line.trim() === "") continue;
|
|
186
|
+
const match = /^- (.+)$/.exec(line);
|
|
187
|
+
if (!match) {
|
|
188
|
+
errors.push(`unrecognized line in "${sectionName}": ${JSON.stringify(line)}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const bulletText = requireDefined(match[1], "bullet regex capture group 1");
|
|
192
|
+
bullets.push(bulletText.trimEnd());
|
|
193
|
+
}
|
|
194
|
+
if (errors.length > 0) return err(errors);
|
|
195
|
+
return ok(bullets);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function serializeRecord(record: ParsedRecord): string {
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
lines.push("---");
|
|
201
|
+
lines.push(`id: ${record.frontmatter.id}`);
|
|
202
|
+
lines.push(`title: ${record.frontmatter.title}`);
|
|
203
|
+
lines.push(`updated: ${record.frontmatter.updated}`);
|
|
204
|
+
lines.push("---");
|
|
205
|
+
lines.push(...record.preambleLines);
|
|
206
|
+
|
|
207
|
+
for (const section of record.sections) {
|
|
208
|
+
if (section.kind === "active-claims") {
|
|
209
|
+
lines.push(ACTIVE_CLAIMS_HEADING);
|
|
210
|
+
lines.push("");
|
|
211
|
+
for (const claim of section.bullets) lines.push(`- ${claim}`);
|
|
212
|
+
lines.push("");
|
|
213
|
+
} else if (section.kind === "evidence-log") {
|
|
214
|
+
lines.push(EVIDENCE_LOG_HEADING);
|
|
215
|
+
lines.push("");
|
|
216
|
+
for (const evidence of section.bullets) lines.push(`- ${evidence}`);
|
|
217
|
+
lines.push("");
|
|
218
|
+
} else {
|
|
219
|
+
lines.push(section.heading);
|
|
220
|
+
lines.push(...section.lines);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Normalize to exactly one trailing newline regardless of how the last
|
|
225
|
+
// section's captured lines ended.
|
|
226
|
+
return `${lines.join("\n").replace(/\n*$/, "")}\n`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function findKnownSection(record: ParsedRecord, kind: "active-claims" | "evidence-log"): KnownSection {
|
|
230
|
+
for (const section of record.sections) {
|
|
231
|
+
if (section.kind === kind) return section;
|
|
232
|
+
}
|
|
233
|
+
// parseRecord guarantees both known sections exist exactly once before
|
|
234
|
+
// ever returning ok(...), so reaching here means a ParsedRecord was
|
|
235
|
+
// constructed some other way. That is a programming error, not a
|
|
236
|
+
// user-facing validation failure, so it throws rather than returning a
|
|
237
|
+
// Result.
|
|
238
|
+
throw new Error(`internal invariant violated: parsed record is missing its "${kind}" section`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function getActiveClaims(record: ParsedRecord): string[] {
|
|
242
|
+
return findKnownSection(record, "active-claims").bullets;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function getEvidenceLog(record: ParsedRecord): string[] {
|
|
246
|
+
return findKnownSection(record, "evidence-log").bullets;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Returns a new ParsedRecord with the active-claims and evidence-log
|
|
251
|
+
* sections replaced and frontmatter updated, preserving the preamble and
|
|
252
|
+
* every other section (unknown headings, and their relative position)
|
|
253
|
+
* unchanged — no candidate field can touch either.
|
|
254
|
+
*/
|
|
255
|
+
export function withMutatedContent(
|
|
256
|
+
record: ParsedRecord,
|
|
257
|
+
update: { frontmatter: RecordFrontmatter; activeClaims: string[]; evidenceLog: string[] },
|
|
258
|
+
): ParsedRecord {
|
|
259
|
+
const sections: RecordSection[] = record.sections.map((section) => {
|
|
260
|
+
if (section.kind === "active-claims") return { kind: "active-claims", bullets: update.activeClaims };
|
|
261
|
+
if (section.kind === "evidence-log") return { kind: "evidence-log", bullets: update.evidenceLog };
|
|
262
|
+
return section;
|
|
263
|
+
});
|
|
264
|
+
return { frontmatter: update.frontmatter, preambleLines: record.preambleLines, sections };
|
|
265
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack loader — resolves a pack's declared `from` module specifier to a
|
|
3
|
+
* KnowledgeExtractor or KnowledgePack implementation. Resolution is explicit
|
|
4
|
+
* and external-only: the loader imports exactly the module the binding named
|
|
5
|
+
* and enforces that the selected export's declared id and version match the
|
|
6
|
+
* binding request. There is no bundled registry to fall back to and no silent
|
|
7
|
+
* substitution.
|
|
8
|
+
*
|
|
9
|
+
* The loader does NOT own pack validation, reconciliation, or the transaction
|
|
10
|
+
* pipeline. It is the one bridge between the binding's declared pack identity
|
|
11
|
+
* and a loadable module.
|
|
12
|
+
*
|
|
13
|
+
* Error contract: every failure is `kind: "validation"` and uses exactly
|
|
14
|
+
* `pack_from_required`, `pack_load_failed`, `pack_export_invalid`, or
|
|
15
|
+
* `pack_identity_mismatch`. Messages describe the category and never print
|
|
16
|
+
* module contents, local paths, or an imported exception stack.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { dirname, resolve } from "node:path";
|
|
22
|
+
import { pathToFileURL } from "node:url";
|
|
23
|
+
import type { KnowledgeExtractor, KnowledgePack, KnowledgeResult, PresentationPack } from "./knowledgeTypes.ts";
|
|
24
|
+
|
|
25
|
+
function packError<T>(code: string, message: string): KnowledgeResult<T> {
|
|
26
|
+
return { ok: false, errors: [{ kind: "validation", code, message }] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function camelToSnake(name: string): string {
|
|
34
|
+
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Locates a candidate export by the requested pack id. Checks, in order:
|
|
39
|
+
* 1. A named export matching the pack id.
|
|
40
|
+
* 2. A `default` export.
|
|
41
|
+
* 3. An export in a `packs` or `packRegistry` object.
|
|
42
|
+
*/
|
|
43
|
+
function findExport(mod: Record<string, unknown>, id: string): unknown {
|
|
44
|
+
const named = mod[id] ?? mod[camelToSnake(id)];
|
|
45
|
+
if (named !== undefined) return named;
|
|
46
|
+
|
|
47
|
+
const defaultExport = mod.default;
|
|
48
|
+
if (defaultExport !== undefined) return defaultExport;
|
|
49
|
+
|
|
50
|
+
const registry = mod.packs ?? mod.packRegistry;
|
|
51
|
+
if (isRecord(registry)) {
|
|
52
|
+
const entry = registry[id];
|
|
53
|
+
if (entry !== undefined) return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isSourceClassPolicy(value: unknown): boolean {
|
|
60
|
+
return (
|
|
61
|
+
isRecord(value) &&
|
|
62
|
+
Array.isArray(value.allowedSourceClasses) &&
|
|
63
|
+
value.allowedSourceClasses.every((sourceClass) => typeof sourceClass === "string") &&
|
|
64
|
+
typeof value.queryStrategy === "function" &&
|
|
65
|
+
typeof value.classifySource === "function" &&
|
|
66
|
+
(typeof value.relevanceThreshold === "number" || value.relevanceThreshold === null) &&
|
|
67
|
+
typeof value.isEligible === "function" &&
|
|
68
|
+
value.includePresentations === false
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isView(value: unknown): boolean {
|
|
73
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.version === "number" &&
|
|
74
|
+
(value.scope === "search" || value.scope === "space") &&
|
|
75
|
+
typeof value.retrievalQuery === "function" && typeof value.project === "function";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isAudience(value: unknown): boolean {
|
|
79
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.version === "number" &&
|
|
80
|
+
typeof value.authorize === "function" && typeof value.adapt === "function";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isDelivery(value: unknown): boolean {
|
|
84
|
+
return isRecord(value) && typeof value.id === "string" && typeof value.version === "number" &&
|
|
85
|
+
(value.format === "markdown" || value.format === "plain" || value.format === "json") &&
|
|
86
|
+
typeof value.maxWords === "number" && typeof value.retain === "boolean";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isKnowledgePack(value: unknown): value is KnowledgePack & PresentationPack & Record<string, unknown> {
|
|
90
|
+
return (
|
|
91
|
+
isRecord(value) &&
|
|
92
|
+
typeof value.id === "string" &&
|
|
93
|
+
typeof value.version === "string" &&
|
|
94
|
+
typeof value.validateEnvelope === "function" &&
|
|
95
|
+
typeof value.relatedQuery === "function" &&
|
|
96
|
+
typeof value.reconcile === "function" &&
|
|
97
|
+
isSourceClassPolicy(value.retrievalPolicy) &&
|
|
98
|
+
Array.isArray(value.views) && value.views.every(isView) &&
|
|
99
|
+
Array.isArray(value.audiences) && value.audiences.every(isAudience) &&
|
|
100
|
+
Array.isArray(value.deliveries) && value.deliveries.every(isDelivery)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isKnowledgeExtractor(value: unknown): value is KnowledgeExtractor & Record<string, unknown> {
|
|
105
|
+
return (
|
|
106
|
+
isRecord(value) &&
|
|
107
|
+
typeof value.id === "string" &&
|
|
108
|
+
typeof value.version === "string" &&
|
|
109
|
+
typeof value.extractCandidates === "function"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function moduleSpecifier(from: string, bindingPath: string | undefined): string {
|
|
114
|
+
if (bindingPath !== undefined && (from.startsWith("./") || from.startsWith("../"))) {
|
|
115
|
+
return pathToFileURL(resolve(dirname(bindingPath), from)).href;
|
|
116
|
+
}
|
|
117
|
+
return from;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolves a pack `id`+`version` to its implementation, importing exactly the
|
|
122
|
+
* binding's `from` module specifier and enforcing that the selected export's
|
|
123
|
+
* declared identity matches. Refuses:
|
|
124
|
+
* - A missing or whitespace-only `from` as `pack_from_required`.
|
|
125
|
+
* - An unloadable module as `pack_load_failed`.
|
|
126
|
+
* - A module whose candidate export is not a complete KnowledgePack and
|
|
127
|
+
* PresentationPack as `pack_export_invalid`.
|
|
128
|
+
* - A valid-shaped export whose id or version differs as
|
|
129
|
+
* `pack_identity_mismatch`.
|
|
130
|
+
*/
|
|
131
|
+
export async function resolveKnowledgePack(
|
|
132
|
+
id: string,
|
|
133
|
+
version: string,
|
|
134
|
+
from: string | undefined,
|
|
135
|
+
bindingPath?: string,
|
|
136
|
+
): Promise<KnowledgeResult<KnowledgePack & PresentationPack>> {
|
|
137
|
+
if (from === undefined || from.trim().length === 0) {
|
|
138
|
+
return packError("pack_from_required", "a from module specifier is required to resolve the external pack");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let mod: Record<string, unknown>;
|
|
142
|
+
try {
|
|
143
|
+
mod = await import(moduleSpecifier(from, bindingPath));
|
|
144
|
+
} catch {
|
|
145
|
+
return packError("pack_load_failed", "the external pack module could not be loaded");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const candidate = findExport(mod, id);
|
|
149
|
+
if (!isKnowledgePack(candidate)) {
|
|
150
|
+
return packError("pack_export_invalid", "the external pack module does not export a complete KnowledgePack and PresentationPack");
|
|
151
|
+
}
|
|
152
|
+
if (candidate.id !== id || candidate.version !== version) {
|
|
153
|
+
return packError("pack_identity_mismatch", "the external pack module's id or version does not match the declared pack");
|
|
154
|
+
}
|
|
155
|
+
return { ok: true, value: candidate };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Loads a KnowledgeExtractor for the pack `id`+`version` declared with `from`,
|
|
160
|
+
* with the same from, load, export, and identity checks as
|
|
161
|
+
* `resolveKnowledgePack`.
|
|
162
|
+
*/
|
|
163
|
+
export async function loadExtractionPack(
|
|
164
|
+
id: string,
|
|
165
|
+
version: string,
|
|
166
|
+
from: string | undefined,
|
|
167
|
+
bindingPath?: string,
|
|
168
|
+
): Promise<KnowledgeResult<KnowledgeExtractor>> {
|
|
169
|
+
if (from === undefined || from.trim().length === 0) {
|
|
170
|
+
return packError("pack_from_required", "a from module specifier is required to load the extraction pack");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let mod: Record<string, unknown>;
|
|
174
|
+
try {
|
|
175
|
+
mod = await import(moduleSpecifier(from, bindingPath));
|
|
176
|
+
} catch {
|
|
177
|
+
return packError("pack_load_failed", "the extraction pack module could not be loaded");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const candidate = findExport(mod, id);
|
|
181
|
+
if (!isKnowledgeExtractor(candidate)) {
|
|
182
|
+
return packError("pack_export_invalid", "the extraction pack module does not export a complete KnowledgeExtractor");
|
|
183
|
+
}
|
|
184
|
+
if (candidate.id !== id || candidate.version !== version) {
|
|
185
|
+
return packError("pack_identity_mismatch", "the extraction pack module's id or version does not match the declared extractor");
|
|
186
|
+
}
|
|
187
|
+
return { ok: true, value: candidate };
|
|
188
|
+
}
|
package/src/packTypes.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public external-pack interface types.
|
|
3
|
+
*
|
|
4
|
+
* A normal knowledge operation requires the full KnowledgePack and
|
|
5
|
+
* PresentationPack surface. KnowledgeExtractor is separately optional and is
|
|
6
|
+
* selected only by a binding entry with extract: true.
|
|
7
|
+
*/
|
|
8
|
+
export type {
|
|
9
|
+
KnowledgeExtractor,
|
|
10
|
+
KnowledgePack,
|
|
11
|
+
PresentationPack,
|
|
12
|
+
} from "./knowledgeTypes.ts";
|