@first-tree-ai/context-tree 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +91 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +95 -0
- package/dist/index.d.mts +29 -0
- package/dist/index.mjs +3 -0
- package/dist/schemas-BJXFTfxw.d.mts +359 -0
- package/dist/schemas-D0Qt1bWc.mjs +207 -0
- package/dist/schemas.d.mts +2 -0
- package/dist/schemas.mjs +2 -0
- package/dist/src-Ce9BtETD.mjs +794 -0
- package/docs/specification.md +74 -0
- package/examples/basic/NODE.md +11 -0
- package/examples/basic/SCOPE.md +7 -0
- package/examples/basic/members/NODE.md +6 -0
- package/examples/basic/members/example-owner/NODE.md +10 -0
- package/examples/basic/systems/NODE.md +7 -0
- package/examples/basic/systems/runtime.md +16 -0
- package/package.json +75 -8
- package/policy/context-tree-policy.md +129 -0
- package/skills/context-tree-init/SKILL.md +43 -0
- package/skills/context-tree-init/agents/openai.yaml +4 -0
- package/skills/context-tree-read/SKILL.md +43 -0
- package/skills/context-tree-read/agents/openai.yaml +4 -0
- package/skills/context-tree-write/SKILL.md +59 -0
- package/skills/context-tree-write/agents/openai.yaml +4 -0
- package/templates/member-node.md +13 -0
- package/templates/members-index.md +9 -0
- package/templates/root-node.md +15 -0
- package/templates/scope.md +5 -0
- package/templates/validate-context-tree.yml +16 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { S as isRecord, g as parseContextTreeScope, i as VALIDATION_CODES, x as parseMarkdownFrontmatter } from "./schemas-D0Qt1bWc.mjs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, parse, posix, relative, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
6
|
+
//#region src/core/internal/packaged-resource.ts
|
|
7
|
+
const PACKAGE_NAME = "@first-tree-ai/context-tree";
|
|
8
|
+
function isPackageRoot(path) {
|
|
9
|
+
const manifestPath = join(path, "package.json");
|
|
10
|
+
if (!existsSync(manifestPath)) return false;
|
|
11
|
+
try {
|
|
12
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
13
|
+
return typeof manifest === "object" && manifest !== null && "name" in manifest && manifest.name === PACKAGE_NAME;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function resolvePackagedResource(...segments) {
|
|
19
|
+
let candidate = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const filesystemRoot = parse(candidate).root;
|
|
21
|
+
while (true) {
|
|
22
|
+
if (isPackageRoot(candidate)) {
|
|
23
|
+
const resource = resolve(candidate, ...segments);
|
|
24
|
+
if (existsSync(resource)) return resource;
|
|
25
|
+
throw new Error(`Packaged resource is missing: ${segments.join("/")}`);
|
|
26
|
+
}
|
|
27
|
+
if (candidate === filesystemRoot) break;
|
|
28
|
+
candidate = dirname(candidate);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Package root is missing while resolving: ${segments.join("/")}`);
|
|
31
|
+
}
|
|
32
|
+
function readPackageManifest() {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(resolvePackagedResource("package.json"), "utf8"));
|
|
34
|
+
if (!isRecord(parsed)) throw new Error("Package metadata is invalid.");
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/core/policy.ts
|
|
39
|
+
function readContextTreePolicy() {
|
|
40
|
+
return {
|
|
41
|
+
content: readFileSync(resolvePackagedResource("policy", "context-tree-policy.md"), "utf8"),
|
|
42
|
+
schemaVersion: 1
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/core/path.ts
|
|
47
|
+
function isPathInside(root, target) {
|
|
48
|
+
const path = relative(root, target);
|
|
49
|
+
return path === "" || !path.startsWith("..") && !isAbsolute(path);
|
|
50
|
+
}
|
|
51
|
+
function resolveTreeRoot(path) {
|
|
52
|
+
const absolute = resolve(path);
|
|
53
|
+
const entry = lstatSync(absolute);
|
|
54
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
|
|
55
|
+
return realpathSync(absolute);
|
|
56
|
+
}
|
|
57
|
+
function toPosixPath(path) {
|
|
58
|
+
return path.replace(/\\/gu, "/");
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/core/internal/content-class.ts
|
|
62
|
+
const GENERATED_DIRECTORY_NAMES = new Set([
|
|
63
|
+
"node_modules",
|
|
64
|
+
"__pycache__",
|
|
65
|
+
"dist",
|
|
66
|
+
"build",
|
|
67
|
+
".next",
|
|
68
|
+
".turbo"
|
|
69
|
+
]);
|
|
70
|
+
const REPO_INFRA_MARKDOWN_FILES = new Set(["AGENTS.md", "CLAUDE.md"]);
|
|
71
|
+
const MANAGED_SYMLINK_PATHS = new Set(["WHITEPAPER.md"]);
|
|
72
|
+
function toTreeRelativePosixPath(treeRoot, targetPath) {
|
|
73
|
+
return relative(treeRoot, targetPath).replace(/\\/gu, "/");
|
|
74
|
+
}
|
|
75
|
+
function classifyContextContent(relativePath) {
|
|
76
|
+
const parts = relativePath.replace(/\\/gu, "/").replace(/^\.\//u, "").split("/").filter((part) => part.length > 0);
|
|
77
|
+
if (parts.length === 0 || parts.some((part) => part.startsWith(".") || GENERATED_DIRECTORY_NAMES.has(part)) || REPO_INFRA_MARKDOWN_FILES.has(parts.at(-1) ?? "")) return "repo-infra";
|
|
78
|
+
if (parts[0] === "raw-context") return "archive-supporting";
|
|
79
|
+
if (parts[0] === "members") return "member";
|
|
80
|
+
return "normal";
|
|
81
|
+
}
|
|
82
|
+
function emptyContentClassCounts() {
|
|
83
|
+
return {
|
|
84
|
+
normal: 0,
|
|
85
|
+
"archive-supporting": 0,
|
|
86
|
+
member: 0,
|
|
87
|
+
"repo-infra": 0
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function isSafeCanonicalMarkdown(file) {
|
|
91
|
+
return file.contentClass !== "repo-infra" && !file.escaped && !file.unresolved && !file.unsupported && file.canonicalContentClass === file.contentClass;
|
|
92
|
+
}
|
|
93
|
+
function readDirectoryEntries(path) {
|
|
94
|
+
try {
|
|
95
|
+
return readdirSync(path, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function collectContextMarkdownContent(treeRoot) {
|
|
101
|
+
const directorySymlinks = [];
|
|
102
|
+
const files = [];
|
|
103
|
+
const realTreeRoot = realpathSync(treeRoot);
|
|
104
|
+
function walk(directoryPath) {
|
|
105
|
+
for (const entry of readDirectoryEntries(directoryPath)) {
|
|
106
|
+
const absolutePath = join(directoryPath, entry.name);
|
|
107
|
+
const relativePath = toTreeRelativePosixPath(treeRoot, absolutePath);
|
|
108
|
+
const contentClass = classifyContextContent(relativePath);
|
|
109
|
+
if (entry.isDirectory()) {
|
|
110
|
+
if (contentClass !== "repo-infra") walk(absolutePath);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const symbolicLink = entry.isSymbolicLink();
|
|
114
|
+
if (symbolicLink) try {
|
|
115
|
+
const targetStat = statSync(absolutePath);
|
|
116
|
+
if (targetStat.isDirectory()) {
|
|
117
|
+
if (contentClass !== "repo-infra" || entry.name.endsWith(".md")) directorySymlinks.push({
|
|
118
|
+
contentClass,
|
|
119
|
+
escaped: !isPathInside(realTreeRoot, realpathSync(absolutePath)),
|
|
120
|
+
relativePath
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!targetStat.isFile() && entry.name.endsWith(".md")) {
|
|
125
|
+
let escaped = false;
|
|
126
|
+
let canonicalContentClass = contentClass;
|
|
127
|
+
let canonicalRelativePath = relativePath;
|
|
128
|
+
try {
|
|
129
|
+
const realTarget = realpathSync(absolutePath);
|
|
130
|
+
escaped = !isPathInside(realTreeRoot, realTarget);
|
|
131
|
+
canonicalRelativePath = toTreeRelativePosixPath(realTreeRoot, realTarget);
|
|
132
|
+
if (!escaped) canonicalContentClass = classifyContextContent(canonicalRelativePath);
|
|
133
|
+
} catch {
|
|
134
|
+
files.push({
|
|
135
|
+
absolutePath,
|
|
136
|
+
canonicalContentClass,
|
|
137
|
+
canonicalRelativePath,
|
|
138
|
+
contentClass,
|
|
139
|
+
escaped: false,
|
|
140
|
+
relativePath,
|
|
141
|
+
unsupported: false,
|
|
142
|
+
unresolved: true
|
|
143
|
+
});
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
files.push({
|
|
147
|
+
absolutePath,
|
|
148
|
+
canonicalContentClass,
|
|
149
|
+
canonicalRelativePath,
|
|
150
|
+
contentClass,
|
|
151
|
+
escaped,
|
|
152
|
+
relativePath,
|
|
153
|
+
unsupported: true,
|
|
154
|
+
unresolved: false
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
if (MANAGED_SYMLINK_PATHS.has(relativePath)) continue;
|
|
160
|
+
if (entry.name.endsWith(".md")) files.push({
|
|
161
|
+
absolutePath,
|
|
162
|
+
canonicalContentClass: contentClass,
|
|
163
|
+
canonicalRelativePath: relativePath,
|
|
164
|
+
contentClass,
|
|
165
|
+
escaped: false,
|
|
166
|
+
relativePath,
|
|
167
|
+
unsupported: false,
|
|
168
|
+
unresolved: true
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!entry.isFile() && !symbolicLink || !entry.name.endsWith(".md")) continue;
|
|
173
|
+
if (symbolicLink && MANAGED_SYMLINK_PATHS.has(relativePath)) continue;
|
|
174
|
+
try {
|
|
175
|
+
if (!statSync(absolutePath).isFile()) continue;
|
|
176
|
+
} catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
let escaped = false;
|
|
180
|
+
let canonicalContentClass = contentClass;
|
|
181
|
+
let canonicalRelativePath = relativePath;
|
|
182
|
+
if (symbolicLink) try {
|
|
183
|
+
const realTarget = realpathSync(absolutePath);
|
|
184
|
+
escaped = !isPathInside(realTreeRoot, realTarget);
|
|
185
|
+
canonicalRelativePath = toTreeRelativePosixPath(realTreeRoot, realTarget);
|
|
186
|
+
if (!escaped) canonicalContentClass = classifyContextContent(canonicalRelativePath);
|
|
187
|
+
} catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
files.push({
|
|
191
|
+
absolutePath,
|
|
192
|
+
canonicalContentClass,
|
|
193
|
+
canonicalRelativePath,
|
|
194
|
+
contentClass,
|
|
195
|
+
escaped,
|
|
196
|
+
relativePath,
|
|
197
|
+
unsupported: false,
|
|
198
|
+
unresolved: false
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
walk(treeRoot);
|
|
203
|
+
return {
|
|
204
|
+
directorySymlinks,
|
|
205
|
+
files
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/core/internal/filesystem.ts
|
|
210
|
+
function readUtf8File(path) {
|
|
211
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/core/internal/context-document.ts
|
|
215
|
+
function readContextDocument(path) {
|
|
216
|
+
try {
|
|
217
|
+
return parseMarkdownFrontmatter(readUtf8File(path));
|
|
218
|
+
} catch (error) {
|
|
219
|
+
return {
|
|
220
|
+
body: "",
|
|
221
|
+
data: null,
|
|
222
|
+
error: error instanceof Error ? error.message : String(error),
|
|
223
|
+
frontmatter: "invalid"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function readNonEmptyStringField(data, key) {
|
|
228
|
+
if (!(key in data)) return {
|
|
229
|
+
present: false,
|
|
230
|
+
valid: false
|
|
231
|
+
};
|
|
232
|
+
const value = data[key];
|
|
233
|
+
if (typeof value !== "string" || value.trim().length === 0) return {
|
|
234
|
+
present: true,
|
|
235
|
+
valid: false
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
present: true,
|
|
239
|
+
valid: true,
|
|
240
|
+
value: value.trim()
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function readNonEmptyStringArrayField(data, key) {
|
|
244
|
+
if (!(key in data)) return {
|
|
245
|
+
present: false,
|
|
246
|
+
valid: false
|
|
247
|
+
};
|
|
248
|
+
const value = data[key];
|
|
249
|
+
if (!Array.isArray(value) || value.length === 0) return {
|
|
250
|
+
present: true,
|
|
251
|
+
valid: false
|
|
252
|
+
};
|
|
253
|
+
const items = [];
|
|
254
|
+
for (const item of value) {
|
|
255
|
+
if (typeof item !== "string" || item.trim().length === 0) return {
|
|
256
|
+
present: true,
|
|
257
|
+
valid: false
|
|
258
|
+
};
|
|
259
|
+
items.push(item.trim());
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
present: true,
|
|
263
|
+
valid: true,
|
|
264
|
+
value: items
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function readNodeMetadata(path) {
|
|
268
|
+
const document = readContextDocument(path);
|
|
269
|
+
if (document.frontmatter !== "valid" || document.data === null) return null;
|
|
270
|
+
const title = readNonEmptyStringField(document.data, "title");
|
|
271
|
+
const owners = readNonEmptyStringArrayField(document.data, "owners");
|
|
272
|
+
const description = readNonEmptyStringField(document.data, "description");
|
|
273
|
+
if (!title.valid || !owners.valid || description.present && !description.valid) return null;
|
|
274
|
+
return {
|
|
275
|
+
title: title.value,
|
|
276
|
+
owners: owners.value,
|
|
277
|
+
...description.valid ? { description: description.value } : {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/core/internal/tree-selection.ts
|
|
282
|
+
function normalizeTreeTarget(value) {
|
|
283
|
+
if (!value || value === ".") return "";
|
|
284
|
+
const normalized = posix.normalize(toPosixPath(value).replace(/^\.\//u, ""));
|
|
285
|
+
if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) throw new Error(`Read target is outside the Context Tree: ${value}`);
|
|
286
|
+
return normalized.replace(/\/$/u, "");
|
|
287
|
+
}
|
|
288
|
+
function compileSegmentLocalGlob(pattern) {
|
|
289
|
+
let source = "^";
|
|
290
|
+
for (const character of pattern) if (character === "*") source += "[^/]*";
|
|
291
|
+
else if (character === "?") source += "[^/]";
|
|
292
|
+
else source += character.replace(/[|\\{}()[\]^$+?.]/gu, "\\$&");
|
|
293
|
+
return new RegExp(`${source}$`, "u");
|
|
294
|
+
}
|
|
295
|
+
function entryDepth(path) {
|
|
296
|
+
if (path === "." || path.length === 0) return 0;
|
|
297
|
+
return path.split("/").length;
|
|
298
|
+
}
|
|
299
|
+
function relativeTreeDepth(path, target) {
|
|
300
|
+
return Math.max(0, entryDepth(path) - entryDepth(target));
|
|
301
|
+
}
|
|
302
|
+
function isTreeEntryWithinTarget(path, sourcePath, target) {
|
|
303
|
+
return target.length === 0 || path === target || path.startsWith(`${target}/`) || sourcePath === target || sourcePath.startsWith(`${target}/`);
|
|
304
|
+
}
|
|
305
|
+
function displayTitle(relativePath) {
|
|
306
|
+
return basename(relativePath, ".md").split(/[-_]/u).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
307
|
+
}
|
|
308
|
+
function deriveTreeEntry(relativePath, metadataTitle) {
|
|
309
|
+
const isNode = basename(relativePath) === "NODE.md";
|
|
310
|
+
const entryPath = isNode ? dirname(relativePath).replace(/^\.$/u, ".") : relativePath;
|
|
311
|
+
const normalizedPath = entryPath === "." ? "" : toPosixPath(entryPath);
|
|
312
|
+
return {
|
|
313
|
+
kind: isNode ? "directory" : "file",
|
|
314
|
+
path: normalizedPath,
|
|
315
|
+
title: metadataTitle ?? (relativePath === "SCOPE.md" ? "Scope" : displayTitle(relativePath))
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function compareTreeEntries(left, right) {
|
|
319
|
+
const leftParts = left.path === "." ? [] : left.path.split("/");
|
|
320
|
+
const rightParts = right.path === "." ? [] : right.path.split("/");
|
|
321
|
+
const sharedLength = Math.min(leftParts.length, rightParts.length);
|
|
322
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
323
|
+
const comparison = (leftParts[index] ?? "").localeCompare(rightParts[index] ?? "");
|
|
324
|
+
if (comparison !== 0) return comparison;
|
|
325
|
+
}
|
|
326
|
+
return leftParts.length - rightParts.length || left.kind.localeCompare(right.kind);
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/core/read.ts
|
|
330
|
+
function allSafeMarkdown(root) {
|
|
331
|
+
return collectContextMarkdownContent(root).files.filter(isSafeCanonicalMarkdown).map((file) => ({
|
|
332
|
+
absolutePath: file.absolutePath,
|
|
333
|
+
contentClass: file.contentClass,
|
|
334
|
+
relativePath: file.relativePath
|
|
335
|
+
})).sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
336
|
+
}
|
|
337
|
+
function readTree(treePath, options = {}) {
|
|
338
|
+
const root = resolveTreeRoot(treePath);
|
|
339
|
+
const target = normalizeTreeTarget(options.path);
|
|
340
|
+
const absoluteTarget = resolve(root, target);
|
|
341
|
+
if (!isPathInside(root, absoluteTarget)) throw new Error("Read target escapes the Context Tree root.");
|
|
342
|
+
const targetEntry = lstatSync(absoluteTarget);
|
|
343
|
+
if (targetEntry.isSymbolicLink() || !targetEntry.isDirectory() && !targetEntry.isFile()) throw new Error(`Read target must be a real file or directory: ${target || "."}`);
|
|
344
|
+
const classes = options.classes ?? ["normal"];
|
|
345
|
+
const pattern = options.pattern ? compileSegmentLocalGlob(options.pattern) : null;
|
|
346
|
+
const entries = [];
|
|
347
|
+
for (const file of allSafeMarkdown(root)) {
|
|
348
|
+
if (classes !== "all" && !classes.includes(file.contentClass)) continue;
|
|
349
|
+
const metadata = file.relativePath === "SCOPE.md" ? null : readNodeMetadata(file.absolutePath);
|
|
350
|
+
const entry = deriveTreeEntry(file.relativePath, metadata?.title);
|
|
351
|
+
if (!isTreeEntryWithinTarget(entry.path, file.relativePath, target)) continue;
|
|
352
|
+
const depth = relativeTreeDepth(entry.path, target);
|
|
353
|
+
if (options.depth !== void 0 && depth > options.depth) continue;
|
|
354
|
+
const candidates = [
|
|
355
|
+
entry.path,
|
|
356
|
+
file.relativePath,
|
|
357
|
+
entry.title,
|
|
358
|
+
metadata?.description ?? ""
|
|
359
|
+
];
|
|
360
|
+
if (pattern && !candidates.some((candidate) => pattern.test(candidate))) continue;
|
|
361
|
+
const source = readUtf8File(file.absolutePath);
|
|
362
|
+
entries.push({
|
|
363
|
+
contentClass: file.contentClass,
|
|
364
|
+
depth,
|
|
365
|
+
kind: entry.kind,
|
|
366
|
+
owners: metadata?.owners ?? [],
|
|
367
|
+
path: entry.path || ".",
|
|
368
|
+
title: entry.title,
|
|
369
|
+
...metadata?.description ? { description: metadata.description } : {},
|
|
370
|
+
...options.content ? { content: source } : {}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
entries: entries.sort(compareTreeEntries),
|
|
375
|
+
root,
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
target: target || "."
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function classifyTreePath(path) {
|
|
381
|
+
return classifyContextContent(path);
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/core/internal/validate-members.ts
|
|
385
|
+
const VALID_TYPES = new Set(["human", "agent"]);
|
|
386
|
+
const VALID_STATUSES = new Set(["invited"]);
|
|
387
|
+
function addFinding$1(findings, code, path, message) {
|
|
388
|
+
findings.push({
|
|
389
|
+
code,
|
|
390
|
+
message,
|
|
391
|
+
path
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function validateMember(nodePath, treeRoot) {
|
|
395
|
+
const findings = [];
|
|
396
|
+
const location = toTreeRelativePosixPath(treeRoot, nodePath);
|
|
397
|
+
const document = readContextDocument(nodePath);
|
|
398
|
+
if (document.frontmatter === "missing") {
|
|
399
|
+
addFinding$1(findings, VALIDATION_CODES.memberFrontmatterMissing, location, "no frontmatter found");
|
|
400
|
+
return findings;
|
|
401
|
+
}
|
|
402
|
+
if (document.frontmatter === "invalid" || document.data === null) {
|
|
403
|
+
addFinding$1(findings, VALIDATION_CODES.memberFrontmatterParse, location, `frontmatter could not be parsed${document.error === void 0 ? "" : `: ${document.error}`}`);
|
|
404
|
+
return findings;
|
|
405
|
+
}
|
|
406
|
+
if (!readNonEmptyStringField(document.data, "title").valid) addFinding$1(findings, VALIDATION_CODES.memberTitleInvalid, location, "missing or invalid 'title' field");
|
|
407
|
+
const owners = readNonEmptyStringArrayField(document.data, "owners");
|
|
408
|
+
if (!owners.present) addFinding$1(findings, VALIDATION_CODES.memberOwnersMissing, location, "missing 'owners' field");
|
|
409
|
+
else if (!owners.valid) addFinding$1(findings, VALIDATION_CODES.memberOwnersInvalid, location, "'owners' must be a non-empty string array");
|
|
410
|
+
const memberType = readNonEmptyStringField(document.data, "type");
|
|
411
|
+
if (!memberType.present) addFinding$1(findings, VALIDATION_CODES.memberTypeMissing, location, "missing 'type' field");
|
|
412
|
+
else if (!memberType.valid) addFinding$1(findings, VALIDATION_CODES.memberTypeShape, location, "'type' must be a non-empty string");
|
|
413
|
+
else if (!VALID_TYPES.has(memberType.value)) addFinding$1(findings, VALIDATION_CODES.memberTypeInvalid, location, `invalid type '${memberType.value}' — must be one of: ${[...VALID_TYPES].sort().join(", ")}`);
|
|
414
|
+
const status = readNonEmptyStringField(document.data, "status");
|
|
415
|
+
if (status.present && !status.valid) addFinding$1(findings, VALIDATION_CODES.memberStatusShape, location, "'status' must be a non-empty string when present");
|
|
416
|
+
else if (status.valid && !VALID_STATUSES.has(status.value)) addFinding$1(findings, VALIDATION_CODES.memberStatusInvalid, location, `invalid status '${status.value}' — must be one of: ${[...VALID_STATUSES].sort().join(", ")}`);
|
|
417
|
+
const role = readNonEmptyStringField(document.data, "role");
|
|
418
|
+
if (!role.present) addFinding$1(findings, VALIDATION_CODES.memberRoleInvalid, location, "missing 'role' field");
|
|
419
|
+
else if (!role.valid) addFinding$1(findings, VALIDATION_CODES.memberRoleShape, location, "'role' must be a non-empty string");
|
|
420
|
+
const domains = readNonEmptyStringArrayField(document.data, "domains");
|
|
421
|
+
if (!domains.present) addFinding$1(findings, VALIDATION_CODES.memberDomainsInvalid, location, "missing 'domains' field");
|
|
422
|
+
else if (!domains.valid) {
|
|
423
|
+
const value = document.data.domains;
|
|
424
|
+
addFinding$1(findings, Array.isArray(value) && value.length === 0 ? VALIDATION_CODES.memberDomainsInvalid : VALIDATION_CODES.memberDomainsShape, location, Array.isArray(value) && value.length === 0 ? "'domains' must contain at least one entry" : "'domains' must be a non-empty string array");
|
|
425
|
+
}
|
|
426
|
+
return findings;
|
|
427
|
+
}
|
|
428
|
+
function collectMemberValidationFindings(treeRoot) {
|
|
429
|
+
const membersDir = join(treeRoot, "members");
|
|
430
|
+
const findings = [];
|
|
431
|
+
if (!existsSync(membersDir)) {
|
|
432
|
+
addFinding$1(findings, VALIDATION_CODES.membersDirectoryMissing, "members/", `Members directory not found: ${membersDir}`);
|
|
433
|
+
return findings;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const membersStat = lstatSync(membersDir);
|
|
437
|
+
if (membersStat.isSymbolicLink()) return findings;
|
|
438
|
+
if (!membersStat.isDirectory()) {
|
|
439
|
+
addFinding$1(findings, VALIDATION_CODES.membersDirectoryMissing, "members/", `Members directory not found: ${membersDir}`);
|
|
440
|
+
return findings;
|
|
441
|
+
}
|
|
442
|
+
} catch {
|
|
443
|
+
addFinding$1(findings, VALIDATION_CODES.membersDirectoryMissing, "members/", `Members directory not found: ${membersDir}`);
|
|
444
|
+
return findings;
|
|
445
|
+
}
|
|
446
|
+
let memberCount = 0;
|
|
447
|
+
function walk(dir, requireNode) {
|
|
448
|
+
const entries = readdirSync(dir, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) continue;
|
|
451
|
+
const childPath = join(dir, entry.name);
|
|
452
|
+
const nodePath = join(childPath, "NODE.md");
|
|
453
|
+
if (!existsSync(nodePath)) {
|
|
454
|
+
if (requireNode) {
|
|
455
|
+
const path = `${toTreeRelativePosixPath(treeRoot, childPath)}/`;
|
|
456
|
+
addFinding$1(findings, VALIDATION_CODES.memberNodeMissing, path, "directory exists but is missing NODE.md");
|
|
457
|
+
walk(childPath, false);
|
|
458
|
+
}
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
memberCount += 1;
|
|
462
|
+
findings.push(...validateMember(nodePath, treeRoot));
|
|
463
|
+
walk(childPath, false);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
walk(membersDir, true);
|
|
467
|
+
if (memberCount === 0) addFinding$1(findings, VALIDATION_CODES.memberNodesEmpty, "members/", "no member nodes were found");
|
|
468
|
+
return findings;
|
|
469
|
+
}
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/core/internal/context-links.ts
|
|
472
|
+
function stripQueryAndFragment(target) {
|
|
473
|
+
const indexes = [target.indexOf("?"), target.indexOf("#")].filter((index) => index >= 0);
|
|
474
|
+
const end = indexes.length === 0 ? target.length : Math.min(...indexes);
|
|
475
|
+
return target.slice(0, end);
|
|
476
|
+
}
|
|
477
|
+
function decodeTarget(target) {
|
|
478
|
+
try {
|
|
479
|
+
return decodeURIComponent(target);
|
|
480
|
+
} catch {
|
|
481
|
+
return target;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function isWindowsAbsoluteTarget(target) {
|
|
485
|
+
return /^[a-z]:[\\/]/iu.test(target) || /^\\/u.test(target);
|
|
486
|
+
}
|
|
487
|
+
function isTreeLocalTarget(target) {
|
|
488
|
+
const trimmed = target.trim();
|
|
489
|
+
if (isWindowsAbsoluteTarget(decodeTarget(stripQueryAndFragment(trimmed)))) return true;
|
|
490
|
+
return trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("//") && !/^[a-z][a-z\d+.-]*:/iu.test(trimmed);
|
|
491
|
+
}
|
|
492
|
+
function targetExists(path, softLink) {
|
|
493
|
+
try {
|
|
494
|
+
const stat = statSync(path);
|
|
495
|
+
if (stat.isFile()) return !softLink || path.endsWith(".md");
|
|
496
|
+
return stat.isDirectory() && (!softLink || existsSync(resolve(path, "NODE.md")));
|
|
497
|
+
} catch {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function resolveLocalTreeTarget(options) {
|
|
502
|
+
if (!isTreeLocalTarget(options.target)) return null;
|
|
503
|
+
const decodedTarget = decodeTarget(stripQueryAndFragment(options.target.trim()));
|
|
504
|
+
const withoutSuffix = decodedTarget.replace(/\\/gu, "/");
|
|
505
|
+
if (withoutSuffix.length === 0) return null;
|
|
506
|
+
if (isWindowsAbsoluteTarget(decodedTarget)) return {
|
|
507
|
+
contentClass: classifyContextContent(withoutSuffix),
|
|
508
|
+
escaped: true,
|
|
509
|
+
exists: false,
|
|
510
|
+
relativePath: withoutSuffix
|
|
511
|
+
};
|
|
512
|
+
const sourceDirectory = posix.dirname(options.sourcePath);
|
|
513
|
+
const relativePath = posix.normalize(options.softLink || withoutSuffix.startsWith("/") ? withoutSuffix.replace(/^\/+/, "") : posix.join(sourceDirectory, withoutSuffix));
|
|
514
|
+
const absoluteRoot = resolve(options.treeRoot);
|
|
515
|
+
const absoluteTarget = resolve(absoluteRoot, relativePath);
|
|
516
|
+
const lexicalEscape = !isPathInside(absoluteRoot, absoluteTarget);
|
|
517
|
+
let contentClass = classifyContextContent(relativePath);
|
|
518
|
+
if (lexicalEscape) return {
|
|
519
|
+
contentClass,
|
|
520
|
+
escaped: true,
|
|
521
|
+
exists: false,
|
|
522
|
+
relativePath
|
|
523
|
+
};
|
|
524
|
+
if (!targetExists(absoluteTarget, options.softLink)) return {
|
|
525
|
+
contentClass,
|
|
526
|
+
escaped: false,
|
|
527
|
+
exists: false,
|
|
528
|
+
relativePath
|
|
529
|
+
};
|
|
530
|
+
try {
|
|
531
|
+
const realRoot = realpathSync(absoluteRoot);
|
|
532
|
+
const realTarget = realpathSync(absoluteTarget);
|
|
533
|
+
if (!isPathInside(realRoot, realTarget)) return {
|
|
534
|
+
contentClass,
|
|
535
|
+
escaped: true,
|
|
536
|
+
exists: true,
|
|
537
|
+
relativePath
|
|
538
|
+
};
|
|
539
|
+
contentClass = classifyContextContent(relative(realRoot, realTarget).replace(/\\/gu, "/"));
|
|
540
|
+
} catch {
|
|
541
|
+
return {
|
|
542
|
+
contentClass,
|
|
543
|
+
escaped: false,
|
|
544
|
+
exists: false,
|
|
545
|
+
relativePath
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
contentClass,
|
|
550
|
+
escaped: false,
|
|
551
|
+
exists: true,
|
|
552
|
+
relativePath
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function readMarkdownLinkTargets(markdown) {
|
|
556
|
+
const root = fromMarkdown(markdown);
|
|
557
|
+
const targets = [];
|
|
558
|
+
function visit(node) {
|
|
559
|
+
if (!isRecord(node)) return;
|
|
560
|
+
if ((node.type === "link" || node.type === "image" || node.type === "definition") && typeof node.url === "string") targets.push(node.url);
|
|
561
|
+
if (Array.isArray(node.children)) for (const child of node.children) visit(child);
|
|
562
|
+
}
|
|
563
|
+
visit(root);
|
|
564
|
+
return targets;
|
|
565
|
+
}
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region src/core/internal/validate-nodes.ts
|
|
568
|
+
const MEMBERS_INDEX_PATH = "members/NODE.md";
|
|
569
|
+
function addFinding(findings, code, path, message, target) {
|
|
570
|
+
findings.push({
|
|
571
|
+
code,
|
|
572
|
+
message,
|
|
573
|
+
path,
|
|
574
|
+
...target === void 0 ? {} : { target }
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function validateRequiredNodeMetadata(document, path, findings) {
|
|
578
|
+
if (document.frontmatter === "missing") {
|
|
579
|
+
addFinding(findings, VALIDATION_CODES.frontmatterMissing, path, "missing frontmatter");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (document.frontmatter === "invalid" || document.data === null) {
|
|
583
|
+
addFinding(findings, VALIDATION_CODES.frontmatterParse, path, `frontmatter could not be parsed${document.error === void 0 ? "" : `: ${document.error}`}`);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const title = readNonEmptyStringField(document.data, "title");
|
|
587
|
+
if (!title.present) addFinding(findings, VALIDATION_CODES.titleMissing, path, "missing 'title' field in frontmatter");
|
|
588
|
+
else if (!title.valid) addFinding(findings, VALIDATION_CODES.titleInvalid, path, "'title' must be a non-empty string");
|
|
589
|
+
const owners = readNonEmptyStringArrayField(document.data, "owners");
|
|
590
|
+
if (!owners.present) addFinding(findings, VALIDATION_CODES.ownersMissing, path, "missing 'owners' field in frontmatter");
|
|
591
|
+
else if (!owners.valid) addFinding(findings, VALIDATION_CODES.ownersInvalid, path, "'owners' must be a non-empty string array");
|
|
592
|
+
const description = readNonEmptyStringField(document.data, "description");
|
|
593
|
+
if (description.present && !description.valid) addFinding(findings, VALIDATION_CODES.descriptionInvalid, path, "'description' must be a non-empty string when present");
|
|
594
|
+
}
|
|
595
|
+
function readSoftLinks(document, path, findings) {
|
|
596
|
+
if (document.data === null) return [];
|
|
597
|
+
const softLinks = readNonEmptyStringArrayField(document.data, "soft_links");
|
|
598
|
+
if (!softLinks.present) return [];
|
|
599
|
+
if (!softLinks.valid) {
|
|
600
|
+
addFinding(findings, VALIDATION_CODES.softLinksInvalid, path, "'soft_links' must be a non-empty string array when present");
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
return softLinks.value;
|
|
604
|
+
}
|
|
605
|
+
function validateSoftLinks(options) {
|
|
606
|
+
for (const target of readSoftLinks(options.document, options.path, options.findings)) {
|
|
607
|
+
const resolved = resolveLocalTreeTarget({
|
|
608
|
+
sourcePath: options.path,
|
|
609
|
+
target,
|
|
610
|
+
treeRoot: options.treeRoot,
|
|
611
|
+
softLink: true
|
|
612
|
+
});
|
|
613
|
+
if (resolved === null || !resolved.exists) addFinding(options.findings, VALIDATION_CODES.softLinkBroken, options.path, "broken soft_links target", target);
|
|
614
|
+
if (resolved === null) continue;
|
|
615
|
+
if (!options.allowArchive && resolved.contentClass === "archive-supporting") addFinding(options.findings, VALIDATION_CODES.softLinkArchiveDependency, options.path, "normal content must not link to archive/supporting content", target);
|
|
616
|
+
if (resolved.escaped) addFinding(options.findings, VALIDATION_CODES.softLinkPathEscape, options.path, "soft_links target resolves outside the Context Tree root", target);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function validateMarkdownLinks(document, path, treeRoot, findings) {
|
|
620
|
+
for (const target of readMarkdownLinkTargets(document.body)) {
|
|
621
|
+
const resolved = resolveLocalTreeTarget({
|
|
622
|
+
sourcePath: path,
|
|
623
|
+
target,
|
|
624
|
+
treeRoot,
|
|
625
|
+
softLink: false
|
|
626
|
+
});
|
|
627
|
+
if (resolved === null) continue;
|
|
628
|
+
if (resolved.contentClass === "archive-supporting") addFinding(findings, VALIDATION_CODES.markdownArchiveDependency, path, "normal content must not link to archive/supporting content", target);
|
|
629
|
+
if (resolved.escaped) addFinding(findings, VALIDATION_CODES.markdownPathEscape, path, "Markdown link resolves outside the Context Tree root", target);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function collectNodeValidationFindings(treeRoot) {
|
|
633
|
+
const findings = [];
|
|
634
|
+
const scannedByContentClass = emptyContentClassCounts();
|
|
635
|
+
const content = collectContextMarkdownContent(treeRoot);
|
|
636
|
+
for (const directory of content.directorySymlinks) addFinding(findings, directory.escaped ? VALIDATION_CODES.directorySymlinkPathEscape : VALIDATION_CODES.directorySymlinkUnsupported, directory.relativePath, directory.escaped ? "directory symlink resolves outside the Context Tree root" : "Context Tree domain directories must not be symlinks");
|
|
637
|
+
for (const file of content.files) {
|
|
638
|
+
if (file.relativePath === "SCOPE.md") continue;
|
|
639
|
+
scannedByContentClass[file.contentClass] += 1;
|
|
640
|
+
if (file.unresolved) {
|
|
641
|
+
addFinding(findings, VALIDATION_CODES.markdownFileSymlinkBroken, file.relativePath, "Markdown file symlink target cannot be resolved");
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (file.escaped) {
|
|
645
|
+
addFinding(findings, VALIDATION_CODES.markdownFilePathEscape, file.relativePath, "Markdown file resolves outside the Context Tree root");
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (file.unsupported) {
|
|
649
|
+
addFinding(findings, VALIDATION_CODES.markdownFileSymlinkUnsupported, file.relativePath, "Markdown file symlink must resolve to a regular file");
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (file.canonicalContentClass !== file.contentClass) {
|
|
653
|
+
addFinding(findings, VALIDATION_CODES.markdownFileContentClassMismatch, file.relativePath, `Markdown file symlink crosses content-class boundary from ${file.contentClass} to ${file.canonicalContentClass}`, file.canonicalRelativePath);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (file.contentClass === "repo-infra" || file.contentClass === "archive-supporting") continue;
|
|
657
|
+
const document = readContextDocument(file.absolutePath);
|
|
658
|
+
if (file.contentClass === "member" && file.relativePath !== MEMBERS_INDEX_PATH) {
|
|
659
|
+
if (document.frontmatter === "missing") continue;
|
|
660
|
+
if (document.frontmatter === "invalid") continue;
|
|
661
|
+
validateSoftLinks({
|
|
662
|
+
allowArchive: true,
|
|
663
|
+
document,
|
|
664
|
+
findings,
|
|
665
|
+
path: file.relativePath,
|
|
666
|
+
treeRoot
|
|
667
|
+
});
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
validateRequiredNodeMetadata(document, file.relativePath, findings);
|
|
671
|
+
validateSoftLinks({
|
|
672
|
+
allowArchive: file.contentClass === "member",
|
|
673
|
+
document,
|
|
674
|
+
findings,
|
|
675
|
+
path: file.relativePath,
|
|
676
|
+
treeRoot
|
|
677
|
+
});
|
|
678
|
+
if (file.contentClass === "normal") validateMarkdownLinks(document, file.relativePath, treeRoot, findings);
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
findings,
|
|
682
|
+
scannedByContentClass
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/core/verify.ts
|
|
687
|
+
function scopeFindings(root) {
|
|
688
|
+
const path = join(root, "SCOPE.md");
|
|
689
|
+
if (!existsSync(path)) return [];
|
|
690
|
+
try {
|
|
691
|
+
const entry = lstatSync(path);
|
|
692
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("SCOPE.md must be a regular root file and must not be a symlink.");
|
|
693
|
+
parseContextTreeScope(readUtf8File(path));
|
|
694
|
+
return [];
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return [{
|
|
697
|
+
code: VALIDATION_CODES.scopeInvalid,
|
|
698
|
+
message: error instanceof Error ? error.message : String(error),
|
|
699
|
+
path: "SCOPE.md"
|
|
700
|
+
}];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function deduplicate(findings) {
|
|
704
|
+
const seen = /* @__PURE__ */ new Set();
|
|
705
|
+
return findings.filter((finding) => {
|
|
706
|
+
const key = `${finding.code}\0${finding.path}\0${finding.target ?? ""}`;
|
|
707
|
+
if (seen.has(key)) return false;
|
|
708
|
+
seen.add(key);
|
|
709
|
+
return true;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
function verifyTree(treePath) {
|
|
713
|
+
const root = resolveTreeRoot(treePath);
|
|
714
|
+
const nodeResult = collectNodeValidationFindings(root);
|
|
715
|
+
const memberFindings = collectMemberValidationFindings(root);
|
|
716
|
+
const findings = deduplicate([
|
|
717
|
+
...existsSync(join(root, "NODE.md")) ? [] : [{
|
|
718
|
+
code: VALIDATION_CODES.rootMissing,
|
|
719
|
+
message: "root NODE.md is missing",
|
|
720
|
+
path: "NODE.md"
|
|
721
|
+
}],
|
|
722
|
+
...scopeFindings(root),
|
|
723
|
+
...nodeResult.findings,
|
|
724
|
+
...memberFindings
|
|
725
|
+
]);
|
|
726
|
+
return {
|
|
727
|
+
findings,
|
|
728
|
+
ok: findings.length === 0,
|
|
729
|
+
root,
|
|
730
|
+
scannedByContentClass: nodeResult.scannedByContentClass,
|
|
731
|
+
schemaVersion: 1
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/core/scaffold.ts
|
|
736
|
+
function template(name, values) {
|
|
737
|
+
let result = readFileSync(resolvePackagedResource("templates", name), "utf8");
|
|
738
|
+
for (const [key, value] of Object.entries(values)) result = result.replaceAll(`{{${key}}}`, value);
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
function scaffoldTree(options) {
|
|
742
|
+
const root = resolve(options.path);
|
|
743
|
+
const destination = lstatSync(root, { throwIfNoEntry: false });
|
|
744
|
+
if (destination !== void 0) {
|
|
745
|
+
if (destination.isSymbolicLink() || !destination.isDirectory()) throw new Error(`Refusing to scaffold into a symlink or non-directory destination: ${root}`);
|
|
746
|
+
if (readdirSync(root).length > 0) throw new Error(`Refusing to scaffold into a non-empty directory: ${root}`);
|
|
747
|
+
}
|
|
748
|
+
const owner = options.owner.trim();
|
|
749
|
+
const repository = options.repository.trim();
|
|
750
|
+
const title = options.title.trim();
|
|
751
|
+
if (!/^[a-z\d][a-z\d._-]{0,127}$/iu.test(owner)) throw new Error("Owner must be a portable identifier containing only letters, digits, dot, underscore, or hyphen.");
|
|
752
|
+
const unsafeTitle = [...title].some((character) => {
|
|
753
|
+
const code = character.codePointAt(0);
|
|
754
|
+
return code !== void 0 && (code <= 31 || code === 127);
|
|
755
|
+
});
|
|
756
|
+
if (!title || title.length > 200 || unsafeTitle) throw new Error("Tree title must be a non-empty single line of at most 200 characters.");
|
|
757
|
+
const repositoryParts = repository.split("/");
|
|
758
|
+
const [repositoryOwner, repositoryName] = repositoryParts;
|
|
759
|
+
if (options.repository !== repository || repositoryParts.length !== 2 || repositoryOwner === void 0 || repositoryName === void 0 || !/^[A-Za-z\d](?:[A-Za-z\d-]{0,37}[A-Za-z\d])?$/u.test(repositoryOwner) || !/^[A-Za-z\d._-]{1,100}$/u.test(repositoryName) || repositoryName === "." || repositoryName === ".." || /\.git$/iu.test(repositoryName)) throw new Error("Repository must be an explicit GitHub OWNER/REPO identity.");
|
|
760
|
+
const manifest = readPackageManifest();
|
|
761
|
+
if (!("version" in manifest) || typeof manifest.version !== "string") throw new Error("Package version is missing or invalid.");
|
|
762
|
+
const values = {
|
|
763
|
+
owner,
|
|
764
|
+
ownerJson: JSON.stringify(owner),
|
|
765
|
+
packageVersion: manifest.version,
|
|
766
|
+
title,
|
|
767
|
+
titleJson: JSON.stringify(title)
|
|
768
|
+
};
|
|
769
|
+
mkdirSync(root, { recursive: true });
|
|
770
|
+
const files = [
|
|
771
|
+
["NODE.md", "root-node.md"],
|
|
772
|
+
["SCOPE.md", "scope.md"],
|
|
773
|
+
["members/NODE.md", "members-index.md"],
|
|
774
|
+
[`members/${values.owner}/NODE.md`, "member-node.md"],
|
|
775
|
+
[".github/workflows/validate-context-tree.yml", "validate-context-tree.yml"]
|
|
776
|
+
];
|
|
777
|
+
for (const [relativePath, source] of files) {
|
|
778
|
+
const path = join(root, relativePath);
|
|
779
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
780
|
+
writeFileSync(path, template(source, values), {
|
|
781
|
+
encoding: "utf8",
|
|
782
|
+
flag: "wx",
|
|
783
|
+
mode: 420
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
files: files.map(([path]) => path),
|
|
788
|
+
root,
|
|
789
|
+
schemaVersion: 1,
|
|
790
|
+
verification: verifyTree(root)
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
//#endregion
|
|
794
|
+
export { readContextTreePolicy as a, readTree as i, verifyTree as n, readPackageManifest as o, classifyTreePath as r, scaffoldTree as t };
|