@agentxm/knowledge-query 0.28.11 → 0.28.13
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/dist/src/corpus/corpus-status.d.ts +134 -0
- package/dist/src/corpus/corpus-status.js +74 -0
- package/dist/src/corpus/installed-corpus.d.ts +46 -0
- package/dist/src/corpus/installed-corpus.js +62 -0
- package/dist/src/documents.d.ts +349 -0
- package/dist/src/documents.js +168 -0
- package/dist/src/errors.d.ts +38 -0
- package/dist/src/errors.js +20 -0
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.js +9 -1
- package/dist/src/knowledge-capabilities.js +1 -1
- package/dist/src/knowledge-capture.js +1 -1
- package/dist/src/knowledge-discovery.d.ts +394 -0
- package/dist/src/knowledge-discovery.js +171 -0
- package/dist/src/knowledge-graph.js +5 -4
- package/dist/src/knowledge-index.d.ts +14 -1
- package/dist/src/knowledge-index.js +15 -1
- package/dist/src/knowledge-projection.d.ts +1 -1
- package/dist/src/lint/lint-knowledge.d.ts +47 -0
- package/dist/src/lint/lint-knowledge.js +61 -0
- package/dist/src/query/request.d.ts +58 -0
- package/dist/src/query/request.js +242 -0
- package/dist/src/testing.d.ts +46 -0
- package/dist/src/testing.js +139 -0
- package/package.json +13 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KnowledgeActorRecord, KnowledgeAuthoredLink, KnowledgeConcept, KnowledgeDocumentKind, KnowledgeTrustTier } from "@agentxm/
|
|
1
|
+
import type { KnowledgeActorRecord, KnowledgeAuthoredLink, KnowledgeConcept, KnowledgeDocumentKind, KnowledgeTrustTier } from "@agentxm/extension-content/knowledge";
|
|
2
2
|
export type KnowledgeSearchableField = "bundle" | "conceptId" | "title" | "description" | "tag" | "type" | "body" | "resource" | "status" | "staleAfter" | "generated" | "verified" | "trust";
|
|
3
3
|
export interface KnowledgeBodyPassage {
|
|
4
4
|
readonly text: string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate Open Knowledge Format bundles without changing them.
|
|
3
|
+
*
|
|
4
|
+
* The caller selects either the installed corpus (all bundles, or one by
|
|
5
|
+
* name) or a locally authored package directory — never both. The verdict is
|
|
6
|
+
* a typed report: every diagnostic located by bundle and path, and validity
|
|
7
|
+
* decided by whether any diagnostic is an error.
|
|
8
|
+
*
|
|
9
|
+
* @experimental This API is unstable and may change without notice.
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Path from "effect/Path";
|
|
13
|
+
import { WorkspaceLocation } from "@agentxm/workspace-state";
|
|
14
|
+
import type { KnowledgeLintQueryResult } from "../documents.js";
|
|
15
|
+
import { KnowledgeCorpusUnavailable, KnowledgeRequestInvalid } from "../errors.js";
|
|
16
|
+
export interface LintKnowledgeRequest {
|
|
17
|
+
/** One installed bundle by name; omit for every installed bundle. */
|
|
18
|
+
readonly bundle?: string;
|
|
19
|
+
/** A locally authored Knowledge package directory, relative to the workspace. */
|
|
20
|
+
readonly packagePath?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LintKnowledgeResult {
|
|
23
|
+
readonly document: KnowledgeLintQueryResult;
|
|
24
|
+
/** How many bundles were validated, for the success sentence. */
|
|
25
|
+
readonly bundleCount: number;
|
|
26
|
+
/** Diagnostics of error severity; a non-empty list means the verdict is invalid. */
|
|
27
|
+
readonly errorCount: number;
|
|
28
|
+
}
|
|
29
|
+
/** Validate the selected installed bundles, or one authored package. */
|
|
30
|
+
export declare const lintKnowledge: (request: LintKnowledgeRequest) => Effect.Effect<{
|
|
31
|
+
document: {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
diagnostics: {
|
|
34
|
+
code: import("@agentxm/extension-content/knowledge").KnowledgeDiagnosticCode;
|
|
35
|
+
severity: "error" | "warning";
|
|
36
|
+
relativePath: string;
|
|
37
|
+
line?: number;
|
|
38
|
+
column?: number;
|
|
39
|
+
message: string;
|
|
40
|
+
details?: import("@agentxm/extension-content/knowledge").KnowledgeFrontmatterParseDetails;
|
|
41
|
+
bundle: string;
|
|
42
|
+
}[];
|
|
43
|
+
};
|
|
44
|
+
bundleCount: number;
|
|
45
|
+
errorCount: number;
|
|
46
|
+
}, KnowledgeRequestInvalid | KnowledgeCorpusUnavailable | import("@agentxm/workspace-state").SettingsIoError | import("@agentxm/workspace-state").SettingsParseError | import("@agentxm/workspace-state").SettingsDecodeError | import("@agentxm/workspace-state").WorkspaceRootEscape | import("@agentxm/workspace-state").LockfileIoError | import("@agentxm/workspace-state").LockfileParseError | import("@agentxm/workspace-state").LockfileDecodeError | import("@agentxm/workspace-state").LockfileVersionUnsupported, import("effect/FileSystem").FileSystem | Path.Path | import("@agentxm/workspace-state").DesiredStateReader | WorkspaceLocation | import("@agentxm/workspace-state").LockfileReader>;
|
|
47
|
+
//# sourceMappingURL=lint-knowledge.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @effect-diagnostics anyUnknownInErrorContext:off — lint accepts opaque OKF accessor errors only at its diagnostic boundary
|
|
2
|
+
/**
|
|
3
|
+
* Validate Open Knowledge Format bundles without changing them.
|
|
4
|
+
*
|
|
5
|
+
* The caller selects either the installed corpus (all bundles, or one by
|
|
6
|
+
* name) or a locally authored package directory — never both. The verdict is
|
|
7
|
+
* a typed report: every diagnostic located by bundle and path, and validity
|
|
8
|
+
* decided by whether any diagnostic is an error.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Path from "effect/Path";
|
|
14
|
+
import { inspectKnowledgePackage, } from "@agentxm/extension-content/knowledge";
|
|
15
|
+
import { InstalledKnowledgeUnavailable, selectInstalledKnowledgeBundles, } from "@agentxm/workspace-projection";
|
|
16
|
+
import { WorkspaceLocation } from "@agentxm/workspace-state";
|
|
17
|
+
import { KnowledgeCorpusUnavailable, KnowledgeRequestInvalid } from "../errors.js";
|
|
18
|
+
/** One installed or authored bundle's diagnostics, tagged with its bundle name. */
|
|
19
|
+
const flatten = (bundles) => bundles.flatMap(({ name, inspection }) => inspection.diagnostics.map((item) => ({ bundle: name, ...item })));
|
|
20
|
+
const inspectAuthored = Effect.fn("Knowledge.lintAuthored")(function* (packagePath) {
|
|
21
|
+
const location = yield* WorkspaceLocation;
|
|
22
|
+
const path = yield* Path.Path;
|
|
23
|
+
const inspected = yield* inspectKnowledgePackage(path.resolve(location.baseDir, packagePath)).pipe(Effect.mapError((cause) => new KnowledgeCorpusUnavailable({
|
|
24
|
+
reason: "source-unreadable",
|
|
25
|
+
detail: `Failed to inspect authored Knowledge package ${packagePath}`,
|
|
26
|
+
cause,
|
|
27
|
+
})));
|
|
28
|
+
return [inspected];
|
|
29
|
+
});
|
|
30
|
+
const inspectInstalled = Effect.fn("Knowledge.lintInstalled")(function* (bundle) {
|
|
31
|
+
const selected = yield* selectInstalledKnowledgeBundles(bundle === undefined ? undefined : { name: bundle }).pipe(Effect.catchTag("InstalledKnowledgeUnavailable", (failure) => Effect.fail(new KnowledgeCorpusUnavailable({
|
|
32
|
+
reason: failure.reason,
|
|
33
|
+
detail: failure.detail,
|
|
34
|
+
...(failure.bundle === undefined ? {} : { bundle: failure.bundle }),
|
|
35
|
+
}))));
|
|
36
|
+
return yield* Effect.forEach(selected, (entry) => inspectKnowledgePackage(entry.packageRoot).pipe(Effect.map((inspected) => ({ ...inspected, name: entry.name })), Effect.mapError((cause) => new KnowledgeCorpusUnavailable({
|
|
37
|
+
reason: "source-unreadable",
|
|
38
|
+
detail: `Failed to inspect knowledge bundle "${entry.name}"`,
|
|
39
|
+
bundle: entry.name,
|
|
40
|
+
cause,
|
|
41
|
+
}))), { concurrency: "unbounded" });
|
|
42
|
+
});
|
|
43
|
+
/** Validate the selected installed bundles, or one authored package. */
|
|
44
|
+
export const lintKnowledge = Effect.fn("Knowledge.lint")(function* (request) {
|
|
45
|
+
if (request.bundle !== undefined && request.packagePath !== undefined) {
|
|
46
|
+
return yield* new KnowledgeRequestInvalid({
|
|
47
|
+
detail: "Choose either an installed bundle name or --path, not both",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const bundles = request.packagePath === undefined
|
|
51
|
+
? yield* inspectInstalled(request.bundle)
|
|
52
|
+
: yield* inspectAuthored(request.packagePath);
|
|
53
|
+
const diagnostics = flatten(bundles);
|
|
54
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
55
|
+
return {
|
|
56
|
+
document: { valid: errorCount === 0, diagnostics },
|
|
57
|
+
bundleCount: bundles.length,
|
|
58
|
+
errorCount,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=lint-knowledge.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published Knowledge discovery request grammar and its bounds.
|
|
3
|
+
*
|
|
4
|
+
* Callers hand over the filter expressions a person typed; this module decides
|
|
5
|
+
* what they mean, refuses what the published contract does not admit, and
|
|
6
|
+
* builds the canonical query. The grammar (`=`, `!=`, `~=`, JSON-pointer
|
|
7
|
+
* properties, searchable field names) and every numeric bound live here, keyed
|
|
8
|
+
* to the same capabilities document discovery publishes.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import type { WorkspaceScope } from "@agentxm/extension-model/unstable/workspace-scope";
|
|
14
|
+
import { KnowledgeRequestInvalid } from "../errors.js";
|
|
15
|
+
import { type KnowledgeQuery } from "../knowledge-query.js";
|
|
16
|
+
/** The filter expressions one structured query request carries. */
|
|
17
|
+
export interface KnowledgeQueryRequest {
|
|
18
|
+
readonly scope: WorkspaceScope;
|
|
19
|
+
readonly expression?: string;
|
|
20
|
+
readonly fields?: ReadonlyArray<string>;
|
|
21
|
+
readonly properties?: ReadonlyArray<string>;
|
|
22
|
+
readonly metadata?: ReadonlyArray<string>;
|
|
23
|
+
readonly lifecycle?: ReadonlyArray<string>;
|
|
24
|
+
readonly tags?: ReadonlyArray<string>;
|
|
25
|
+
readonly bundle?: string;
|
|
26
|
+
readonly kind?: "concept" | "index" | "log";
|
|
27
|
+
readonly status?: string;
|
|
28
|
+
readonly resultLimit?: number;
|
|
29
|
+
readonly passageLimit?: number;
|
|
30
|
+
readonly passageLength?: number;
|
|
31
|
+
readonly cursor?: string;
|
|
32
|
+
}
|
|
33
|
+
/** A lexical search request: one text expression plus paging. */
|
|
34
|
+
export interface KnowledgeSearchRequest {
|
|
35
|
+
readonly scope: WorkspaceScope;
|
|
36
|
+
readonly expression: string;
|
|
37
|
+
readonly resultLimit?: number;
|
|
38
|
+
readonly cursor?: string;
|
|
39
|
+
}
|
|
40
|
+
/** The published explanation of how the lexical strategy ranked a page. */
|
|
41
|
+
export interface KnowledgeQueryExplanation {
|
|
42
|
+
readonly strategy: "lexical";
|
|
43
|
+
readonly ordering: "relevance" | "metadata";
|
|
44
|
+
readonly rankFactors: ReadonlyArray<{
|
|
45
|
+
readonly field: string;
|
|
46
|
+
readonly weight: number;
|
|
47
|
+
}>;
|
|
48
|
+
readonly tieBreak: string;
|
|
49
|
+
}
|
|
50
|
+
/** Explain a query's ranking from the ranker's own weights. */
|
|
51
|
+
export declare const explainKnowledgeQuery: (query: KnowledgeQuery) => KnowledgeQueryExplanation;
|
|
52
|
+
/** Build the canonical query one structured request denotes. */
|
|
53
|
+
export declare const makeKnowledgeQueryRequest: (request: KnowledgeQueryRequest) => Effect.Effect<KnowledgeQuery, KnowledgeRequestInvalid>;
|
|
54
|
+
/** Build the canonical query one lexical search request denotes. */
|
|
55
|
+
export declare const makeKnowledgeSearchRequest: (request: KnowledgeSearchRequest) => Effect.Effect<KnowledgeQuery, KnowledgeRequestInvalid>;
|
|
56
|
+
/** Refuse a traversal depth outside the published maximum. */
|
|
57
|
+
export declare const checkTraversalDepth: (depth: number) => Effect.Effect<number, KnowledgeRequestInvalid>;
|
|
58
|
+
//# sourceMappingURL=request.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published Knowledge discovery request grammar and its bounds.
|
|
3
|
+
*
|
|
4
|
+
* Callers hand over the filter expressions a person typed; this module decides
|
|
5
|
+
* what they mean, refuses what the published contract does not admit, and
|
|
6
|
+
* builds the canonical query. The grammar (`=`, `!=`, `~=`, JSON-pointer
|
|
7
|
+
* properties, searchable field names) and every numeric bound live here, keyed
|
|
8
|
+
* to the same capabilities document discovery publishes.
|
|
9
|
+
*
|
|
10
|
+
* @experimental This API is unstable and may change without notice.
|
|
11
|
+
*/
|
|
12
|
+
import * as Effect from "effect/Effect";
|
|
13
|
+
import { parseKnowledgeSearchQuery } from "@agentxm/extension-content/knowledge";
|
|
14
|
+
import { KNOWLEDGE_DISCOVERY_CAPABILITIES } from "../knowledge-capabilities.js";
|
|
15
|
+
import { KNOWLEDGE_RANK_FACTORS, KNOWLEDGE_RANK_TIE_BREAK } from "../knowledge-index.js";
|
|
16
|
+
import { KnowledgeRequestInvalid } from "../errors.js";
|
|
17
|
+
import { KNOWLEDGE_LIFECYCLE_FILTER_FIELDS, KNOWLEDGE_METADATA_FILTER_FIELDS, KNOWLEDGE_SEARCHABLE_FIELDS, makeKnowledgeQuery, } from "../knowledge-query.js";
|
|
18
|
+
const limits = KNOWLEDGE_DISCOVERY_CAPABILITIES.limits;
|
|
19
|
+
/** Explain a query's ranking from the ranker's own weights. */
|
|
20
|
+
export const explainKnowledgeQuery = (query) => ({
|
|
21
|
+
strategy: "lexical",
|
|
22
|
+
ordering: query.ordering,
|
|
23
|
+
rankFactors: KNOWLEDGE_RANK_FACTORS,
|
|
24
|
+
tieBreak: KNOWLEDGE_RANK_TIE_BREAK,
|
|
25
|
+
});
|
|
26
|
+
const textClauses = (input) => {
|
|
27
|
+
const parsed = parseKnowledgeSearchQuery(input);
|
|
28
|
+
if (!parsed.ok)
|
|
29
|
+
return parsed.detail;
|
|
30
|
+
return parsed.query.clauses.map((clause) => {
|
|
31
|
+
switch (clause.kind) {
|
|
32
|
+
case "term":
|
|
33
|
+
return { kind: "term", value: clause.token };
|
|
34
|
+
case "phrase":
|
|
35
|
+
return { kind: "phrase", value: clause.tokens.join(" ") };
|
|
36
|
+
case "literal":
|
|
37
|
+
return { kind: "literal", value: clause.value };
|
|
38
|
+
default:
|
|
39
|
+
return clause;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const splitAssignment = (input) => {
|
|
44
|
+
const separator = input.indexOf("=");
|
|
45
|
+
if (separator <= 0 || separator === input.length - 1)
|
|
46
|
+
return undefined;
|
|
47
|
+
return [input.slice(0, separator), input.slice(separator + 1)];
|
|
48
|
+
};
|
|
49
|
+
const splitFilterAssignment = (input) => {
|
|
50
|
+
for (const [token, operator] of [
|
|
51
|
+
["!=", "not-equals"],
|
|
52
|
+
["~=", "contains"],
|
|
53
|
+
["=", "equals"],
|
|
54
|
+
]) {
|
|
55
|
+
const separator = input.indexOf(token);
|
|
56
|
+
if (separator <= 0 || separator + token.length === input.length)
|
|
57
|
+
continue;
|
|
58
|
+
return [input.slice(0, separator), operator, input.slice(separator + token.length)];
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
const fieldClauses = (inputs) => {
|
|
63
|
+
const clauses = [];
|
|
64
|
+
for (const input of inputs) {
|
|
65
|
+
const assignment = splitAssignment(input);
|
|
66
|
+
if (assignment === undefined)
|
|
67
|
+
return `Expected --field FIELD=QUERY, received "${input}"`;
|
|
68
|
+
const [fieldName, query] = assignment;
|
|
69
|
+
const field = KNOWLEDGE_SEARCHABLE_FIELDS.find((candidate) => candidate === fieldName);
|
|
70
|
+
if (field === undefined)
|
|
71
|
+
return `Unknown searchable field "${fieldName}"`;
|
|
72
|
+
const parsed = textClauses(query);
|
|
73
|
+
if (typeof parsed === "string")
|
|
74
|
+
return parsed;
|
|
75
|
+
for (const clause of parsed) {
|
|
76
|
+
if (clause.kind !== "term" && clause.kind !== "phrase" && clause.kind !== "literal") {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
clauses.push({ kind: "field", field, clause });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return clauses;
|
|
83
|
+
};
|
|
84
|
+
const propertyClauses = (inputs) => {
|
|
85
|
+
const clauses = [];
|
|
86
|
+
for (const input of inputs) {
|
|
87
|
+
const assignment = splitFilterAssignment(input);
|
|
88
|
+
if (assignment === undefined ||
|
|
89
|
+
!assignment[0].startsWith("/") ||
|
|
90
|
+
/~(?:[^01]|$)/u.test(assignment[0])) {
|
|
91
|
+
return `Expected --property /json/pointer{=|!=|~=}VALUE, received "${input}"`;
|
|
92
|
+
}
|
|
93
|
+
clauses.push({
|
|
94
|
+
kind: "property",
|
|
95
|
+
pointer: assignment[0],
|
|
96
|
+
operator: assignment[1],
|
|
97
|
+
value: assignment[2],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return clauses;
|
|
101
|
+
};
|
|
102
|
+
const metadataClauses = (inputs) => {
|
|
103
|
+
const clauses = [];
|
|
104
|
+
for (const input of inputs) {
|
|
105
|
+
const assignment = splitFilterAssignment(input);
|
|
106
|
+
if (assignment === undefined) {
|
|
107
|
+
return `Expected --metadata FIELD{=|!=|~=}VALUE, received "${input}"`;
|
|
108
|
+
}
|
|
109
|
+
const [fieldName, operator, value] = assignment;
|
|
110
|
+
const field = KNOWLEDGE_METADATA_FILTER_FIELDS.find((candidate) => candidate === fieldName);
|
|
111
|
+
if (field === undefined)
|
|
112
|
+
return `Unknown metadata field "${fieldName}"`;
|
|
113
|
+
clauses.push({ kind: "metadata", field, operator, value });
|
|
114
|
+
}
|
|
115
|
+
return clauses;
|
|
116
|
+
};
|
|
117
|
+
const lifecycleClauses = (inputs) => {
|
|
118
|
+
const clauses = [];
|
|
119
|
+
for (const input of inputs) {
|
|
120
|
+
const assignment = splitFilterAssignment(input);
|
|
121
|
+
if (assignment === undefined) {
|
|
122
|
+
return `Expected --lifecycle FIELD{=|!=}VALUE, received "${input}"`;
|
|
123
|
+
}
|
|
124
|
+
const [fieldName, operator, value] = assignment;
|
|
125
|
+
const field = KNOWLEDGE_LIFECYCLE_FILTER_FIELDS.find((candidate) => candidate === fieldName);
|
|
126
|
+
if (field === undefined)
|
|
127
|
+
return `Unknown lifecycle field "${fieldName}"`;
|
|
128
|
+
if (operator === "contains")
|
|
129
|
+
return "Lifecycle filters do not support the contains operator";
|
|
130
|
+
clauses.push({ kind: "lifecycle", field, operator, value });
|
|
131
|
+
}
|
|
132
|
+
return clauses;
|
|
133
|
+
};
|
|
134
|
+
const buildClauses = (request) => {
|
|
135
|
+
const base = request.expression === undefined ? [] : textClauses(request.expression);
|
|
136
|
+
if (typeof base === "string")
|
|
137
|
+
return base;
|
|
138
|
+
const fields = fieldClauses(request.fields ?? []);
|
|
139
|
+
if (typeof fields === "string")
|
|
140
|
+
return fields;
|
|
141
|
+
const properties = propertyClauses(request.properties ?? []);
|
|
142
|
+
if (typeof properties === "string")
|
|
143
|
+
return properties;
|
|
144
|
+
const metadata = metadataClauses(request.metadata ?? []);
|
|
145
|
+
if (typeof metadata === "string")
|
|
146
|
+
return metadata;
|
|
147
|
+
const lifecycle = lifecycleClauses(request.lifecycle ?? []);
|
|
148
|
+
if (typeof lifecycle === "string")
|
|
149
|
+
return lifecycle;
|
|
150
|
+
const tags = request.tags ?? [];
|
|
151
|
+
if (tags.some((value) => value.length === 0) || request.bundle === "" || request.status === "") {
|
|
152
|
+
return "Filter values must not be empty";
|
|
153
|
+
}
|
|
154
|
+
return [
|
|
155
|
+
...base,
|
|
156
|
+
...fields,
|
|
157
|
+
...properties,
|
|
158
|
+
...metadata,
|
|
159
|
+
...lifecycle,
|
|
160
|
+
...tags.map((value) => ({
|
|
161
|
+
kind: "metadata",
|
|
162
|
+
field: "tag",
|
|
163
|
+
operator: "equals",
|
|
164
|
+
value,
|
|
165
|
+
})),
|
|
166
|
+
...(request.bundle === undefined
|
|
167
|
+
? []
|
|
168
|
+
: [
|
|
169
|
+
{
|
|
170
|
+
kind: "metadata",
|
|
171
|
+
field: "bundle",
|
|
172
|
+
operator: "equals",
|
|
173
|
+
value: request.bundle,
|
|
174
|
+
},
|
|
175
|
+
]),
|
|
176
|
+
...(request.kind === undefined
|
|
177
|
+
? []
|
|
178
|
+
: [
|
|
179
|
+
{
|
|
180
|
+
kind: "metadata",
|
|
181
|
+
field: "kind",
|
|
182
|
+
operator: "equals",
|
|
183
|
+
value: request.kind,
|
|
184
|
+
},
|
|
185
|
+
]),
|
|
186
|
+
...(request.status === undefined
|
|
187
|
+
? []
|
|
188
|
+
: [
|
|
189
|
+
{
|
|
190
|
+
kind: "lifecycle",
|
|
191
|
+
field: "status",
|
|
192
|
+
operator: "equals",
|
|
193
|
+
value: request.status,
|
|
194
|
+
},
|
|
195
|
+
]),
|
|
196
|
+
];
|
|
197
|
+
};
|
|
198
|
+
const withinBound = (value, minimum, maximum) => value === undefined || (Number.isSafeInteger(value) && value >= minimum && value <= maximum);
|
|
199
|
+
const paging = (request) => ({
|
|
200
|
+
...(request.resultLimit === undefined ? {} : { resultLimit: request.resultLimit }),
|
|
201
|
+
...(request.passageLimit === undefined ? {} : { passageLimit: request.passageLimit }),
|
|
202
|
+
...(request.passageLength === undefined ? {} : { passageLength: request.passageLength }),
|
|
203
|
+
...(request.cursor === undefined ? {} : { cursor: request.cursor }),
|
|
204
|
+
});
|
|
205
|
+
/**
|
|
206
|
+
* Refuse a request whose paging bounds fall outside what discovery publishes.
|
|
207
|
+
* Whole numbers only: a fractional bound is not a page size discovery can honour.
|
|
208
|
+
*/
|
|
209
|
+
const checkPagingBounds = (request) => withinBound(request.resultLimit, 1, limits.maximumPageSize) &&
|
|
210
|
+
withinBound(request.passageLimit, 0, limits.maximumPassagesPerResult) &&
|
|
211
|
+
withinBound(request.passageLength, 1, limits.maximumPassageLength)
|
|
212
|
+
? undefined
|
|
213
|
+
: `Query bounds must keep --limit within 1-${limits.maximumPageSize}, --passages within 0-${limits.maximumPassagesPerResult}, and --passage-length within 1-${limits.maximumPassageLength}`;
|
|
214
|
+
/** Build the canonical query one structured request denotes. */
|
|
215
|
+
export const makeKnowledgeQueryRequest = (request) => {
|
|
216
|
+
const clauses = buildClauses(request);
|
|
217
|
+
if (typeof clauses === "string") {
|
|
218
|
+
return Effect.fail(new KnowledgeRequestInvalid({ detail: clauses }));
|
|
219
|
+
}
|
|
220
|
+
const bounds = checkPagingBounds(request);
|
|
221
|
+
if (bounds !== undefined)
|
|
222
|
+
return Effect.fail(new KnowledgeRequestInvalid({ detail: bounds }));
|
|
223
|
+
return Effect.succeed(makeKnowledgeQuery(request.scope, clauses, paging(request)));
|
|
224
|
+
};
|
|
225
|
+
/** Build the canonical query one lexical search request denotes. */
|
|
226
|
+
export const makeKnowledgeSearchRequest = (request) => {
|
|
227
|
+
const clauses = textClauses(request.expression);
|
|
228
|
+
if (typeof clauses === "string") {
|
|
229
|
+
return Effect.fail(new KnowledgeRequestInvalid({ detail: clauses }));
|
|
230
|
+
}
|
|
231
|
+
const bounds = checkPagingBounds(request);
|
|
232
|
+
if (bounds !== undefined)
|
|
233
|
+
return Effect.fail(new KnowledgeRequestInvalid({ detail: bounds }));
|
|
234
|
+
return Effect.succeed(makeKnowledgeQuery(request.scope, clauses, paging(request)));
|
|
235
|
+
};
|
|
236
|
+
/** Refuse a traversal depth outside the published maximum. */
|
|
237
|
+
export const checkTraversalDepth = (depth) => Number.isSafeInteger(depth) && depth >= 1 && depth <= limits.maximumTraversalDepth
|
|
238
|
+
? Effect.succeed(depth)
|
|
239
|
+
: Effect.fail(new KnowledgeRequestInvalid({
|
|
240
|
+
detail: `Depth must be between 1 and ${limits.maximumTraversalDepth}`,
|
|
241
|
+
}));
|
|
242
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentxm/knowledge-query deterministic fixtures and ports.
|
|
3
|
+
*
|
|
4
|
+
* A throwaway workspace with authored Knowledge bundles, the layer discovery
|
|
5
|
+
* needs over it, and the changing-source port that proves capture refuses an
|
|
6
|
+
* unstable corpus. Production source never imports this module.
|
|
7
|
+
*
|
|
8
|
+
* @experimental This API is unstable and may change without notice.
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as FileSystem from "effect/FileSystem";
|
|
13
|
+
import type { WorkspaceScope } from "@agentxm/extension-model/unstable/workspace-scope";
|
|
14
|
+
export declare const knowledgeBundleFqn = "@acme/knowledge/platform";
|
|
15
|
+
/** An OKF concept document with fixture frontmatter and the given body. */
|
|
16
|
+
export declare const knowledgeDocument: (body: string, frontmatter?: Readonly<Record<string, unknown>>) => string;
|
|
17
|
+
export interface KnowledgeFixtureBundle {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly enabled?: boolean;
|
|
20
|
+
readonly instructionEntry?: boolean;
|
|
21
|
+
readonly documents?: Readonly<Record<string, string>>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A project workspace whose Knowledge bundles are authored in place, exactly
|
|
25
|
+
* as `axm knowledge new` leaves them.
|
|
26
|
+
*/
|
|
27
|
+
export declare const makeKnowledgeFixtureWorkspace: (options?: {
|
|
28
|
+
readonly bundles?: ReadonlyArray<KnowledgeFixtureBundle>;
|
|
29
|
+
readonly scope?: WorkspaceScope;
|
|
30
|
+
}) => {
|
|
31
|
+
root: string;
|
|
32
|
+
sourcePath: (bundle: string, relativePath: string) => string;
|
|
33
|
+
writeDocument: (relativePath: string, content: string, bundle?: string) => void;
|
|
34
|
+
readFile: (relativePath: string) => string;
|
|
35
|
+
readSettings: () => string;
|
|
36
|
+
readLockfileText: () => string;
|
|
37
|
+
snapshot: () => ReadonlyArray<readonly [string, string]>;
|
|
38
|
+
provide: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | import("@agentxm/workspace-state").WorkspaceMutationsError, FileSystem.FileSystem | import("effect/Path").Path | Exclude<R, import("./knowledge-index.ts").KnowledgeIndex | import("@agentxm/workspace-state/live").WorkspaceStateServices | import("@agentxm/workspace-state").WorkspaceMutations>>;
|
|
39
|
+
cleanup: () => void;
|
|
40
|
+
};
|
|
41
|
+
export type KnowledgeFixtureWorkspace = ReturnType<typeof makeKnowledgeFixtureWorkspace>;
|
|
42
|
+
/** Capture the fixture workspace's corpus and return its snapshot. */
|
|
43
|
+
export declare const captureFixtureSnapshot: (workspace: KnowledgeFixtureWorkspace) => Effect.Effect<import("./knowledge-index.ts").KnowledgeIndexSnapshot, import("./errors.ts").KnowledgeCorpusUnavailable | import("@agentxm/workspace-state").WorkspaceMutationsError, FileSystem.FileSystem | import("effect/Path").Path>;
|
|
44
|
+
/** Each capture read sees another version through the production filesystem port. */
|
|
45
|
+
export declare const withChangingKnowledgeReads: <A, E, R>(program: Effect.Effect<A, E, R>) => Effect.Effect<A, E, FileSystem.FileSystem | Exclude<R, FileSystem.FileSystem>>;
|
|
46
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentxm/knowledge-query deterministic fixtures and ports.
|
|
3
|
+
*
|
|
4
|
+
* A throwaway workspace with authored Knowledge bundles, the layer discovery
|
|
5
|
+
* needs over it, and the changing-source port that proves capture refuses an
|
|
6
|
+
* unstable corpus. Production source never imports this module.
|
|
7
|
+
*
|
|
8
|
+
* @experimental This API is unstable and may change without notice.
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as nodePath from "node:path";
|
|
14
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as FileSystem from "effect/FileSystem";
|
|
17
|
+
import * as Layer from "effect/Layer";
|
|
18
|
+
import * as Ref from "effect/Ref";
|
|
19
|
+
import { decodeAbsolutePathSync } from "@agentxm/extension-model/unstable/path-types";
|
|
20
|
+
import { WorkspaceStateLive } from "@agentxm/workspace-state/live";
|
|
21
|
+
import { captureInstalledKnowledgeCorpus } from "./corpus/installed-corpus.js";
|
|
22
|
+
import { KnowledgeIndexLive } from "./live.js";
|
|
23
|
+
export const knowledgeBundleFqn = "@acme/knowledge/platform";
|
|
24
|
+
/** Minimal YAML emitter for fixture frontmatter: scalars and string arrays. */
|
|
25
|
+
const yamlFrontmatter = (values) => Object.entries(values)
|
|
26
|
+
.map(([key, value]) => Array.isArray(value)
|
|
27
|
+
? `${key}:\n${value.map((item) => ` - ${JSON.stringify(item)}`).join("\n")}\n`
|
|
28
|
+
: `${key}: ${JSON.stringify(value)}\n`)
|
|
29
|
+
.join("");
|
|
30
|
+
/** An OKF concept document with fixture frontmatter and the given body. */
|
|
31
|
+
export const knowledgeDocument = (body, frontmatter = {}) => `---\n${yamlFrontmatter({ type: "guide", description: "Fixture guidance", tags: ["fixture"], ...frontmatter })}---\n${body}`;
|
|
32
|
+
/**
|
|
33
|
+
* The workspace-state services over the fixture, with a hermetic user home so
|
|
34
|
+
* the machine's real home is never read.
|
|
35
|
+
*/
|
|
36
|
+
const makeKnowledgeFixtureLayer = (root, home, scope) => Layer.provideMerge(Layer.merge(WorkspaceStateLive({
|
|
37
|
+
scope,
|
|
38
|
+
projectRoot: decodeAbsolutePathSync(root),
|
|
39
|
+
allowUninitialized: true,
|
|
40
|
+
}), KnowledgeIndexLive), ConfigProvider.layer(ConfigProvider.fromEnv({ env: { AXM_USER_HOME: home } })));
|
|
41
|
+
/**
|
|
42
|
+
* A project workspace whose Knowledge bundles are authored in place, exactly
|
|
43
|
+
* as `axm knowledge new` leaves them.
|
|
44
|
+
*/
|
|
45
|
+
export const makeKnowledgeFixtureWorkspace = (options = {}) => {
|
|
46
|
+
const bundles = options.bundles ?? [{ name: "platform", documents: {} }];
|
|
47
|
+
const root = fs.realpathSync(fs.mkdtempSync(nodePath.join(os.tmpdir(), "axm-knowledge-")));
|
|
48
|
+
const home = fs.realpathSync(fs.mkdtempSync(nodePath.join(os.tmpdir(), "axm-knowledge-home-")));
|
|
49
|
+
fs.mkdirSync(nodePath.join(root, ".axm"), { recursive: true });
|
|
50
|
+
fs.mkdirSync(nodePath.join(home, ".axm", "workspace"), { recursive: true });
|
|
51
|
+
const sourcePath = (bundle, relativePath) => nodePath.join(root, "knowledge", bundle, "src", relativePath);
|
|
52
|
+
const writeDocument = (relativePath, content, bundle = "platform") => {
|
|
53
|
+
const file = sourcePath(bundle, relativePath);
|
|
54
|
+
fs.mkdirSync(nodePath.dirname(file), { recursive: true });
|
|
55
|
+
fs.writeFileSync(file, content);
|
|
56
|
+
};
|
|
57
|
+
fs.writeFileSync(nodePath.join(root, "axm.json"), JSON.stringify({
|
|
58
|
+
agents: [],
|
|
59
|
+
// A workspace-sourced bundle names its owner through the workspace's own.
|
|
60
|
+
owner: "@acme",
|
|
61
|
+
knowledge: Object.fromEntries(bundles.map((bundle) => [
|
|
62
|
+
bundle.name,
|
|
63
|
+
{
|
|
64
|
+
source: "workspace",
|
|
65
|
+
enabled: bundle.enabled ?? true,
|
|
66
|
+
...(bundle.instructionEntry === undefined
|
|
67
|
+
? {}
|
|
68
|
+
: { instructionEntry: bundle.instructionEntry }),
|
|
69
|
+
},
|
|
70
|
+
])),
|
|
71
|
+
}, null, 2));
|
|
72
|
+
// JSON is valid YAML, so the lockfile fixture needs no emitter.
|
|
73
|
+
fs.writeFileSync(nodePath.join(root, "axm-lock.yaml"), JSON.stringify({ lockfileVersion: 7, skills: {} }));
|
|
74
|
+
for (const bundle of bundles) {
|
|
75
|
+
const bundlePath = nodePath.join(root, "knowledge", bundle.name);
|
|
76
|
+
fs.mkdirSync(nodePath.join(bundlePath, "src"), { recursive: true });
|
|
77
|
+
fs.writeFileSync(nodePath.join(bundlePath, "knowledge.json"), JSON.stringify({
|
|
78
|
+
owner: "@acme",
|
|
79
|
+
type: "knowledge",
|
|
80
|
+
name: bundle.name,
|
|
81
|
+
version: "1.0.0",
|
|
82
|
+
description: "Fixture Knowledge bundle",
|
|
83
|
+
format: { name: "okf", version: "0.2" },
|
|
84
|
+
bundleRoot: "src",
|
|
85
|
+
}));
|
|
86
|
+
writeDocument("index.md", '---\nokf_version: "0.2"\n---\n# Fixture knowledge\n', bundle.name);
|
|
87
|
+
for (const [relativePath, content] of Object.entries(bundle.documents ?? {}))
|
|
88
|
+
writeDocument(relativePath, content, bundle.name);
|
|
89
|
+
}
|
|
90
|
+
const readFile = (relativePath) => fs.readFileSync(nodePath.join(root, relativePath), "utf8");
|
|
91
|
+
/** Every file under the workspace, so a read-only operation can be shown to write nothing. */
|
|
92
|
+
const snapshot = () => {
|
|
93
|
+
const entries = [];
|
|
94
|
+
const walk = (directory) => {
|
|
95
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
96
|
+
const absolute = nodePath.join(directory, entry.name);
|
|
97
|
+
if (entry.isDirectory())
|
|
98
|
+
walk(absolute);
|
|
99
|
+
else
|
|
100
|
+
entries.push([nodePath.relative(root, absolute), fs.readFileSync(absolute, "utf8")]);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
walk(root);
|
|
104
|
+
return entries.sort((left, right) => left[0].localeCompare(right[0]));
|
|
105
|
+
};
|
|
106
|
+
const layer = makeKnowledgeFixtureLayer(root, home, options.scope ?? "project");
|
|
107
|
+
return {
|
|
108
|
+
root,
|
|
109
|
+
sourcePath,
|
|
110
|
+
writeDocument,
|
|
111
|
+
readFile,
|
|
112
|
+
readSettings: () => readFile("axm.json"),
|
|
113
|
+
readLockfileText: () => readFile("axm-lock.yaml"),
|
|
114
|
+
snapshot,
|
|
115
|
+
provide: (effect) => Effect.provide(effect, layer),
|
|
116
|
+
cleanup: () => {
|
|
117
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
118
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
/** Capture the fixture workspace's corpus and return its snapshot. */
|
|
123
|
+
export const captureFixtureSnapshot = (workspace) => workspace.provide(captureInstalledKnowledgeCorpus().pipe(Effect.flatMap((captured) => captured.outcome === "ready"
|
|
124
|
+
? Effect.succeed(captured.snapshot)
|
|
125
|
+
: Effect.die("fixture corpus kept changing"))));
|
|
126
|
+
/** Each capture read sees another version through the production filesystem port. */
|
|
127
|
+
export const withChangingKnowledgeReads = (program) => Effect.gen(function* () {
|
|
128
|
+
const filesystem = yield* FileSystem.FileSystem;
|
|
129
|
+
const reads = yield* Ref.make(0);
|
|
130
|
+
return yield* program.pipe(Effect.provideService(FileSystem.FileSystem, {
|
|
131
|
+
...filesystem,
|
|
132
|
+
readFile: (filename) => filesystem
|
|
133
|
+
.readFile(filename)
|
|
134
|
+
.pipe(Effect.flatMap((bytes) => filename.endsWith(".md")
|
|
135
|
+
? Ref.getAndUpdate(reads, (count) => count + 1).pipe(Effect.map((count) => new TextEncoder().encode(`${new TextDecoder().decode(bytes)}\nCapture version ${count}\n`)))
|
|
136
|
+
: Effect.succeed(bytes))),
|
|
137
|
+
}));
|
|
138
|
+
});
|
|
139
|
+
//# sourceMappingURL=testing.js.map
|
package/package.json
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
"url": "https://github.com/agentxm/axm/issues"
|
|
5
5
|
},
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@agentxm/extension-
|
|
8
|
-
"@agentxm/
|
|
7
|
+
"@agentxm/extension-content": "^0.28.13",
|
|
8
|
+
"@agentxm/extension-model": "^0.28.13",
|
|
9
|
+
"@agentxm/workspace-projection": "^0.28.13",
|
|
10
|
+
"@agentxm/workspace-state": "^0.28.13",
|
|
9
11
|
"effect": "4.0.0-rc.112"
|
|
10
12
|
},
|
|
11
13
|
"description": "AXM knowledge-query feature: Knowledge concept resolution, retrieval, search, related concepts, and status for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
|
|
12
14
|
"devDependencies": {
|
|
15
|
+
"@agentxm/specification-metadata": "0.28.13",
|
|
13
16
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
14
17
|
"@effect/vitest": "4.0.0-rc.112",
|
|
15
18
|
"@types/bun": "^1.3.14",
|
|
@@ -30,6 +33,11 @@
|
|
|
30
33
|
"axm-source": "./src/live.ts",
|
|
31
34
|
"default": "./dist/src/live.js",
|
|
32
35
|
"types": "./dist/src/live.d.ts"
|
|
36
|
+
},
|
|
37
|
+
"./testing": {
|
|
38
|
+
"axm-source": "./src/testing.ts",
|
|
39
|
+
"default": "./dist/src/testing.js",
|
|
40
|
+
"types": "./dist/src/testing.d.ts"
|
|
33
41
|
}
|
|
34
42
|
},
|
|
35
43
|
"files": [
|
|
@@ -46,11 +54,11 @@
|
|
|
46
54
|
"access": "public"
|
|
47
55
|
},
|
|
48
56
|
"repository": {
|
|
49
|
-
"directory": "packages/knowledge-query",
|
|
57
|
+
"directory": "packages/core/knowledge-query",
|
|
50
58
|
"type": "git",
|
|
51
59
|
"url": "https://github.com/agentxm/axm.git"
|
|
52
60
|
},
|
|
53
61
|
"sideEffects": false,
|
|
54
62
|
"type": "module",
|
|
55
|
-
"version": "0.28.
|
|
56
|
-
}
|
|
63
|
+
"version": "0.28.13"
|
|
64
|
+
}
|