@agentxm/registry-protocol 0.28.4-bootstrap.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 +110 -0
- package/README.md +13 -0
- package/dist/src/unstable/content/agent-skills-standard.d.ts +17 -0
- package/dist/src/unstable/content/agent-skills-standard.js +24 -0
- package/dist/src/unstable/content/frontmatter.d.ts +52 -0
- package/dist/src/unstable/content/frontmatter.js +115 -0
- package/dist/src/unstable/content/index.d.ts +11 -0
- package/dist/src/unstable/content/index.js +11 -0
- package/dist/src/unstable/content/skill-content.d.ts +43 -0
- package/dist/src/unstable/content/skill-content.js +184 -0
- package/dist/src/unstable/content/skill-types.d.ts +22 -0
- package/dist/src/unstable/content/skill-types.js +9 -0
- package/dist/src/unstable/content/subagent-content.d.ts +58 -0
- package/dist/src/unstable/content/subagent-content.js +101 -0
- package/dist/src/unstable/knowledge/index.d.ts +8 -0
- package/dist/src/unstable/knowledge/index.js +8 -0
- package/dist/src/unstable/knowledge/knowledge-search.d.ts +32 -0
- package/dist/src/unstable/knowledge/knowledge-search.js +147 -0
- package/dist/src/unstable/knowledge/okf.d.ts +86 -0
- package/dist/src/unstable/knowledge/okf.js +1090 -0
- package/dist/src/unstable/lint/catalog/hook/decision-portability.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/hook/decision-portability.js +40 -0
- package/dist/src/unstable/lint/catalog/hook/entrypoint-exists.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/hook/entrypoint-exists.js +39 -0
- package/dist/src/unstable/lint/catalog/hook/envelope.d.ts +6 -0
- package/dist/src/unstable/lint/catalog/hook/envelope.js +15 -0
- package/dist/src/unstable/lint/catalog/hook/matcher-raw-portability.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/hook/matcher-raw-portability.js +51 -0
- package/dist/src/unstable/lint/catalog/hook.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/hook.js +17 -0
- package/dist/src/unstable/lint/catalog/knowledge/diagnostics.d.ts +128 -0
- package/dist/src/unstable/lint/catalog/knowledge/diagnostics.js +78 -0
- package/dist/src/unstable/lint/catalog/knowledge/envelope.d.ts +6 -0
- package/dist/src/unstable/lint/catalog/knowledge/envelope.js +15 -0
- package/dist/src/unstable/lint/catalog/knowledge.d.ts +5 -0
- package/dist/src/unstable/lint/catalog/knowledge.js +11 -0
- package/dist/src/unstable/lint/catalog/mcp-server/envelope.d.ts +6 -0
- package/dist/src/unstable/lint/catalog/mcp-server/envelope.js +15 -0
- package/dist/src/unstable/lint/catalog/mcp-server.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/mcp-server.js +6 -0
- package/dist/src/unstable/lint/catalog/pack/manifest-keys-recognized.d.ts +28 -0
- package/dist/src/unstable/lint/catalog/pack/manifest-keys-recognized.js +38 -0
- package/dist/src/unstable/lint/catalog/pack/manifest-present.d.ts +17 -0
- package/dist/src/unstable/lint/catalog/pack/manifest-present.js +24 -0
- package/dist/src/unstable/lint/catalog/pack/manifest-schema-valid.d.ts +25 -0
- package/dist/src/unstable/lint/catalog/pack/manifest-schema-valid.js +33 -0
- package/dist/src/unstable/lint/catalog/pack-accessor/contexts.d.ts +51 -0
- package/dist/src/unstable/lint/catalog/pack-accessor/contexts.js +45 -0
- package/dist/src/unstable/lint/catalog/pack-accessor/vft.d.ts +60 -0
- package/dist/src/unstable/lint/catalog/pack-accessor/vft.js +85 -0
- package/dist/src/unstable/lint/catalog/pack.d.ts +37 -0
- package/dist/src/unstable/lint/catalog/pack.js +47 -0
- package/dist/src/unstable/lint/catalog/rule/envelope.d.ts +6 -0
- package/dist/src/unstable/lint/catalog/rule/envelope.js +15 -0
- package/dist/src/unstable/lint/catalog/rule.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/rule.js +6 -0
- package/dist/src/unstable/lint/catalog/shared/envelope-rules.d.ts +93 -0
- package/dist/src/unstable/lint/catalog/shared/envelope-rules.js +89 -0
- package/dist/src/unstable/lint/catalog/shared/manifest-json.d.ts +18 -0
- package/dist/src/unstable/lint/catalog/shared/manifest-json.js +28 -0
- package/dist/src/unstable/lint/catalog/shared/manifest-present.d.ts +25 -0
- package/dist/src/unstable/lint/catalog/shared/manifest-present.js +37 -0
- package/dist/src/unstable/lint/catalog/shared/recommended-packs-rules.d.ts +70 -0
- package/dist/src/unstable/lint/catalog/shared/recommended-packs-rules.js +139 -0
- package/dist/src/unstable/lint/catalog/shared/schema-rule.d.ts +58 -0
- package/dist/src/unstable/lint/catalog/shared/schema-rule.js +103 -0
- package/dist/src/unstable/lint/catalog/skill/frontmatter-parseable.d.ts +31 -0
- package/dist/src/unstable/lint/catalog/skill/frontmatter-parseable.js +148 -0
- package/dist/src/unstable/lint/catalog/skill/frontmatter-standard-valid.d.ts +13 -0
- package/dist/src/unstable/lint/catalog/skill/frontmatter-standard-valid.js +61 -0
- package/dist/src/unstable/lint/catalog/skill/manifest-keys-recognized.d.ts +26 -0
- package/dist/src/unstable/lint/catalog/skill/manifest-keys-recognized.js +41 -0
- package/dist/src/unstable/lint/catalog/skill/manifest-present.d.ts +23 -0
- package/dist/src/unstable/lint/catalog/skill/manifest-present.js +32 -0
- package/dist/src/unstable/lint/catalog/skill/manifest-schema-valid.d.ts +21 -0
- package/dist/src/unstable/lint/catalog/skill/manifest-schema-valid.js +36 -0
- package/dist/src/unstable/lint/catalog/skill/recommended-packs-valid.d.ts +8 -0
- package/dist/src/unstable/lint/catalog/skill/recommended-packs-valid.js +12 -0
- package/dist/src/unstable/lint/catalog/skill/skill-md-present.d.ts +17 -0
- package/dist/src/unstable/lint/catalog/skill/skill-md-present.js +37 -0
- package/dist/src/unstable/lint/catalog/skill/standalone-declaration-valid.d.ts +8 -0
- package/dist/src/unstable/lint/catalog/skill/standalone-declaration-valid.js +12 -0
- package/dist/src/unstable/lint/catalog/skill-accessor/contexts.d.ts +61 -0
- package/dist/src/unstable/lint/catalog/skill-accessor/contexts.js +55 -0
- package/dist/src/unstable/lint/catalog/skill-accessor/vft.d.ts +77 -0
- package/dist/src/unstable/lint/catalog/skill-accessor/vft.js +135 -0
- package/dist/src/unstable/lint/catalog/skill.d.ts +44 -0
- package/dist/src/unstable/lint/catalog/skill.js +64 -0
- package/dist/src/unstable/lint/catalog/subagent/envelope.d.ts +6 -0
- package/dist/src/unstable/lint/catalog/subagent/envelope.js +15 -0
- package/dist/src/unstable/lint/catalog/subagent.d.ts +4 -0
- package/dist/src/unstable/lint/catalog/subagent.js +6 -0
- package/dist/src/unstable/lint/compose-path.d.ts +28 -0
- package/dist/src/unstable/lint/compose-path.js +64 -0
- package/dist/src/unstable/lint/config.d.ts +103 -0
- package/dist/src/unstable/lint/config.js +140 -0
- package/dist/src/unstable/lint/context.d.ts +246 -0
- package/dist/src/unstable/lint/context.js +21 -0
- package/dist/src/unstable/lint/describe-document.d.ts +31 -0
- package/dist/src/unstable/lint/describe-document.js +50 -0
- package/dist/src/unstable/lint/evaluate.d.ts +47 -0
- package/dist/src/unstable/lint/evaluate.js +82 -0
- package/dist/src/unstable/lint/issues-to-findings.d.ts +35 -0
- package/dist/src/unstable/lint/issues-to-findings.js +149 -0
- package/dist/src/unstable/lint/publish.d.ts +49 -0
- package/dist/src/unstable/lint/publish.js +86 -0
- package/dist/src/unstable/lint/rule.d.ts +98 -0
- package/dist/src/unstable/lint/rule.js +18 -0
- package/dist/src/unstable/publish/archive-guardrails.d.ts +39 -0
- package/dist/src/unstable/publish/archive-guardrails.js +250 -0
- package/dist/src/unstable/publish/filtered-package-validation.d.ts +24 -0
- package/dist/src/unstable/publish/filtered-package-validation.js +117 -0
- package/dist/src/unstable/publish/index.d.ts +12 -0
- package/dist/src/unstable/publish/index.js +12 -0
- package/dist/src/unstable/publish/ingest-limits.d.ts +25 -0
- package/dist/src/unstable/publish/ingest-limits.js +41 -0
- package/dist/src/unstable/publish/input-normalization.d.ts +40 -0
- package/dist/src/unstable/publish/input-normalization.js +125 -0
- package/dist/src/unstable/publish/manifest-policy.d.ts +611 -0
- package/dist/src/unstable/publish/manifest-policy.js +196 -0
- package/dist/src/unstable/publish/test-zip-helpers.d.ts +13 -0
- package/dist/src/unstable/publish/test-zip-helpers.js +117 -0
- package/dist/src/unstable/publish/visibility.d.ts +161 -0
- package/dist/src/unstable/publish/visibility.js +124 -0
- package/dist/src/unstable/registry/discover-schema.d.ts +68 -0
- package/dist/src/unstable/registry/discover-schema.js +35 -0
- package/dist/src/unstable/registry/index.d.ts +9 -0
- package/dist/src/unstable/registry/index.js +9 -0
- package/dist/src/unstable/registry/publication-set.d.ts +303 -0
- package/dist/src/unstable/registry/publication-set.js +507 -0
- package/dist/src/unstable/registry/release-age-policy.d.ts +57 -0
- package/dist/src/unstable/registry/release-age-policy.js +117 -0
- package/dist/src/unstable/registry/schema.d.ts +220 -0
- package/dist/src/unstable/registry/schema.js +114 -0
- package/dist/src/unstable/suggested-action.d.ts +19 -0
- package/dist/src/unstable/suggested-action.js +42 -0
- package/package.json +214 -0
|
@@ -0,0 +1,1090 @@
|
|
|
1
|
+
// @effect-diagnostics anyUnknownInErrorContext:off — OKF inspection accepts opaque accessor errors and converts them to diagnostics
|
|
2
|
+
/** Open Knowledge Format discovery and AgentXM-profile validation (v0.2). */
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
import * as Path from "effect/Path";
|
|
6
|
+
import * as Option from "effect/Option";
|
|
7
|
+
import * as Result from "effect/Result";
|
|
8
|
+
import { FrontmatterParseFailure, parseFrontmatterEffect } from "../content/frontmatter.js";
|
|
9
|
+
import { matchesKnowledgeSearchQuery } from "./knowledge-search.js";
|
|
10
|
+
export const KNOWLEDGE_DIAGNOSTIC_CODES = [
|
|
11
|
+
"bundle-too-large",
|
|
12
|
+
"file-too-large",
|
|
13
|
+
"invalid-tags",
|
|
14
|
+
"missing-root-index",
|
|
15
|
+
"missing-okf-version",
|
|
16
|
+
"missing-title",
|
|
17
|
+
"missing-description",
|
|
18
|
+
"missing-manifest-description",
|
|
19
|
+
"empty-bundle",
|
|
20
|
+
"missing-tags",
|
|
21
|
+
"symbolic-link",
|
|
22
|
+
"too-many-files",
|
|
23
|
+
"unsupported-okf-version",
|
|
24
|
+
"missing-type",
|
|
25
|
+
"invalid-frontmatter",
|
|
26
|
+
"case-collision",
|
|
27
|
+
"dangerous-uri",
|
|
28
|
+
"detected-secret",
|
|
29
|
+
"unsafe-path",
|
|
30
|
+
"invalid-index",
|
|
31
|
+
"invalid-log",
|
|
32
|
+
"invalid-resource",
|
|
33
|
+
"escaping-resource",
|
|
34
|
+
"unresolved-resource",
|
|
35
|
+
"broken-internal-link",
|
|
36
|
+
"escaping-link",
|
|
37
|
+
"unreachable-concept",
|
|
38
|
+
"missing-index-entry",
|
|
39
|
+
"stale-index-entry",
|
|
40
|
+
"embedded-html",
|
|
41
|
+
"duplicate-resource",
|
|
42
|
+
"inconsistent-type",
|
|
43
|
+
"large-concept",
|
|
44
|
+
"large-index",
|
|
45
|
+
"unreferenced-asset",
|
|
46
|
+
"invalid-sources",
|
|
47
|
+
"invalid-generated",
|
|
48
|
+
"invalid-verified",
|
|
49
|
+
"invalid-status",
|
|
50
|
+
"invalid-stale-after",
|
|
51
|
+
"invalid-attestation",
|
|
52
|
+
];
|
|
53
|
+
/** Convert a typed frontmatter failure into the stable Knowledge diagnostic contract. */
|
|
54
|
+
export const frontmatterParseDiagnostic = (relativePath, failure) => ({
|
|
55
|
+
code: "invalid-frontmatter",
|
|
56
|
+
severity: "error",
|
|
57
|
+
relativePath,
|
|
58
|
+
...(failure.line === undefined ? {} : { line: failure.line }),
|
|
59
|
+
...(failure.column === undefined ? {} : { column: failure.column }),
|
|
60
|
+
message: `Invalid YAML frontmatter: ${failure.reason}`,
|
|
61
|
+
details: { kind: "frontmatter-parse", reason: failure.reason },
|
|
62
|
+
});
|
|
63
|
+
const RESERVED_BASENAMES = new Set(["index.md", "log.md"]);
|
|
64
|
+
const OKF_VERSION = "0.2";
|
|
65
|
+
const CONCEPT_STATUSES = new Set(["draft", "stable", "deprecated"]);
|
|
66
|
+
const ATTESTED_COMPUTATION_TYPE = "attested computation";
|
|
67
|
+
const MAX_FILE_BYTES = 1024n * 1024n;
|
|
68
|
+
const MAX_BUNDLE_BYTES = 10n * 1024n * 1024n;
|
|
69
|
+
const MAX_BUNDLE_FILES = 1_000;
|
|
70
|
+
const LARGE_CONCEPT_BYTES = 128n * 1024n;
|
|
71
|
+
const LARGE_INDEX_BYTES = 256n * 1024n;
|
|
72
|
+
const bundleEntries = (root, current) => Effect.gen(function* () {
|
|
73
|
+
const fs = yield* FileSystem.FileSystem;
|
|
74
|
+
const path = yield* Path.Path;
|
|
75
|
+
const entries = [...(yield* fs.readDirectory(current))].sort();
|
|
76
|
+
const files = [];
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const absolute = path.join(current, entry);
|
|
79
|
+
// fs.stat follows symlinks, so detect them first (readLink succeeds only
|
|
80
|
+
// for a symlink) and record them without following. Otherwise a directory
|
|
81
|
+
// symlink would be recursed into (escaping the bundle or cycling) and the
|
|
82
|
+
// symbolic-link safety check would never fire.
|
|
83
|
+
const link = yield* fs.readLink(absolute).pipe(Effect.option);
|
|
84
|
+
if (Option.isSome(link)) {
|
|
85
|
+
files.push({
|
|
86
|
+
relativePath: path.relative(root, absolute).replace(/\\/g, "/"),
|
|
87
|
+
type: "SymbolicLink",
|
|
88
|
+
size: 0n,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const stat = yield* fs.stat(absolute);
|
|
93
|
+
if (stat.type === "Directory") {
|
|
94
|
+
files.push(...(yield* bundleEntries(root, absolute)));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
files.push({
|
|
98
|
+
relativePath: path.relative(root, absolute).replace(/\\/g, "/"),
|
|
99
|
+
type: stat.type,
|
|
100
|
+
size: stat.size,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return files;
|
|
105
|
+
});
|
|
106
|
+
/** Collect a bundle's normalized entry inventory without following symbolic links. */
|
|
107
|
+
export const collectKnowledgeBundleEntries = (sourceRoot) => bundleEntries(sourceRoot, sourceRoot);
|
|
108
|
+
const firstHeading = (body) => {
|
|
109
|
+
for (const line of body.split(/\r?\n/)) {
|
|
110
|
+
const match = /^#\s+(.+?)\s*$/.exec(line);
|
|
111
|
+
if (match?.[1] !== undefined)
|
|
112
|
+
return match[1];
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
};
|
|
116
|
+
const conceptId = (relativePath) => relativePath.replace(/\.md$/i, "");
|
|
117
|
+
const markdownLinks = (body) => {
|
|
118
|
+
const links = [];
|
|
119
|
+
const pattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
120
|
+
for (const match of body.matchAll(pattern)) {
|
|
121
|
+
const target = match[1];
|
|
122
|
+
if (target === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
const offset = match.index ?? 0;
|
|
125
|
+
links.push({
|
|
126
|
+
target,
|
|
127
|
+
line: body.slice(0, offset).split(/\r?\n/).length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return links;
|
|
131
|
+
};
|
|
132
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
+
const metadataField = (metadata, key) => {
|
|
134
|
+
if (typeof metadata !== "object" || metadata === null)
|
|
135
|
+
return undefined;
|
|
136
|
+
return Object.entries(metadata).find(([entryKey]) => entryKey === key)?.[1];
|
|
137
|
+
};
|
|
138
|
+
const normalizedPath = (sourcePath, target) => {
|
|
139
|
+
const pathOnly = (target.split("#", 1)[0] ?? "").split("?", 1)[0] ?? "";
|
|
140
|
+
if (pathOnly.length === 0 || pathOnly.startsWith("//") || /^[a-z][a-z0-9+.-]*:/i.test(pathOnly)) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const decoded = (() => {
|
|
144
|
+
try {
|
|
145
|
+
return decodeURIComponent(pathOnly);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return pathOnly;
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
151
|
+
const sourceSegments = sourcePath.split("/").slice(0, -1);
|
|
152
|
+
const segments = decoded.startsWith("/")
|
|
153
|
+
? decoded.slice(1).split("/")
|
|
154
|
+
: [...sourceSegments, ...decoded.split("/")];
|
|
155
|
+
const resolved = [];
|
|
156
|
+
let escaped = false;
|
|
157
|
+
for (const segment of segments) {
|
|
158
|
+
if (segment.length === 0 || segment === ".")
|
|
159
|
+
continue;
|
|
160
|
+
if (segment === "..") {
|
|
161
|
+
if (resolved.length === 0)
|
|
162
|
+
escaped = true;
|
|
163
|
+
else
|
|
164
|
+
resolved.pop();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
resolved.push(segment);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { path: resolved.join("/"), escaped };
|
|
171
|
+
};
|
|
172
|
+
const activeScheme = (target) => /^(?:javascript|vbscript|data):/i.test(target.trim());
|
|
173
|
+
const schemeLike = (target) => /^[a-z][a-z0-9+.-]*:/i.test(target);
|
|
174
|
+
const resourcePathPortion = (target) => {
|
|
175
|
+
const query = target.indexOf("?");
|
|
176
|
+
const fragment = target.indexOf("#");
|
|
177
|
+
const suffix = [query, fragment]
|
|
178
|
+
.filter((index) => index >= 0)
|
|
179
|
+
.sort((left, right) => left - right)[0];
|
|
180
|
+
return suffix === undefined ? target : target.slice(0, suffix);
|
|
181
|
+
};
|
|
182
|
+
const resourceSuffix = (target) => target.slice(resourcePathPortion(target).length);
|
|
183
|
+
const classifyResource = (value, allowScope) => {
|
|
184
|
+
const target = value.trim();
|
|
185
|
+
if (target.length === 0)
|
|
186
|
+
return "invalid";
|
|
187
|
+
if (schemeLike(target)) {
|
|
188
|
+
if (activeScheme(target))
|
|
189
|
+
return "invalid";
|
|
190
|
+
try {
|
|
191
|
+
return new URL(target).protocol.length > 1 ? "url" : "invalid";
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return "invalid";
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!allowScope)
|
|
198
|
+
return "path";
|
|
199
|
+
const pathPortion = resourcePathPortion(target);
|
|
200
|
+
return pathPortion.startsWith("/") ||
|
|
201
|
+
pathPortion.startsWith("./") ||
|
|
202
|
+
pathPortion.startsWith("../") ||
|
|
203
|
+
pathPortion.includes("/") ||
|
|
204
|
+
(/^\S+$/.test(pathPortion) && /\.[a-z0-9]{1,8}$/i.test(pathPortion))
|
|
205
|
+
? "path"
|
|
206
|
+
: "scope";
|
|
207
|
+
};
|
|
208
|
+
const inspectResource = (value, sourcePath, allFilePaths, options) => {
|
|
209
|
+
const target = value.trim();
|
|
210
|
+
const kind = classifyResource(value, options.allowScope);
|
|
211
|
+
if (kind === "invalid") {
|
|
212
|
+
return {
|
|
213
|
+
kind,
|
|
214
|
+
issue: {
|
|
215
|
+
code: options.invalidCode,
|
|
216
|
+
severity: "error",
|
|
217
|
+
message: `${options.label} must be a safe absolute URI${options.allowScope ? ", contained path, or scope description" : " or contained path"}.`,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (kind === "url")
|
|
222
|
+
return { kind, identity: value };
|
|
223
|
+
if (kind === "scope")
|
|
224
|
+
return { kind };
|
|
225
|
+
const normalized = normalizedPath(sourcePath, target);
|
|
226
|
+
if (normalized === null) {
|
|
227
|
+
return {
|
|
228
|
+
kind: "invalid",
|
|
229
|
+
issue: {
|
|
230
|
+
code: options.invalidCode,
|
|
231
|
+
severity: "error",
|
|
232
|
+
message: `${options.label} must be a safe absolute URI${options.allowScope ? ", contained path, or scope description" : " or contained path"}.`,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (normalized.escaped) {
|
|
237
|
+
return {
|
|
238
|
+
kind,
|
|
239
|
+
issue: {
|
|
240
|
+
code: "escaping-resource",
|
|
241
|
+
severity: "error",
|
|
242
|
+
message: `${options.label} escapes the Knowledge bundle.`,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const identity = `${normalized.path}${resourceSuffix(target)}`;
|
|
247
|
+
if (!allFilePaths.has(normalized.path)) {
|
|
248
|
+
return {
|
|
249
|
+
kind,
|
|
250
|
+
identity,
|
|
251
|
+
issue: {
|
|
252
|
+
code: "unresolved-resource",
|
|
253
|
+
severity: "warning",
|
|
254
|
+
message: `${options.label} points to missing bundle path ${resourcePathPortion(target)}.`,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { kind, identity };
|
|
259
|
+
};
|
|
260
|
+
const validIsoTimestamp = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value) &&
|
|
261
|
+
!Number.isNaN(Date.parse(value));
|
|
262
|
+
const validIsoDate = (value) => /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`));
|
|
263
|
+
/**
|
|
264
|
+
* OKF 0.2 actor convention: `<producer>/<version>` for agents and tools,
|
|
265
|
+
* `human:<id>` for people, and `process:<id>` for automated processes.
|
|
266
|
+
*/
|
|
267
|
+
const validActor = (value) => {
|
|
268
|
+
if (value.length === 0 || /\s/.test(value))
|
|
269
|
+
return false;
|
|
270
|
+
const prefixed = /^(?:human|process):(.+)$/.exec(value);
|
|
271
|
+
if (prefixed !== null)
|
|
272
|
+
return (prefixed[1]?.length ?? 0) > 0;
|
|
273
|
+
const segments = value.split("/");
|
|
274
|
+
return segments.length === 2 && segments.every((segment) => segment.length > 0);
|
|
275
|
+
};
|
|
276
|
+
const dateIssue = (value, label) => typeof value === "string" && validIsoDate(value)
|
|
277
|
+
? undefined
|
|
278
|
+
: `${label} must be an ISO 8601 YYYY-MM-DD date.`;
|
|
279
|
+
/** Validate one `{ by, at }` actor record shared by `generated` and `verified`. */
|
|
280
|
+
const actorRecordIssues = (value, label) => {
|
|
281
|
+
if (!isRecord(value))
|
|
282
|
+
return [`${label} must be a mapping with a by field.`];
|
|
283
|
+
const issues = [];
|
|
284
|
+
const by = value["by"];
|
|
285
|
+
if (typeof by !== "string" || !validActor(by)) {
|
|
286
|
+
issues.push(`${label}.by must use the OKF actor convention (<producer>/<version>, human:<id>, or process:<id>).`);
|
|
287
|
+
}
|
|
288
|
+
const at = value["at"];
|
|
289
|
+
if (at !== undefined && (typeof at !== "string" || !validIsoTimestamp(at))) {
|
|
290
|
+
issues.push(`${label}.at must be an ISO 8601 date-time with a timezone.`);
|
|
291
|
+
}
|
|
292
|
+
return issues;
|
|
293
|
+
};
|
|
294
|
+
const sourcesIssues = (value, relativePath, allFilePaths) => {
|
|
295
|
+
if (!Array.isArray(value)) {
|
|
296
|
+
return [
|
|
297
|
+
{
|
|
298
|
+
code: "invalid-sources",
|
|
299
|
+
severity: "error",
|
|
300
|
+
message: "sources must be a list of source entries.",
|
|
301
|
+
},
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
const issues = [];
|
|
305
|
+
const invalid = (message) => {
|
|
306
|
+
issues.push({ code: "invalid-sources", severity: "error", message });
|
|
307
|
+
};
|
|
308
|
+
const ids = new Map();
|
|
309
|
+
value.forEach((entry, index) => {
|
|
310
|
+
const label = `sources[${index}]`;
|
|
311
|
+
if (!isRecord(entry)) {
|
|
312
|
+
invalid(`${label} must be a mapping.`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const resource = entry["resource"];
|
|
316
|
+
if (typeof resource !== "string") {
|
|
317
|
+
invalid(`${label}.resource is required and must be a safe absolute URI, contained path, or scope description.`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
const inspected = inspectResource(resource, relativePath, allFilePaths, {
|
|
321
|
+
allowScope: true,
|
|
322
|
+
invalidCode: "invalid-sources",
|
|
323
|
+
label: `${label}.resource`,
|
|
324
|
+
});
|
|
325
|
+
if (inspected.issue !== undefined)
|
|
326
|
+
issues.push(inspected.issue);
|
|
327
|
+
}
|
|
328
|
+
const id = entry["id"];
|
|
329
|
+
if (id !== undefined) {
|
|
330
|
+
if (typeof id !== "string" || id.trim().length === 0) {
|
|
331
|
+
invalid(`${label}.id must be a non-empty string.`);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const seen = ids.get(id);
|
|
335
|
+
if (seen !== undefined)
|
|
336
|
+
invalid(`${label}.id duplicates sources[${seen}].id.`);
|
|
337
|
+
else
|
|
338
|
+
ids.set(id, index);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const title = entry["title"];
|
|
342
|
+
if (title !== undefined && typeof title !== "string") {
|
|
343
|
+
invalid(`${label}.title must be a string.`);
|
|
344
|
+
}
|
|
345
|
+
// `author` is a free-form credibility signal, not an actor field; the spec
|
|
346
|
+
// uses values such as `team:ga4-docs` that the actor convention rejects.
|
|
347
|
+
const author = entry["author"];
|
|
348
|
+
if (author !== undefined && (typeof author !== "string" || author.trim().length === 0)) {
|
|
349
|
+
invalid(`${label}.author must be a non-empty string.`);
|
|
350
|
+
}
|
|
351
|
+
const usageCount = entry["usage_count"];
|
|
352
|
+
if (usageCount !== undefined &&
|
|
353
|
+
(typeof usageCount !== "number" || !Number.isInteger(usageCount) || usageCount < 0)) {
|
|
354
|
+
invalid(`${label}.usage_count must be a non-negative integer.`);
|
|
355
|
+
}
|
|
356
|
+
const lastModified = entry["last_modified"];
|
|
357
|
+
if (lastModified !== undefined) {
|
|
358
|
+
const issue = dateIssue(lastModified, `${label}.last_modified`);
|
|
359
|
+
if (issue !== undefined)
|
|
360
|
+
invalid(issue);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return issues;
|
|
364
|
+
};
|
|
365
|
+
const usageWindowIssues = (value) => {
|
|
366
|
+
if (!isRecord(value))
|
|
367
|
+
return ["usage_window must be a mapping with from and to dates."];
|
|
368
|
+
const issues = [];
|
|
369
|
+
for (const key of ["from", "to"]) {
|
|
370
|
+
const issue = dateIssue(value[key], `usage_window.${key}`);
|
|
371
|
+
if (issue !== undefined)
|
|
372
|
+
issues.push(issue);
|
|
373
|
+
}
|
|
374
|
+
return issues;
|
|
375
|
+
};
|
|
376
|
+
const verifiedRecords = (value) => Array.isArray(value) ? value : [value];
|
|
377
|
+
const attestationIssues = (metadata, body) => {
|
|
378
|
+
const issues = [];
|
|
379
|
+
const runtime = metadataField(metadata, "runtime");
|
|
380
|
+
if (typeof runtime !== "string" || runtime.trim().length === 0) {
|
|
381
|
+
issues.push("Attested Computation concepts require a non-empty runtime.");
|
|
382
|
+
}
|
|
383
|
+
const parameters = metadataField(metadata, "parameters");
|
|
384
|
+
if (parameters !== undefined) {
|
|
385
|
+
if (!Array.isArray(parameters)) {
|
|
386
|
+
issues.push("parameters must be a list of typed parameter mappings.");
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
parameters.forEach((parameter, index) => {
|
|
390
|
+
const label = `parameters[${index}]`;
|
|
391
|
+
if (!isRecord(parameter)) {
|
|
392
|
+
issues.push(`${label} must be a mapping.`);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const name = parameter["name"];
|
|
396
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
397
|
+
issues.push(`${label}.name is required and must be a non-empty string.`);
|
|
398
|
+
}
|
|
399
|
+
const type = parameter["type"];
|
|
400
|
+
if (typeof type !== "string" || type.trim().length === 0) {
|
|
401
|
+
issues.push(`${label}.type is required and must be a non-empty string.`);
|
|
402
|
+
}
|
|
403
|
+
const required = parameter["required"];
|
|
404
|
+
if (required !== undefined && typeof required !== "boolean") {
|
|
405
|
+
issues.push(`${label}.required must be a boolean.`);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (const key of ["executor", "attester"]) {
|
|
411
|
+
const value = metadataField(metadata, key);
|
|
412
|
+
if (value === undefined)
|
|
413
|
+
continue;
|
|
414
|
+
if (!isRecord(value)) {
|
|
415
|
+
issues.push(`${key} must be a mapping with a resource.`);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const resource = value["resource"];
|
|
419
|
+
if (typeof resource !== "string" || resource.trim().length === 0) {
|
|
420
|
+
issues.push(`${key}.resource is required and must be a non-empty string.`);
|
|
421
|
+
}
|
|
422
|
+
const receipt = value["receipt"];
|
|
423
|
+
if (receipt !== undefined &&
|
|
424
|
+
(!Array.isArray(receipt) || !receipt.every((field) => typeof field === "string"))) {
|
|
425
|
+
issues.push(`${key}.receipt must be a list of receipt field names.`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const computation = metadataField(metadata, "computation");
|
|
429
|
+
if (computation !== undefined && typeof computation !== "string") {
|
|
430
|
+
issues.push("computation must be a bundle-relative path to a computation file.");
|
|
431
|
+
}
|
|
432
|
+
if (computation === undefined && !/^#\s+Computation\s*$/im.test(body)) {
|
|
433
|
+
issues.push("Attested Computation concepts require a `# Computation` body heading or a computation field.");
|
|
434
|
+
}
|
|
435
|
+
return issues;
|
|
436
|
+
};
|
|
437
|
+
const secretPatterns = [
|
|
438
|
+
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
439
|
+
/\bAKIA[0-9A-Z]{16}\b/,
|
|
440
|
+
/\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b/,
|
|
441
|
+
/\b(?:sk_live|rk_live)_[A-Za-z0-9]{20,}\b/,
|
|
442
|
+
];
|
|
443
|
+
const secretLine = (raw) => {
|
|
444
|
+
const lines = raw.split(/\r?\n/);
|
|
445
|
+
const index = lines.findIndex((line) => secretPatterns.some((pattern) => pattern.test(line)));
|
|
446
|
+
return index === -1 ? undefined : index + 1;
|
|
447
|
+
};
|
|
448
|
+
const isReserved = (relativePath) => {
|
|
449
|
+
const baseName = relativePath.replace(/\\/g, "/").split("/").at(-1)?.toLowerCase();
|
|
450
|
+
return baseName !== undefined && RESERVED_BASENAMES.has(baseName);
|
|
451
|
+
};
|
|
452
|
+
const documentKind = (relativePath) => {
|
|
453
|
+
const baseName = relativePath.replace(/\\/g, "/").split("/").at(-1)?.toLowerCase();
|
|
454
|
+
if (baseName === "index.md")
|
|
455
|
+
return "index";
|
|
456
|
+
if (baseName === "log.md")
|
|
457
|
+
return "log";
|
|
458
|
+
return "concept";
|
|
459
|
+
};
|
|
460
|
+
const resolvedMarkdownTarget = (sourcePath, target, markdownPaths) => {
|
|
461
|
+
const normalized = normalizedPath(sourcePath, target);
|
|
462
|
+
if (normalized === null || normalized.escaped)
|
|
463
|
+
return normalized;
|
|
464
|
+
const candidates = normalized.path.toLowerCase().endsWith(".md")
|
|
465
|
+
? [normalized.path]
|
|
466
|
+
: [normalized.path, `${normalized.path}.md`, `${normalized.path}/index.md`];
|
|
467
|
+
return {
|
|
468
|
+
path: candidates.find((candidate) => markdownPaths.has(candidate)) ??
|
|
469
|
+
candidates[0] ??
|
|
470
|
+
normalized.path,
|
|
471
|
+
escaped: false,
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
/**
|
|
475
|
+
* Inspect a canonical OKF 0.2 bundle. The root index requirement is an explicit
|
|
476
|
+
* AgentXM profile constraint; upstream OKF permits a root without it.
|
|
477
|
+
*/
|
|
478
|
+
export const inspectKnowledgeEntries = (entries, readMarkdown) => Effect.gen(function* () {
|
|
479
|
+
const files = entries
|
|
480
|
+
.filter((entry) => entry.type === "File" && entry.relativePath.toLowerCase().endsWith(".md"))
|
|
481
|
+
.map((entry) => entry.relativePath)
|
|
482
|
+
.sort((left, right) => left.localeCompare(right));
|
|
483
|
+
const markdownPaths = new Set(files);
|
|
484
|
+
const allFilePaths = new Set(entries.filter((entry) => entry.type === "File").map((entry) => entry.relativePath));
|
|
485
|
+
const diagnostics = [];
|
|
486
|
+
const concepts = [];
|
|
487
|
+
const linksByPath = new Map();
|
|
488
|
+
const resources = new Map();
|
|
489
|
+
const types = new Map();
|
|
490
|
+
if (entries.length > MAX_BUNDLE_FILES) {
|
|
491
|
+
diagnostics.push({
|
|
492
|
+
code: "too-many-files",
|
|
493
|
+
severity: "error",
|
|
494
|
+
relativePath: ".",
|
|
495
|
+
message: `Knowledge bundle contains ${entries.length} files; the safety limit is ${MAX_BUNDLE_FILES}.`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const totalBytes = entries.reduce((sum, entry) => sum + entry.size, 0n);
|
|
499
|
+
if (totalBytes > MAX_BUNDLE_BYTES) {
|
|
500
|
+
diagnostics.push({
|
|
501
|
+
code: "bundle-too-large",
|
|
502
|
+
severity: "error",
|
|
503
|
+
relativePath: ".",
|
|
504
|
+
message: "Knowledge bundle exceeds the 10 MiB uncompressed safety limit.",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const pathsByCase = new Map();
|
|
508
|
+
for (const entry of entries) {
|
|
509
|
+
const segments = entry.relativePath.split("/");
|
|
510
|
+
if (entry.relativePath.startsWith("/") ||
|
|
511
|
+
entry.relativePath.includes("\\") ||
|
|
512
|
+
entry.relativePath.includes("\0") ||
|
|
513
|
+
segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
514
|
+
diagnostics.push({
|
|
515
|
+
code: "unsafe-path",
|
|
516
|
+
severity: "error",
|
|
517
|
+
relativePath: entry.relativePath,
|
|
518
|
+
message: `${entry.relativePath} is not a safe bundle-relative path.`,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
const folded = entry.relativePath.normalize("NFC").toLocaleLowerCase();
|
|
522
|
+
const colliding = pathsByCase.get(folded) ?? [];
|
|
523
|
+
colliding.push(entry.relativePath);
|
|
524
|
+
pathsByCase.set(folded, colliding);
|
|
525
|
+
if (entry.type === "SymbolicLink") {
|
|
526
|
+
diagnostics.push({
|
|
527
|
+
code: "symbolic-link",
|
|
528
|
+
severity: "error",
|
|
529
|
+
relativePath: entry.relativePath,
|
|
530
|
+
message: `${entry.relativePath} is a symbolic link; knowledge bundles must be self-contained.`,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (entry.size > MAX_FILE_BYTES) {
|
|
534
|
+
diagnostics.push({
|
|
535
|
+
code: "file-too-large",
|
|
536
|
+
severity: "error",
|
|
537
|
+
relativePath: entry.relativePath,
|
|
538
|
+
message: `${entry.relativePath} exceeds the 1 MiB per-file safety limit.`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (entry.type === "File" &&
|
|
542
|
+
entry.relativePath.toLowerCase().endsWith(".md") &&
|
|
543
|
+
isReserved(entry.relativePath) &&
|
|
544
|
+
entry.relativePath.toLowerCase().endsWith("index.md") &&
|
|
545
|
+
entry.size > LARGE_INDEX_BYTES) {
|
|
546
|
+
diagnostics.push({
|
|
547
|
+
code: "large-index",
|
|
548
|
+
severity: "warning",
|
|
549
|
+
relativePath: entry.relativePath,
|
|
550
|
+
message: `${entry.relativePath} is unusually large for progressive discovery.`,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
else if (entry.type === "File" &&
|
|
554
|
+
entry.relativePath.toLowerCase().endsWith(".md") &&
|
|
555
|
+
entry.size > LARGE_CONCEPT_BYTES) {
|
|
556
|
+
diagnostics.push({
|
|
557
|
+
code: "large-concept",
|
|
558
|
+
severity: "warning",
|
|
559
|
+
relativePath: entry.relativePath,
|
|
560
|
+
message: `${entry.relativePath} is unusually large and may be expensive to consume.`,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
for (const colliding of pathsByCase.values()) {
|
|
565
|
+
if (new Set(colliding).size > 1) {
|
|
566
|
+
diagnostics.push({
|
|
567
|
+
code: "case-collision",
|
|
568
|
+
severity: "error",
|
|
569
|
+
relativePath: colliding[0] ?? ".",
|
|
570
|
+
message: `Bundle paths collide on case-insensitive filesystems: ${colliding.join(", ")}.`,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (!files.includes("index.md")) {
|
|
575
|
+
diagnostics.push({
|
|
576
|
+
code: "missing-root-index",
|
|
577
|
+
severity: "error",
|
|
578
|
+
relativePath: "index.md",
|
|
579
|
+
message: "AgentXM knowledge bundles require src/index.md as their discovery root.",
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const rawByPath = new Map();
|
|
583
|
+
for (const relativePath of files) {
|
|
584
|
+
rawByPath.set(relativePath, yield* readMarkdown(relativePath));
|
|
585
|
+
}
|
|
586
|
+
for (const relativePath of files) {
|
|
587
|
+
const raw = rawByPath.get(relativePath) ?? "";
|
|
588
|
+
const links = markdownLinks(raw);
|
|
589
|
+
linksByPath.set(relativePath, links);
|
|
590
|
+
const foundSecretLine = secretLine(raw);
|
|
591
|
+
if (foundSecretLine !== undefined) {
|
|
592
|
+
diagnostics.push({
|
|
593
|
+
code: "detected-secret",
|
|
594
|
+
severity: "error",
|
|
595
|
+
relativePath,
|
|
596
|
+
line: foundSecretLine,
|
|
597
|
+
message: `${relativePath} contains a high-confidence credential or private key pattern.`,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
for (const link of links) {
|
|
601
|
+
if (activeScheme(link.target)) {
|
|
602
|
+
diagnostics.push({
|
|
603
|
+
code: "dangerous-uri",
|
|
604
|
+
severity: "error",
|
|
605
|
+
relativePath,
|
|
606
|
+
line: link.line,
|
|
607
|
+
message: `${relativePath} contains a dangerous active URI scheme.`,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const activeHtmlUri = /\b(?:href|src)\s*=\s*["']\s*(?:javascript|vbscript|data):/i.exec(raw);
|
|
612
|
+
if (activeHtmlUri !== null) {
|
|
613
|
+
diagnostics.push({
|
|
614
|
+
code: "dangerous-uri",
|
|
615
|
+
severity: "error",
|
|
616
|
+
relativePath,
|
|
617
|
+
line: raw.slice(0, activeHtmlUri.index).split(/\r?\n/).length,
|
|
618
|
+
message: `${relativePath} contains a dangerous active URI scheme.`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (/<\/?(?:script|iframe|object|embed|form|style|video|audio|svg|math)\b[^>]*>/i.test(raw)) {
|
|
622
|
+
diagnostics.push({
|
|
623
|
+
code: "embedded-html",
|
|
624
|
+
severity: "warning",
|
|
625
|
+
relativePath,
|
|
626
|
+
message: `${relativePath} contains embedded HTML; consumers must sanitize it.`,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const parsed = yield* parseFrontmatterEffect(raw).pipe(Effect.result);
|
|
630
|
+
if (Result.isFailure(parsed)) {
|
|
631
|
+
diagnostics.push(frontmatterParseDiagnostic(relativePath, parsed.failure));
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const parsedValue = parsed.success;
|
|
635
|
+
const baseName = (relativePath.replace(/\\/g, "/").split("/").at(-1) ?? relativePath).toLowerCase();
|
|
636
|
+
const metadata = parsedValue.frontmatter;
|
|
637
|
+
const isMetadata = typeof metadata === "object" && metadata !== null;
|
|
638
|
+
if (baseName === "index.md") {
|
|
639
|
+
if (relativePath !== "index.md" && metadata !== undefined) {
|
|
640
|
+
diagnostics.push({
|
|
641
|
+
code: "invalid-index",
|
|
642
|
+
severity: "error",
|
|
643
|
+
relativePath,
|
|
644
|
+
message: `${relativePath} is a reserved index and must not contain frontmatter.`,
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
if (firstHeading(parsedValue.body) === undefined) {
|
|
648
|
+
diagnostics.push({
|
|
649
|
+
code: "invalid-index",
|
|
650
|
+
severity: "error",
|
|
651
|
+
relativePath,
|
|
652
|
+
message: `${relativePath} must contain at least one level-one section heading.`,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (baseName === "log.md") {
|
|
657
|
+
if (metadata !== undefined) {
|
|
658
|
+
diagnostics.push({
|
|
659
|
+
code: "invalid-log",
|
|
660
|
+
severity: "error",
|
|
661
|
+
relativePath,
|
|
662
|
+
message: `${relativePath} is a reserved log and must not contain frontmatter.`,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const dateHeadings = parsedValue.body
|
|
666
|
+
.split(/\r?\n/)
|
|
667
|
+
.map((line, index) => ({ line, number: index + 1 }))
|
|
668
|
+
.filter(({ line }) => /^##\s+/.test(line));
|
|
669
|
+
if (dateHeadings.some(({ line }) => !/^##\s+\d{4}-\d{2}-\d{2}\s*$/.test(line)) ||
|
|
670
|
+
dateHeadings.some(({ line }) => {
|
|
671
|
+
const value = line.replace(/^##\s+/, "").trim();
|
|
672
|
+
const parsedDate = Date.parse(`${value}T00:00:00Z`);
|
|
673
|
+
return Number.isNaN(parsedDate);
|
|
674
|
+
})) {
|
|
675
|
+
const invalid = dateHeadings.find(({ line }) => !/^##\s+\d{4}-\d{2}-\d{2}\s*$/.test(line));
|
|
676
|
+
diagnostics.push({
|
|
677
|
+
code: "invalid-log",
|
|
678
|
+
severity: "error",
|
|
679
|
+
relativePath,
|
|
680
|
+
...(invalid === undefined ? {} : { line: invalid.number }),
|
|
681
|
+
message: `${relativePath} date headings must use valid ISO 8601 YYYY-MM-DD dates.`,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const dates = dateHeadings.map(({ line }) => line.replace(/^##\s+/, "").trim());
|
|
685
|
+
if (dates.some((date, index) => index > 0 && date > (dates[index - 1] ?? date))) {
|
|
686
|
+
diagnostics.push({
|
|
687
|
+
code: "invalid-log",
|
|
688
|
+
severity: "error",
|
|
689
|
+
relativePath,
|
|
690
|
+
message: `${relativePath} date headings must be newest first.`,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const type = isMetadata &&
|
|
695
|
+
"type" in metadata &&
|
|
696
|
+
typeof metadata.type === "string" &&
|
|
697
|
+
metadata.type.length > 0
|
|
698
|
+
? metadata.type
|
|
699
|
+
: undefined;
|
|
700
|
+
const description = isMetadata && "description" in metadata && typeof metadata.description === "string"
|
|
701
|
+
? metadata.description
|
|
702
|
+
: undefined;
|
|
703
|
+
const tags = isMetadata &&
|
|
704
|
+
"tags" in metadata &&
|
|
705
|
+
Array.isArray(metadata.tags) &&
|
|
706
|
+
metadata.tags.every((tag) => typeof tag === "string")
|
|
707
|
+
? metadata.tags
|
|
708
|
+
: undefined;
|
|
709
|
+
const resourceValue = metadataField(metadata, "resource");
|
|
710
|
+
const resource = typeof resourceValue === "string" ? resourceValue : undefined;
|
|
711
|
+
const resourceInspection = resource === undefined
|
|
712
|
+
? undefined
|
|
713
|
+
: inspectResource(resource, relativePath, allFilePaths, {
|
|
714
|
+
allowScope: false,
|
|
715
|
+
invalidCode: "invalid-resource",
|
|
716
|
+
label: `${relativePath} resource`,
|
|
717
|
+
});
|
|
718
|
+
const timestampValue = metadataField(metadata, "timestamp");
|
|
719
|
+
if (isMetadata && "tags" in metadata && metadata.tags !== undefined && tags === undefined) {
|
|
720
|
+
diagnostics.push({
|
|
721
|
+
code: "invalid-tags",
|
|
722
|
+
severity: "error",
|
|
723
|
+
relativePath,
|
|
724
|
+
message: `${relativePath} tags must be an array of strings.`,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (relativePath === "index.md") {
|
|
728
|
+
const declaredVersion = isMetadata && "okf_version" in metadata ? String(metadata.okf_version) : undefined;
|
|
729
|
+
if (declaredVersion === undefined) {
|
|
730
|
+
diagnostics.push({
|
|
731
|
+
code: "missing-okf-version",
|
|
732
|
+
severity: "error",
|
|
733
|
+
relativePath,
|
|
734
|
+
message: `src/index.md requires okf_version: ${OKF_VERSION} in YAML frontmatter.`,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else if (declaredVersion !== OKF_VERSION) {
|
|
738
|
+
diagnostics.push({
|
|
739
|
+
code: "unsupported-okf-version",
|
|
740
|
+
severity: "error",
|
|
741
|
+
relativePath,
|
|
742
|
+
message: `Unsupported OKF version ${declaredVersion}; expected ${OKF_VERSION}.`,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!RESERVED_BASENAMES.has(baseName) &&
|
|
747
|
+
resourceValue !== undefined &&
|
|
748
|
+
(resource === undefined || resourceInspection?.issue !== undefined)) {
|
|
749
|
+
const issue = resourceInspection?.issue ??
|
|
750
|
+
{
|
|
751
|
+
code: "invalid-resource",
|
|
752
|
+
severity: "error",
|
|
753
|
+
message: `${relativePath} resource must be a safe absolute URI or contained path.`,
|
|
754
|
+
};
|
|
755
|
+
diagnostics.push({ ...issue, relativePath });
|
|
756
|
+
}
|
|
757
|
+
if (!RESERVED_BASENAMES.has(baseName) && timestampValue !== undefined) {
|
|
758
|
+
diagnostics.push({
|
|
759
|
+
code: "invalid-frontmatter",
|
|
760
|
+
severity: "error",
|
|
761
|
+
relativePath,
|
|
762
|
+
message: `${relativePath} timestamp is not part of OKF 0.2; use generated.at.`,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
const statusValue = metadataField(metadata, "status");
|
|
766
|
+
const staleAfterValue = metadataField(metadata, "stale_after");
|
|
767
|
+
const generatedValue = metadataField(metadata, "generated");
|
|
768
|
+
const verifiedValue = metadataField(metadata, "verified");
|
|
769
|
+
const sourcesValue = metadataField(metadata, "sources");
|
|
770
|
+
const usageWindowValue = metadataField(metadata, "usage_window");
|
|
771
|
+
const generated = isRecord(generatedValue) && typeof generatedValue["by"] === "string"
|
|
772
|
+
? {
|
|
773
|
+
by: generatedValue["by"],
|
|
774
|
+
...(typeof generatedValue["at"] === "string" ? { at: generatedValue["at"] } : {}),
|
|
775
|
+
}
|
|
776
|
+
: undefined;
|
|
777
|
+
const verified = verifiedRecords(verifiedValue)
|
|
778
|
+
.filter(isRecord)
|
|
779
|
+
.flatMap((record) => typeof record["by"] === "string"
|
|
780
|
+
? [
|
|
781
|
+
{
|
|
782
|
+
by: record["by"],
|
|
783
|
+
...(typeof record["at"] === "string" ? { at: record["at"] } : {}),
|
|
784
|
+
},
|
|
785
|
+
]
|
|
786
|
+
: []);
|
|
787
|
+
const trust = verifiedValue === undefined || verified.length === 0
|
|
788
|
+
? "unverified"
|
|
789
|
+
: verified.some((record) => record.by.startsWith("human:"))
|
|
790
|
+
? "human-reviewed"
|
|
791
|
+
: "machine-confirmed";
|
|
792
|
+
if (!RESERVED_BASENAMES.has(baseName)) {
|
|
793
|
+
if (sourcesValue !== undefined) {
|
|
794
|
+
for (const issue of sourcesIssues(sourcesValue, relativePath, allFilePaths)) {
|
|
795
|
+
diagnostics.push({
|
|
796
|
+
code: issue.code,
|
|
797
|
+
severity: issue.severity,
|
|
798
|
+
relativePath,
|
|
799
|
+
message: `${relativePath} ${issue.message}`,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (usageWindowValue !== undefined) {
|
|
804
|
+
for (const issue of usageWindowIssues(usageWindowValue)) {
|
|
805
|
+
diagnostics.push({
|
|
806
|
+
code: "invalid-sources",
|
|
807
|
+
severity: "error",
|
|
808
|
+
relativePath,
|
|
809
|
+
message: `${relativePath} ${issue}`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (generatedValue !== undefined) {
|
|
814
|
+
for (const issue of actorRecordIssues(generatedValue, "generated")) {
|
|
815
|
+
diagnostics.push({
|
|
816
|
+
code: "invalid-generated",
|
|
817
|
+
severity: "error",
|
|
818
|
+
relativePath,
|
|
819
|
+
message: `${relativePath} ${issue}`,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (verifiedValue !== undefined) {
|
|
824
|
+
const records = verifiedRecords(verifiedValue);
|
|
825
|
+
records.forEach((record, index) => {
|
|
826
|
+
const label = Array.isArray(verifiedValue) ? `verified[${index}]` : "verified";
|
|
827
|
+
for (const issue of actorRecordIssues(record, label)) {
|
|
828
|
+
diagnostics.push({
|
|
829
|
+
code: "invalid-verified",
|
|
830
|
+
severity: "error",
|
|
831
|
+
relativePath,
|
|
832
|
+
message: `${relativePath} ${issue}`,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
if (statusValue !== undefined &&
|
|
838
|
+
(typeof statusValue !== "string" || !CONCEPT_STATUSES.has(statusValue))) {
|
|
839
|
+
diagnostics.push({
|
|
840
|
+
code: "invalid-status",
|
|
841
|
+
severity: "error",
|
|
842
|
+
relativePath,
|
|
843
|
+
message: `${relativePath} status must be one of ${[...CONCEPT_STATUSES].join(", ")}.`,
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (staleAfterValue !== undefined) {
|
|
847
|
+
const issue = dateIssue(staleAfterValue, "stale_after");
|
|
848
|
+
if (issue !== undefined) {
|
|
849
|
+
diagnostics.push({
|
|
850
|
+
code: "invalid-stale-after",
|
|
851
|
+
severity: "error",
|
|
852
|
+
relativePath,
|
|
853
|
+
message: `${relativePath} ${issue}`,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (type !== undefined && type.toLocaleLowerCase() === ATTESTED_COMPUTATION_TYPE) {
|
|
858
|
+
for (const issue of attestationIssues(metadata, parsedValue.body)) {
|
|
859
|
+
diagnostics.push({
|
|
860
|
+
code: "invalid-attestation",
|
|
861
|
+
severity: "error",
|
|
862
|
+
relativePath,
|
|
863
|
+
message: `${relativePath} ${issue}`,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (!RESERVED_BASENAMES.has(baseName) && type === undefined) {
|
|
869
|
+
diagnostics.push({
|
|
870
|
+
code: "missing-type",
|
|
871
|
+
severity: "error",
|
|
872
|
+
relativePath,
|
|
873
|
+
message: `${relativePath} is an OKF concept and requires a non-empty frontmatter type.`,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
const title = firstHeading(parsedValue.body);
|
|
877
|
+
if (title === undefined) {
|
|
878
|
+
diagnostics.push({
|
|
879
|
+
code: "missing-title",
|
|
880
|
+
severity: "warning",
|
|
881
|
+
relativePath,
|
|
882
|
+
message: `${relativePath} should include one level-one heading for a stable display title.`,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
if (!RESERVED_BASENAMES.has(baseName) && description === undefined) {
|
|
886
|
+
diagnostics.push({
|
|
887
|
+
code: "missing-description",
|
|
888
|
+
severity: "warning",
|
|
889
|
+
relativePath,
|
|
890
|
+
message: `${relativePath} should include a concise frontmatter description.`,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (!RESERVED_BASENAMES.has(baseName) && tags === undefined) {
|
|
894
|
+
diagnostics.push({
|
|
895
|
+
code: "missing-tags",
|
|
896
|
+
severity: "warning",
|
|
897
|
+
relativePath,
|
|
898
|
+
message: `${relativePath} should include frontmatter tags for discovery.`,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
if (!RESERVED_BASENAMES.has(baseName) &&
|
|
902
|
+
resource !== undefined &&
|
|
903
|
+
resourceInspection?.identity !== undefined &&
|
|
904
|
+
resourceInspection.issue?.code !== "escaping-resource" &&
|
|
905
|
+
resourceInspection.issue?.code !== "invalid-resource") {
|
|
906
|
+
const matching = resources.get(resourceInspection.identity) ?? [];
|
|
907
|
+
matching.push(relativePath);
|
|
908
|
+
resources.set(resourceInspection.identity, matching);
|
|
909
|
+
}
|
|
910
|
+
if (!RESERVED_BASENAMES.has(baseName) && type !== undefined) {
|
|
911
|
+
const spellings = types.get(type.toLocaleLowerCase()) ?? new Set();
|
|
912
|
+
spellings.add(type);
|
|
913
|
+
types.set(type.toLocaleLowerCase(), spellings);
|
|
914
|
+
}
|
|
915
|
+
const citationHeading = parsedValue.body.search(/^#\s+Citations\s*$/im);
|
|
916
|
+
if (citationHeading >= 0 && !RESERVED_BASENAMES.has(baseName)) {
|
|
917
|
+
diagnostics.push({
|
|
918
|
+
code: "invalid-frontmatter",
|
|
919
|
+
severity: "error",
|
|
920
|
+
relativePath,
|
|
921
|
+
line: parsedValue.body.slice(0, citationHeading).split(/\r?\n/).length,
|
|
922
|
+
message: `${relativePath} # Citations is not part of OKF 0.2; use sources frontmatter.`,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
concepts.push({
|
|
926
|
+
id: conceptId(relativePath),
|
|
927
|
+
kind: documentKind(relativePath),
|
|
928
|
+
title: title ?? conceptId(relativePath),
|
|
929
|
+
...(title === undefined ? {} : { authoredTitle: title }),
|
|
930
|
+
...(isRecord(metadata) ? { frontmatter: metadata } : {}),
|
|
931
|
+
...(type === undefined ? {} : { type }),
|
|
932
|
+
...(description === undefined ? {} : { description }),
|
|
933
|
+
...(tags === undefined ? {} : { tags }),
|
|
934
|
+
...(resource === undefined ? {} : { resource }),
|
|
935
|
+
...(typeof statusValue === "string" ? { status: statusValue } : {}),
|
|
936
|
+
...(typeof staleAfterValue === "string" ? { staleAfter: staleAfterValue } : {}),
|
|
937
|
+
...(generated === undefined ? {} : { generated }),
|
|
938
|
+
...(verified.length === 0 ? {} : { verified }),
|
|
939
|
+
trust,
|
|
940
|
+
authoredLinks: [],
|
|
941
|
+
relativePath,
|
|
942
|
+
body: parsedValue.body,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
const outgoing = new Map();
|
|
946
|
+
const authoredLinksByPath = new Map();
|
|
947
|
+
const referencedFiles = new Set();
|
|
948
|
+
for (const [sourcePath, links] of linksByPath) {
|
|
949
|
+
const targets = new Set();
|
|
950
|
+
const authoredLinks = [];
|
|
951
|
+
for (const link of links) {
|
|
952
|
+
const normalized = normalizedPath(sourcePath, link.target);
|
|
953
|
+
if (normalized === null) {
|
|
954
|
+
authoredLinks.push(link);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (normalized.escaped) {
|
|
958
|
+
authoredLinks.push(link);
|
|
959
|
+
diagnostics.push({
|
|
960
|
+
code: "escaping-link",
|
|
961
|
+
severity: "warning",
|
|
962
|
+
relativePath: sourcePath,
|
|
963
|
+
line: link.line,
|
|
964
|
+
message: `${sourcePath} links outside the Knowledge bundle.`,
|
|
965
|
+
});
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const resolved = resolvedMarkdownTarget(sourcePath, link.target, markdownPaths);
|
|
969
|
+
const targetPath = resolved?.path ?? normalized.path;
|
|
970
|
+
if (allFilePaths.has(targetPath))
|
|
971
|
+
referencedFiles.add(targetPath);
|
|
972
|
+
if (markdownPaths.has(targetPath)) {
|
|
973
|
+
targets.add(targetPath);
|
|
974
|
+
authoredLinks.push({ ...link, resolvedConceptId: conceptId(targetPath) });
|
|
975
|
+
}
|
|
976
|
+
else if (!allFilePaths.has(targetPath)) {
|
|
977
|
+
authoredLinks.push(link);
|
|
978
|
+
diagnostics.push({
|
|
979
|
+
code: sourcePath.toLowerCase().endsWith("index.md")
|
|
980
|
+
? "stale-index-entry"
|
|
981
|
+
: "broken-internal-link",
|
|
982
|
+
severity: "warning",
|
|
983
|
+
relativePath: sourcePath,
|
|
984
|
+
line: link.line,
|
|
985
|
+
message: `${sourcePath} links to missing bundle path ${link.target}.`,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
authoredLinks.push(link);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
outgoing.set(sourcePath, targets);
|
|
993
|
+
authoredLinksByPath.set(sourcePath, authoredLinks);
|
|
994
|
+
}
|
|
995
|
+
const reachable = new Set();
|
|
996
|
+
const hasRootIndex = files.includes("index.md");
|
|
997
|
+
const pending = hasRootIndex ? ["index.md"] : [];
|
|
998
|
+
while (pending.length > 0) {
|
|
999
|
+
const current = pending.shift();
|
|
1000
|
+
if (current === undefined || reachable.has(current))
|
|
1001
|
+
continue;
|
|
1002
|
+
reachable.add(current);
|
|
1003
|
+
for (const target of outgoing.get(current) ?? [])
|
|
1004
|
+
pending.push(target);
|
|
1005
|
+
}
|
|
1006
|
+
for (const concept of concepts) {
|
|
1007
|
+
if (hasRootIndex &&
|
|
1008
|
+
!isReserved(concept.relativePath) &&
|
|
1009
|
+
!reachable.has(concept.relativePath)) {
|
|
1010
|
+
diagnostics.push({
|
|
1011
|
+
code: "unreachable-concept",
|
|
1012
|
+
severity: "warning",
|
|
1013
|
+
relativePath: concept.relativePath,
|
|
1014
|
+
message: `${concept.relativePath} is not reachable from the root index.`,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (!isReserved(concept.relativePath)) {
|
|
1018
|
+
const directory = concept.relativePath.split("/").slice(0, -1).join("/");
|
|
1019
|
+
const nearestIndex = directory.length === 0 ? "index.md" : `${directory}/index.md`;
|
|
1020
|
+
if (markdownPaths.has(nearestIndex) &&
|
|
1021
|
+
!(outgoing.get(nearestIndex) ?? new Set()).has(concept.relativePath)) {
|
|
1022
|
+
diagnostics.push({
|
|
1023
|
+
code: "missing-index-entry",
|
|
1024
|
+
severity: "warning",
|
|
1025
|
+
relativePath: nearestIndex,
|
|
1026
|
+
message: `${nearestIndex} does not link to ${concept.relativePath}.`,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
for (const entry of entries) {
|
|
1032
|
+
if (entry.type === "File" &&
|
|
1033
|
+
!entry.relativePath.toLowerCase().endsWith(".md") &&
|
|
1034
|
+
!referencedFiles.has(entry.relativePath)) {
|
|
1035
|
+
diagnostics.push({
|
|
1036
|
+
code: "unreferenced-asset",
|
|
1037
|
+
severity: "warning",
|
|
1038
|
+
relativePath: entry.relativePath,
|
|
1039
|
+
message: `${entry.relativePath} is not referenced by any Markdown document.`,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
for (const [resource, matching] of resources) {
|
|
1044
|
+
if (matching.length > 1) {
|
|
1045
|
+
for (const relativePath of matching) {
|
|
1046
|
+
diagnostics.push({
|
|
1047
|
+
code: "duplicate-resource",
|
|
1048
|
+
severity: "warning",
|
|
1049
|
+
relativePath,
|
|
1050
|
+
message: `${relativePath} shares canonical resource ${resource} with another concept.`,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
for (const spellings of types.values()) {
|
|
1056
|
+
if (spellings.size > 1) {
|
|
1057
|
+
diagnostics.push({
|
|
1058
|
+
code: "inconsistent-type",
|
|
1059
|
+
severity: "warning",
|
|
1060
|
+
relativePath: ".",
|
|
1061
|
+
message: `Concept type spellings differ only by casing: ${[...spellings].sort().join(", ")}.`,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (!concepts.some((concept) => !isReserved(concept.relativePath))) {
|
|
1066
|
+
diagnostics.push({
|
|
1067
|
+
code: "empty-bundle",
|
|
1068
|
+
severity: "warning",
|
|
1069
|
+
relativePath: "index.md",
|
|
1070
|
+
message: "Knowledge bundle contains no concept documents to discover.",
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
concepts: concepts.map((concept) => ({
|
|
1075
|
+
...concept,
|
|
1076
|
+
authoredLinks: authoredLinksByPath.get(concept.relativePath) ?? [],
|
|
1077
|
+
})),
|
|
1078
|
+
diagnostics,
|
|
1079
|
+
okfVersion: OKF_VERSION,
|
|
1080
|
+
};
|
|
1081
|
+
});
|
|
1082
|
+
export const inspectKnowledgeBundle = (sourceRoot) => Effect.gen(function* () {
|
|
1083
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1084
|
+
const path = yield* Path.Path;
|
|
1085
|
+
const entries = yield* collectKnowledgeBundleEntries(sourceRoot);
|
|
1086
|
+
return yield* inspectKnowledgeEntries(entries, (relativePath) => fs.readFileString(path.join(sourceRoot, relativePath)));
|
|
1087
|
+
});
|
|
1088
|
+
export const searchKnowledgeConcepts = (concepts, query) => concepts.filter((concept) => matchesKnowledgeSearchQuery(concept, query));
|
|
1089
|
+
export const openKnowledgeConcept = (concepts, id) => concepts.find((concept) => concept.id === id);
|
|
1090
|
+
//# sourceMappingURL=okf.js.map
|