@first-tree-ai/context-tree 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,2 +1,1087 @@
1
- import { a as readContextTreePolicy, i as readTree, n as verifyTree, t as scaffoldTree } from "./src-DJZoQVCF.mjs";
2
- export { readContextTreePolicy, readTree, scaffoldTree, verifyTree };
1
+ import { A as isRecord, C as githubRepositoryIdentitySchema, S as credentialFreeRepositoryUrlSchema, f as contextTreeLinkSchema, i as VALIDATION_CODES, k as parseMarkdownFrontmatter, t as CLI_ERROR_CODES, w as parseContextTreeRootNode } from "./schemas-BWM6Q6iz.mjs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { basename, dirname, isAbsolute, join, parse, posix, relative, resolve } from "node:path";
6
+ import { z } from "zod";
7
+ import { fromMarkdown } from "mdast-util-from-markdown";
8
+ import { fileURLToPath } from "node:url";
9
+ //#region src/core/internal/filesystem.ts
10
+ function readUtf8File(path) {
11
+ return new TextDecoder("utf-8", { fatal: true }).decode(readFileSync(path));
12
+ }
13
+ //#endregion
14
+ //#region src/core/internal/github-repository.ts
15
+ function parseGitHubRepositoryIdentity(repository) {
16
+ githubRepositoryIdentitySchema.parse(repository);
17
+ return repository.split("/")[1] ?? "";
18
+ }
19
+ function repositoryIdentityFromGitHubUrl(repositoryUrl) {
20
+ try {
21
+ credentialFreeRepositoryUrlSchema.parse(repositoryUrl);
22
+ } catch {
23
+ throw new Error("Context Tree origin must be a safe credential-free github.com repository URL.");
24
+ }
25
+ let host;
26
+ let path;
27
+ const scp = /^(?:git@)?([^:]+):(.+)$/u.exec(repositoryUrl);
28
+ if (scp !== null && !repositoryUrl.includes("://")) {
29
+ host = scp[1] ?? "";
30
+ path = scp[2] ?? "";
31
+ } else {
32
+ let parsed;
33
+ try {
34
+ parsed = new URL(repositoryUrl);
35
+ } catch {
36
+ throw new Error("Context Tree origin must be a safe credential-free github.com repository URL.");
37
+ }
38
+ if (parsed.password || (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username) throw new Error("Context Tree origin must be a safe credential-free github.com repository URL.");
39
+ host = parsed.hostname;
40
+ path = parsed.pathname;
41
+ }
42
+ if (host.toLowerCase() !== "github.com") throw new Error("Context Tree origin must use github.com.");
43
+ const identity = path.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "");
44
+ try {
45
+ parseGitHubRepositoryIdentity(identity);
46
+ } catch {
47
+ throw new Error("Context Tree origin must identify a safe GitHub OWNER/REPO repository.");
48
+ }
49
+ return identity;
50
+ }
51
+ function canonicalGitHubRepositoryUrl(repository) {
52
+ parseGitHubRepositoryIdentity(repository);
53
+ return `https://github.com/${repository}.git`;
54
+ }
55
+ //#endregion
56
+ //#region src/core/path.ts
57
+ function isPathInside(root, target) {
58
+ const path = relative(root, target);
59
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
60
+ }
61
+ function resolveTreeRoot(path) {
62
+ const absolute = resolve(path);
63
+ const entry = lstatSync(absolute);
64
+ if (entry.isSymbolicLink() || !entry.isDirectory()) throw new Error(`Context Tree root must be a real directory: ${path}`);
65
+ return realpathSync(absolute);
66
+ }
67
+ function toPosixPath(path) {
68
+ return path.replace(/\\/gu, "/");
69
+ }
70
+ //#endregion
71
+ //#region src/core/internal/content-class.ts
72
+ const GENERATED_DIRECTORY_NAMES = new Set([
73
+ "node_modules",
74
+ "__pycache__",
75
+ "dist",
76
+ "build",
77
+ ".next",
78
+ ".turbo"
79
+ ]);
80
+ const REPO_INFRA_MARKDOWN_FILES = new Set(["AGENTS.md", "CLAUDE.md"]);
81
+ const MANAGED_SYMLINK_PATHS = new Set(["WHITEPAPER.md"]);
82
+ function toTreeRelativePosixPath(treeRoot, targetPath) {
83
+ return relative(treeRoot, targetPath).replace(/\\/gu, "/");
84
+ }
85
+ function classifyContextContent(relativePath) {
86
+ const parts = relativePath.replace(/\\/gu, "/").replace(/^\.\//u, "").split("/").filter((part) => part.length > 0);
87
+ if (parts.some((part) => part.startsWith(".") || GENERATED_DIRECTORY_NAMES.has(part)) || parts[0] === "scripts" || REPO_INFRA_MARKDOWN_FILES.has(parts.at(-1) ?? "")) return "repo-infra";
88
+ if (parts[0] === "members") return "member";
89
+ return "normal";
90
+ }
91
+ function emptyContentClassCounts() {
92
+ return {
93
+ normal: 0,
94
+ member: 0,
95
+ "repo-infra": 0
96
+ };
97
+ }
98
+ function readDirectoryEntries(path) {
99
+ try {
100
+ return readdirSync(path, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
101
+ } catch {
102
+ return [];
103
+ }
104
+ }
105
+ function canonicalTarget$1(realTreeRoot, path) {
106
+ try {
107
+ const realTarget = realpathSync(path);
108
+ if (!isPathInside(realTreeRoot, realTarget)) return { kind: "escaped" };
109
+ return {
110
+ kind: "resolved",
111
+ relativePath: toTreeRelativePosixPath(realTreeRoot, realTarget)
112
+ };
113
+ } catch {
114
+ return { kind: "unresolved" };
115
+ }
116
+ }
117
+ function inspectMarkdownSymlink(realTreeRoot, absolutePath, contentClass) {
118
+ let targetStat;
119
+ try {
120
+ targetStat = statSync(absolutePath);
121
+ } catch {
122
+ return { kind: "unresolved" };
123
+ }
124
+ const target = canonicalTarget$1(realTreeRoot, absolutePath);
125
+ if (target.kind !== "resolved") return target;
126
+ if (!targetStat.isFile()) return { kind: "unsupported" };
127
+ if (classifyContextContent(target.relativePath) !== contentClass) return {
128
+ kind: "content-class-mismatch",
129
+ canonicalRelativePath: target.relativePath
130
+ };
131
+ return { kind: "regular" };
132
+ }
133
+ function collectContextMarkdownContent(treeRoot) {
134
+ const directories = [];
135
+ const directorySymlinks = [];
136
+ const files = [];
137
+ const realTreeRoot = realpathSync(treeRoot);
138
+ function walk(directoryPath) {
139
+ for (const entry of readDirectoryEntries(directoryPath)) {
140
+ const absolutePath = join(directoryPath, entry.name);
141
+ const relativePath = toTreeRelativePosixPath(treeRoot, absolutePath);
142
+ const contentClass = classifyContextContent(relativePath);
143
+ if (entry.isDirectory()) {
144
+ if (contentClass !== "repo-infra") {
145
+ directories.push(relativePath);
146
+ walk(absolutePath);
147
+ }
148
+ continue;
149
+ }
150
+ const symbolicLink = entry.isSymbolicLink();
151
+ if (symbolicLink) try {
152
+ if (statSync(absolutePath).isDirectory()) {
153
+ if (contentClass !== "repo-infra" || entry.name.endsWith(".md")) {
154
+ const target = canonicalTarget$1(realTreeRoot, absolutePath);
155
+ directorySymlinks.push({
156
+ escaped: target.kind === "escaped",
157
+ relativePath
158
+ });
159
+ }
160
+ continue;
161
+ }
162
+ } catch {
163
+ if (MANAGED_SYMLINK_PATHS.has(relativePath)) continue;
164
+ if (entry.name.endsWith(".md")) files.push({
165
+ absolutePath,
166
+ contentClass,
167
+ inspection: { kind: "unresolved" },
168
+ relativePath
169
+ });
170
+ continue;
171
+ }
172
+ if (!entry.isFile() && !symbolicLink || !entry.name.endsWith(".md")) continue;
173
+ if (symbolicLink && MANAGED_SYMLINK_PATHS.has(relativePath)) continue;
174
+ files.push({
175
+ absolutePath,
176
+ contentClass,
177
+ inspection: symbolicLink ? inspectMarkdownSymlink(realTreeRoot, absolutePath, contentClass) : { kind: "regular" },
178
+ relativePath
179
+ });
180
+ }
181
+ }
182
+ walk(treeRoot);
183
+ return {
184
+ directories,
185
+ directorySymlinks,
186
+ files
187
+ };
188
+ }
189
+ //#endregion
190
+ //#region src/core/internal/context-document.ts
191
+ function readContextDocument(path) {
192
+ try {
193
+ return parseMarkdownFrontmatter(readUtf8File(path));
194
+ } catch (error) {
195
+ return {
196
+ body: "",
197
+ data: null,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ frontmatter: "invalid"
200
+ };
201
+ }
202
+ }
203
+ function readNonEmptyStringField(data, key) {
204
+ if (!(key in data)) return {
205
+ present: false,
206
+ valid: false
207
+ };
208
+ const value = data[key];
209
+ if (typeof value !== "string" || value.trim().length === 0) return {
210
+ present: true,
211
+ valid: false
212
+ };
213
+ return {
214
+ present: true,
215
+ valid: true,
216
+ value: value.trim()
217
+ };
218
+ }
219
+ function readNonEmptyStringArrayField(data, key) {
220
+ if (!(key in data)) return {
221
+ present: false,
222
+ valid: false
223
+ };
224
+ const value = data[key];
225
+ if (!Array.isArray(value) || value.length === 0) return {
226
+ present: true,
227
+ valid: false
228
+ };
229
+ const items = [];
230
+ for (const item of value) {
231
+ if (typeof item !== "string" || item.trim().length === 0) return {
232
+ present: true,
233
+ valid: false
234
+ };
235
+ items.push(item.trim());
236
+ }
237
+ return {
238
+ present: true,
239
+ valid: true,
240
+ value: items
241
+ };
242
+ }
243
+ function readNodeDocument(path) {
244
+ const document = readContextDocument(path);
245
+ if (document.frontmatter !== "valid") return null;
246
+ const title = readNonEmptyStringField(document.data, "title");
247
+ const description = readNonEmptyStringField(document.data, "description");
248
+ if (!title.valid || description.present && !description.valid) return null;
249
+ return {
250
+ body: document.body,
251
+ frontmatter: document.data,
252
+ title: title.value,
253
+ ...description.valid ? { description: description.value } : {}
254
+ };
255
+ }
256
+ //#endregion
257
+ //#region src/core/internal/context-links.ts
258
+ function stripQueryAndFragment(target) {
259
+ const indexes = [target.indexOf("?"), target.indexOf("#")].filter((index) => index >= 0);
260
+ const end = indexes.length === 0 ? target.length : Math.min(...indexes);
261
+ return target.slice(0, end);
262
+ }
263
+ function decodeTarget(target) {
264
+ try {
265
+ return decodeURIComponent(target);
266
+ } catch {
267
+ return target;
268
+ }
269
+ }
270
+ function isWindowsAbsoluteTarget(target) {
271
+ return /^[a-z]:[\\/]/iu.test(target) || /^\\/u.test(target);
272
+ }
273
+ function isTreeLocalTarget(target) {
274
+ const trimmed = target.trim();
275
+ if (isWindowsAbsoluteTarget(decodeTarget(stripQueryAndFragment(trimmed)))) return true;
276
+ return trimmed.length > 0 && !trimmed.startsWith("#") && !trimmed.startsWith("//") && !/^[a-z][a-z\d+.-]*:/iu.test(trimmed);
277
+ }
278
+ function targetExists(path, softLink) {
279
+ try {
280
+ const stat = statSync(path);
281
+ if (stat.isFile()) return !softLink || path.endsWith(".md");
282
+ return stat.isDirectory() && (!softLink || existsSync(resolve(path, "NODE.md")));
283
+ } catch {
284
+ return false;
285
+ }
286
+ }
287
+ function resolveLocalTreeTarget(options) {
288
+ if (!isTreeLocalTarget(options.target)) return null;
289
+ const decodedTarget = decodeTarget(stripQueryAndFragment(options.target.trim()));
290
+ const withoutSuffix = decodedTarget.replace(/\\/gu, "/");
291
+ if (withoutSuffix.length === 0) return null;
292
+ if (isWindowsAbsoluteTarget(decodedTarget)) return "escaped-missing";
293
+ const sourceDirectory = posix.dirname(options.sourcePath);
294
+ const relativePath = posix.normalize(options.softLink || withoutSuffix.startsWith("/") ? withoutSuffix.replace(/^\/+/, "") : posix.join(sourceDirectory, withoutSuffix));
295
+ const absoluteRoot = resolve(options.treeRoot);
296
+ const absoluteTarget = resolve(absoluteRoot, relativePath);
297
+ if (!isPathInside(absoluteRoot, absoluteTarget)) return "escaped-missing";
298
+ if (!targetExists(absoluteTarget, options.softLink)) return "missing";
299
+ try {
300
+ if (!isPathInside(realpathSync(absoluteRoot), realpathSync(absoluteTarget))) return "escaped-existing";
301
+ } catch {
302
+ return "missing";
303
+ }
304
+ return "valid";
305
+ }
306
+ function readMarkdownLinkTargets(markdown) {
307
+ const root = fromMarkdown(markdown);
308
+ const targets = [];
309
+ function visit(node) {
310
+ if (!isRecord(node)) return;
311
+ if ((node.type === "link" || node.type === "image" || node.type === "definition") && typeof node.url === "string") targets.push(node.url);
312
+ if (Array.isArray(node.children)) for (const child of node.children) visit(child);
313
+ }
314
+ visit(root);
315
+ return targets;
316
+ }
317
+ //#endregion
318
+ //#region src/core/internal/validate-nodes.ts
319
+ function addFinding(findings, code, path, message, target) {
320
+ findings.push({
321
+ code,
322
+ message,
323
+ path,
324
+ ...target === void 0 ? {} : { target }
325
+ });
326
+ }
327
+ function validateRequiredNodeMetadata(document, path, findings) {
328
+ if (document.frontmatter === "missing") {
329
+ addFinding(findings, VALIDATION_CODES.frontmatterMissing, path, "missing frontmatter");
330
+ return;
331
+ }
332
+ if (document.frontmatter === "invalid") {
333
+ addFinding(findings, VALIDATION_CODES.frontmatterParse, path, `frontmatter could not be parsed: ${document.error}`);
334
+ return;
335
+ }
336
+ const title = readNonEmptyStringField(document.data, "title");
337
+ if (!title.present) addFinding(findings, VALIDATION_CODES.titleMissing, path, "missing 'title' field in frontmatter");
338
+ else if (!title.valid) addFinding(findings, VALIDATION_CODES.titleInvalid, path, "'title' must be a non-empty string");
339
+ const description = readNonEmptyStringField(document.data, "description");
340
+ if (description.present && !description.valid) addFinding(findings, VALIDATION_CODES.descriptionInvalid, path, "'description' must be a non-empty string when present");
341
+ }
342
+ function validateRootOnlyFields(document, path, findings) {
343
+ if (path === "NODE.md" || document.frontmatter !== "valid") return;
344
+ const fields = ["schemaVersion"].filter((field) => field in document.data);
345
+ if (fields.length > 0) addFinding(findings, VALIDATION_CODES.rootOnlyFields, path, `root-only frontmatter field${fields.length === 1 ? "" : "s"} must appear only in root NODE.md: ${fields.join(", ")}`);
346
+ }
347
+ function readSoftLinks(document, path, findings) {
348
+ if (document.frontmatter !== "valid") return [];
349
+ const softLinks = readNonEmptyStringArrayField(document.data, "soft_links");
350
+ if (!softLinks.present) return [];
351
+ if (!softLinks.valid) {
352
+ addFinding(findings, VALIDATION_CODES.softLinksInvalid, path, "'soft_links' must be a non-empty string array when present");
353
+ return [];
354
+ }
355
+ return softLinks.value;
356
+ }
357
+ function validateSoftLinks(options) {
358
+ for (const target of readSoftLinks(options.document, options.path, options.findings)) {
359
+ const resolved = resolveLocalTreeTarget({
360
+ sourcePath: options.path,
361
+ target,
362
+ treeRoot: options.treeRoot,
363
+ softLink: true
364
+ });
365
+ if (resolved === null || resolved === "missing" || resolved === "escaped-missing") addFinding(options.findings, VALIDATION_CODES.softLinkBroken, options.path, "broken soft_links target", target);
366
+ if (resolved === null) continue;
367
+ if (resolved === "escaped-existing" || resolved === "escaped-missing") addFinding(options.findings, VALIDATION_CODES.softLinkPathEscape, options.path, "soft_links target resolves outside the Context Tree root", target);
368
+ }
369
+ }
370
+ function validateMarkdownLinks(document, path, treeRoot, findings) {
371
+ for (const target of readMarkdownLinkTargets(document.body)) {
372
+ const resolved = resolveLocalTreeTarget({
373
+ sourcePath: path,
374
+ target,
375
+ treeRoot,
376
+ softLink: false
377
+ });
378
+ if (resolved === null) continue;
379
+ if (resolved === "escaped-existing" || resolved === "escaped-missing") addFinding(findings, VALIDATION_CODES.markdownPathEscape, path, "Markdown link resolves outside the Context Tree root", target);
380
+ }
381
+ }
382
+ function collectNodeValidationFindings(treeRoot) {
383
+ const findings = [];
384
+ const scannedByContentClass = emptyContentClassCounts();
385
+ const content = collectContextMarkdownContent(treeRoot);
386
+ for (const directory of content.directories) {
387
+ const nodePath = `${directory}/NODE.md`;
388
+ let hasRegularNode = false;
389
+ try {
390
+ const entry = lstatSync(join(treeRoot, nodePath));
391
+ hasRegularNode = entry.isFile() && !entry.isSymbolicLink();
392
+ } catch {}
393
+ if (!hasRegularNode) addFinding(findings, VALIDATION_CODES.directoryNodeMissing, directory, "Context Tree directory is missing NODE.md");
394
+ }
395
+ 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");
396
+ for (const file of content.files) {
397
+ scannedByContentClass[file.contentClass] += 1;
398
+ if (file.inspection.kind === "unresolved") {
399
+ addFinding(findings, VALIDATION_CODES.markdownFileSymlinkBroken, file.relativePath, "Markdown file symlink target cannot be resolved");
400
+ continue;
401
+ }
402
+ if (file.inspection.kind === "escaped") {
403
+ addFinding(findings, VALIDATION_CODES.markdownFilePathEscape, file.relativePath, "Markdown file resolves outside the Context Tree root");
404
+ continue;
405
+ }
406
+ if (file.inspection.kind === "unsupported") {
407
+ addFinding(findings, VALIDATION_CODES.markdownFileSymlinkUnsupported, file.relativePath, "Markdown file symlink must resolve to a regular file");
408
+ continue;
409
+ }
410
+ if (file.inspection.kind === "content-class-mismatch") {
411
+ const canonicalContentClass = classifyContextContent(file.inspection.canonicalRelativePath);
412
+ addFinding(findings, VALIDATION_CODES.markdownFileContentClassMismatch, file.relativePath, `Markdown file symlink crosses content-class boundary from ${file.contentClass} to ${canonicalContentClass}`, file.inspection.canonicalRelativePath);
413
+ continue;
414
+ }
415
+ if (file.contentClass === "repo-infra") continue;
416
+ const document = readContextDocument(file.absolutePath);
417
+ validateRootOnlyFields(document, file.relativePath, findings);
418
+ if (file.relativePath !== "NODE.md" || document.frontmatter === "valid") validateRequiredNodeMetadata(document, file.relativePath, findings);
419
+ validateSoftLinks({
420
+ document,
421
+ findings,
422
+ path: file.relativePath,
423
+ treeRoot
424
+ });
425
+ validateMarkdownLinks(document, file.relativePath, treeRoot, findings);
426
+ }
427
+ return {
428
+ findings,
429
+ scannedByContentClass
430
+ };
431
+ }
432
+ //#endregion
433
+ //#region src/core/verify.ts
434
+ function rootNodeFindings(root) {
435
+ const path = join(root, "NODE.md");
436
+ if (!existsSync(path)) return [{
437
+ code: VALIDATION_CODES.rootMissing,
438
+ message: "root NODE.md is missing",
439
+ path: "NODE.md"
440
+ }];
441
+ try {
442
+ const entry = lstatSync(path);
443
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Root NODE.md must be a regular file and must not be a symlink.");
444
+ parseContextTreeRootNode(readUtf8File(path));
445
+ return [];
446
+ } catch (error) {
447
+ return [{
448
+ code: VALIDATION_CODES.rootNodeInvalid,
449
+ message: error instanceof Error ? error.message : String(error),
450
+ path: "NODE.md"
451
+ }];
452
+ }
453
+ }
454
+ function deduplicate(findings) {
455
+ const seen = /* @__PURE__ */ new Set();
456
+ return findings.filter((finding) => {
457
+ const key = `${finding.code}\0${finding.path}\0${finding.target ?? ""}`;
458
+ if (seen.has(key)) return false;
459
+ seen.add(key);
460
+ return true;
461
+ });
462
+ }
463
+ function verifyTree(treePath) {
464
+ const root = resolveTreeRoot(treePath);
465
+ const nodeResult = collectNodeValidationFindings(root);
466
+ const findings = deduplicate([...rootNodeFindings(root), ...nodeResult.findings]);
467
+ return {
468
+ findings,
469
+ ok: findings.length === 0,
470
+ root,
471
+ scannedByContentClass: nodeResult.scannedByContentClass,
472
+ schemaVersion: 1
473
+ };
474
+ }
475
+ //#endregion
476
+ //#region src/core/links.ts
477
+ const contextTreeLinksFileSchema = z.object({
478
+ links: z.array(contextTreeLinkSchema),
479
+ schemaVersion: z.literal(1)
480
+ }).strict();
481
+ var LinkError = class extends Error {
482
+ code;
483
+ constructor(code, message) {
484
+ super(message);
485
+ this.name = "LinkError";
486
+ this.code = code;
487
+ }
488
+ };
489
+ function git$1(path, args) {
490
+ const result = spawnSync("git", [
491
+ "-C",
492
+ path,
493
+ ...args
494
+ ], {
495
+ encoding: "utf8",
496
+ stdio: [
497
+ "ignore",
498
+ "pipe",
499
+ "ignore"
500
+ ]
501
+ });
502
+ if (result.error !== void 0 || result.status !== 0) return void 0;
503
+ return result.stdout.replace(/\r?\n$/u, "");
504
+ }
505
+ function requireGit$1(path, args, message, allowEmpty = false) {
506
+ const value = git$1(path, args);
507
+ if (value === void 0 || !allowEmpty && value.length === 0) throw new Error(message);
508
+ return value;
509
+ }
510
+ function normalizeRepositoryUrl(repositoryUrl) {
511
+ try {
512
+ credentialFreeRepositoryUrlSchema.parse(repositoryUrl);
513
+ } catch {
514
+ throw new Error("Git origin must be a canonical credential-free repository URL.");
515
+ }
516
+ try {
517
+ return canonicalGitHubRepositoryUrl(repositoryIdentityFromGitHubUrl(repositoryUrl).toLowerCase());
518
+ } catch {}
519
+ const scp = /^(?:([^@]+)@)?([^:]+):(.+)$/u.exec(repositoryUrl);
520
+ if (scp !== null && !repositoryUrl.includes("://")) return `${scp[1] === void 0 ? "" : `${scp[1].toLowerCase()}@`}${(scp[2] ?? "").toLowerCase()}:${(scp[3] ?? "").replace(/\/+$/gu, "").replace(/\.git$/iu, "")}.git`;
521
+ const parsed = new URL(repositoryUrl);
522
+ parsed.hostname = parsed.hostname.toLowerCase();
523
+ parsed.pathname = `${parsed.pathname.replace(/\/+$/gu, "").replace(/\.git$/iu, "")}.git`;
524
+ return parsed.toString();
525
+ }
526
+ function realDirectory(path) {
527
+ const absolute = resolve(path);
528
+ if (!lstatSync(absolute).isDirectory()) throw new Error("Project path must be a directory.");
529
+ return realpathSync(absolute);
530
+ }
531
+ function identifyProject(projectPath) {
532
+ const path = realDirectory(projectPath);
533
+ const gitRoot = git$1(path, ["rev-parse", "--show-toplevel"]);
534
+ if (gitRoot === void 0) return {
535
+ kind: "directory",
536
+ path
537
+ };
538
+ return {
539
+ kind: "git",
540
+ origin: normalizeRepositoryUrl(requireGit$1(realpathSync(gitRoot), [
541
+ "remote",
542
+ "get-url",
543
+ "origin"
544
+ ], "Git project must have an origin remote."))
545
+ };
546
+ }
547
+ function linksPath() {
548
+ return join(homedir(), ".context-tree", "connections.json");
549
+ }
550
+ function emptyLinks() {
551
+ return {
552
+ links: [],
553
+ schemaVersion: 1
554
+ };
555
+ }
556
+ function loadLinks(required) {
557
+ const path = linksPath();
558
+ if (!existsSync(path)) {
559
+ if (required) throw new LinkError(CLI_ERROR_CODES.noLink, "No Context Tree link exists for this project.");
560
+ return emptyLinks();
561
+ }
562
+ try {
563
+ const entry = lstatSync(path);
564
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("not a regular file");
565
+ return contextTreeLinksFileSchema.parse(JSON.parse(readFileSync(path, "utf8")));
566
+ } catch {
567
+ throw new LinkError(CLI_ERROR_CODES.corruptLink, "Context Tree links are corrupt; run link after repairing or removing the internal links file.");
568
+ }
569
+ }
570
+ function saveLinks(value) {
571
+ const path = linksPath();
572
+ const directory = dirname(path);
573
+ mkdirSync(directory, {
574
+ recursive: true,
575
+ mode: 448
576
+ });
577
+ const directoryEntry = lstatSync(directory);
578
+ if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) throw new Error("Context Tree links directory must be a real directory.");
579
+ const temporary = join(directory, `.links-${process.pid}-${Date.now()}.tmp`);
580
+ writeFileSync(temporary, `${JSON.stringify(contextTreeLinksFileSchema.parse(value), null, 2)}\n`, {
581
+ encoding: "utf8",
582
+ flag: "wx",
583
+ mode: 384
584
+ });
585
+ renameSync(temporary, path);
586
+ chmodSync(path, 384);
587
+ }
588
+ function exactCheckoutRoot(treePath) {
589
+ const absolute = resolve(treePath);
590
+ const entry = lstatSync(absolute);
591
+ const root = realpathSync(absolute);
592
+ if (!entry.isDirectory() || entry.isSymbolicLink() || absolute !== root) throw new Error("Context Tree checkout path must be a real directory with no symlink component.");
593
+ if (realpathSync(requireGit$1(root, ["rev-parse", "--show-toplevel"], "Context Tree checkout must be a Git repository.")) !== root) throw new Error("Context Tree checkout must be the real Git root.");
594
+ return root;
595
+ }
596
+ function checkoutRepository(root) {
597
+ return repositoryIdentityFromGitHubUrl(requireGit$1(root, [
598
+ "remote",
599
+ "get-url",
600
+ "origin"
601
+ ], "Context Tree checkout must have an origin remote."));
602
+ }
603
+ function requireCheckoutClean(root, mode) {
604
+ const status = requireGit$1(root, [
605
+ "status",
606
+ "--porcelain",
607
+ "--untracked-files=all"
608
+ ], "Failed to inspect Context Tree cleanliness.", true);
609
+ if (status.length === 0) return;
610
+ if (mode === "scaffold") {
611
+ const lines = status.split("\n").sort();
612
+ if (git$1(root, [
613
+ "rev-parse",
614
+ "--verify",
615
+ "HEAD"
616
+ ]) === void 0 && JSON.stringify(lines) === JSON.stringify([
617
+ "?? .github/workflows/validate-context-tree.yml",
618
+ "?? AGENTS.md",
619
+ "?? CLAUDE.md",
620
+ "?? NODE.md"
621
+ ])) return;
622
+ }
623
+ throw new Error("Context Tree checkout must be clean.");
624
+ }
625
+ function parseRootNode(root) {
626
+ const path = join(root, "NODE.md");
627
+ const entry = lstatSync(path);
628
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error("Context Tree root NODE.md must be a regular file.");
629
+ return parseContextTreeRootNode(readUtf8File(path));
630
+ }
631
+ function verifyCheckout(treePath, mode) {
632
+ const root = exactCheckoutRoot(treePath);
633
+ requireCheckoutClean(root, mode);
634
+ const repository = checkoutRepository(root);
635
+ if (!verifyTree(root).ok) throw new Error("Context Tree checkout is invalid; run context-tree verify.");
636
+ return {
637
+ path: root,
638
+ repository
639
+ };
640
+ }
641
+ function sameProject(left, right) {
642
+ return left.kind === "git" && right.kind === "git" ? left.origin === right.origin : left.kind === "directory" && right.kind === "directory" && left.path === right.path;
643
+ }
644
+ function projectMatches(candidate, current) {
645
+ if (candidate.kind === "git" && current.kind === "git") return candidate.origin === current.origin;
646
+ if (candidate.kind === "directory" && current.kind === "directory") return isPathInside(candidate.path, current.path);
647
+ return false;
648
+ }
649
+ function liveStoredCheckout(link) {
650
+ try {
651
+ const path = exactCheckoutRoot(link.tree.path);
652
+ return {
653
+ path,
654
+ repository: checkoutRepository(path)
655
+ };
656
+ } catch {
657
+ return;
658
+ }
659
+ }
660
+ function linkWithMode(projectPath, treePath, mode) {
661
+ const project = identifyProject(projectPath);
662
+ const tree = verifyCheckout(treePath, mode);
663
+ const stored = loadLinks(false);
664
+ const existing = stored.links.filter((link) => sameProject(link.project, project));
665
+ if (existing.length > 1) throw new LinkError(CLI_ERROR_CODES.ambiguousLink, "Multiple Context Tree links match this project.");
666
+ const previous = existing[0];
667
+ if (previous !== void 0) {
668
+ if (previous.tree.repository.toLowerCase() !== tree.repository.toLowerCase()) throw new Error("A project cannot link to a different Context Tree repository.");
669
+ if (previous.tree.path !== tree.path) {
670
+ const live = liveStoredCheckout(previous);
671
+ if (live !== void 0 && live.repository.toLowerCase() === previous.tree.repository.toLowerCase()) throw new Error("The existing Context Tree checkout is still live; replacement is allowed only when it is stale.");
672
+ }
673
+ }
674
+ const link = {
675
+ project,
676
+ tree
677
+ };
678
+ saveLinks({
679
+ links: [...stored.links.filter((candidate) => !sameProject(candidate.project, project)), link],
680
+ schemaVersion: 1
681
+ });
682
+ return {
683
+ link,
684
+ schemaVersion: 1
685
+ };
686
+ }
687
+ function linkProject(projectPath, treePath) {
688
+ return linkWithMode(projectPath, treePath, "link");
689
+ }
690
+ function resolveLink(projectPath) {
691
+ const project = identifyProject(projectPath);
692
+ const matches = loadLinks(true).links.filter((link) => projectMatches(link.project, project));
693
+ if (matches.length === 0) throw new LinkError(CLI_ERROR_CODES.noLink, "No Context Tree link exists for this project.");
694
+ if (matches.length > 1) throw new LinkError(CLI_ERROR_CODES.ambiguousLink, "Multiple Context Tree links match this project.");
695
+ const link = matches[0];
696
+ if (link === void 0) throw new Error("Link lookup failed.");
697
+ try {
698
+ const root = exactCheckoutRoot(link.tree.path);
699
+ requireCheckoutClean(root, "link");
700
+ const repository = checkoutRepository(root);
701
+ if (repository.toLowerCase() !== link.tree.repository.toLowerCase()) throw new Error("The linked path now contains a different Context Tree repository.");
702
+ parseRootNode(root);
703
+ return {
704
+ link: {
705
+ project: link.project,
706
+ tree: {
707
+ path: root,
708
+ repository
709
+ }
710
+ },
711
+ schemaVersion: 1
712
+ };
713
+ } catch (error) {
714
+ const message = error instanceof Error && error.message === "The linked path now contains a different Context Tree repository." ? error.message : "The linked Context Tree checkout is no longer a valid clean candidate; run link when its stored path is stale.";
715
+ throw new LinkError(CLI_ERROR_CODES.staleLink, message);
716
+ }
717
+ }
718
+ //#endregion
719
+ //#region src/core/live.ts
720
+ function git(root, args) {
721
+ const result = spawnSync("git", [
722
+ "-C",
723
+ root,
724
+ ...args
725
+ ], {
726
+ encoding: "utf8",
727
+ stdio: [
728
+ "ignore",
729
+ "pipe",
730
+ "ignore"
731
+ ]
732
+ });
733
+ if (result.error !== void 0 || result.status !== 0) throw new Error("A Git operation failed while preparing the Context Tree.");
734
+ return typeof result.stdout === "string" ? result.stdout : "";
735
+ }
736
+ function requireGit(root, args, allowEmpty = false) {
737
+ const output = git(root, args).trim();
738
+ if (output.length === 0 && !allowEmpty) throw new Error("Unexpected empty Git output.");
739
+ return output;
740
+ }
741
+ function discoverDefaultBranch(root) {
742
+ const refs = git(root, [
743
+ "ls-remote",
744
+ "--symref",
745
+ "origin",
746
+ "HEAD"
747
+ ]).split("\n").map((line) => /^ref: refs\/heads\/([^\s\t]+)\tHEAD$/u.exec(line)?.[1]).filter((value) => value !== void 0 && value.length > 0);
748
+ if (refs.length !== 1) throw new Error("The Context Tree origin must report exactly one live default branch.");
749
+ return refs[0] ?? "";
750
+ }
751
+ function refreshProject(projectPath) {
752
+ const result = resolveLink(projectPath);
753
+ const root = result.link.tree.path;
754
+ const defaultBranch = discoverDefaultBranch(root);
755
+ if (requireGit(root, [
756
+ "symbolic-ref",
757
+ "--short",
758
+ "HEAD"
759
+ ]) !== defaultBranch) throw new Error(`The Context Tree checkout must be on the live default branch "${defaultBranch}".`);
760
+ const before = requireGit(root, ["rev-parse", "HEAD"]);
761
+ requireGit(root, [
762
+ "pull",
763
+ "--ff-only",
764
+ "origin",
765
+ defaultBranch
766
+ ]);
767
+ const after = requireGit(root, ["rev-parse", "HEAD"]);
768
+ return {
769
+ link: {
770
+ ...result.link,
771
+ tree: {
772
+ ...result.link.tree,
773
+ path: realpathSync(root)
774
+ }
775
+ },
776
+ defaultBranch,
777
+ refreshed: before !== after,
778
+ schemaVersion: 1,
779
+ sha: after
780
+ };
781
+ }
782
+ function createIsolatedWorktree(treePath, baseSha) {
783
+ const destination = mkdtempSync(join(tmpdir(), "context-tree-stage-"));
784
+ const taskBranch = `context-tree/write/${basename(destination)}`;
785
+ git(treePath, [
786
+ "worktree",
787
+ "add",
788
+ "-b",
789
+ taskBranch,
790
+ destination,
791
+ baseSha
792
+ ]);
793
+ return {
794
+ taskBranch,
795
+ worktreePath: realpathSync(destination)
796
+ };
797
+ }
798
+ function stageContextWrite(projectPath) {
799
+ const result = resolveLink(projectPath);
800
+ const treePath = result.link.tree.path;
801
+ const ownerRepository = result.link.tree.repository;
802
+ const defaultBranch = discoverDefaultBranch(treePath);
803
+ if (requireGit(treePath, [
804
+ "symbolic-ref",
805
+ "--short",
806
+ "HEAD"
807
+ ]) !== defaultBranch) throw new Error(`The Context Tree checkout must be on the live default branch "${defaultBranch}".`);
808
+ requireGit(treePath, [
809
+ "fetch",
810
+ "origin",
811
+ defaultBranch
812
+ ], true);
813
+ const baseSha = requireGit(treePath, ["rev-parse", `origin/${defaultBranch}`]);
814
+ const { taskBranch, worktreePath } = createIsolatedWorktree(treePath, baseSha);
815
+ return {
816
+ baseSha,
817
+ link: {
818
+ project: result.link.project,
819
+ tree: {
820
+ path: treePath,
821
+ repository: ownerRepository
822
+ }
823
+ },
824
+ defaultBranch,
825
+ schemaVersion: 1,
826
+ taskBranch,
827
+ worktreePath
828
+ };
829
+ }
830
+ const DIFF_STATUS = {
831
+ A: "added",
832
+ D: "deleted",
833
+ M: "modified",
834
+ R: "renamed"
835
+ };
836
+ function changedFiles(root, base) {
837
+ const output = git(root, [
838
+ "diff",
839
+ "--name-status",
840
+ base
841
+ ]);
842
+ const files = [];
843
+ for (const line of output.split("\n")) {
844
+ const match = /^([ADMR])\s+(.+)$/u.exec(line);
845
+ if (match === null) continue;
846
+ const status = DIFF_STATUS[match[1]];
847
+ if (status === void 0) continue;
848
+ files.push({
849
+ path: match[2] ?? "",
850
+ status
851
+ });
852
+ }
853
+ const porcelain = git(root, [
854
+ "status",
855
+ "--porcelain",
856
+ "--untracked-files=all"
857
+ ]);
858
+ for (const line of porcelain.split("\n")) {
859
+ const path = /^\?\?\s+(.+)$/u.exec(line)?.[1];
860
+ if (path !== void 0 && path.length > 0) files.push({
861
+ path,
862
+ status: "added"
863
+ });
864
+ }
865
+ return files;
866
+ }
867
+ function inspectContextTreeDiff(treePath, base) {
868
+ const root = realpathSync(treePath);
869
+ const reference = base ?? "HEAD";
870
+ return {
871
+ base: reference,
872
+ files: changedFiles(root, reference),
873
+ patch: git(root, ["diff", reference]),
874
+ schemaVersion: 1,
875
+ treePath: root
876
+ };
877
+ }
878
+ //#endregion
879
+ //#region src/core/internal/packaged-resource.ts
880
+ const PACKAGE_NAME = "@first-tree-ai/context-tree";
881
+ function isPackageRoot(path) {
882
+ const manifestPath = join(path, "package.json");
883
+ if (!existsSync(manifestPath)) return false;
884
+ try {
885
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
886
+ return isRecord(manifest) && manifest.name === PACKAGE_NAME;
887
+ } catch {
888
+ return false;
889
+ }
890
+ }
891
+ function resolvePackagedResource(...segments) {
892
+ let candidate = dirname(fileURLToPath(import.meta.url));
893
+ const filesystemRoot = parse(candidate).root;
894
+ while (true) {
895
+ if (isPackageRoot(candidate)) {
896
+ const resource = resolve(candidate, ...segments);
897
+ if (existsSync(resource)) return resource;
898
+ throw new Error(`Packaged resource is missing: ${segments.join("/")}`);
899
+ }
900
+ if (candidate === filesystemRoot) break;
901
+ candidate = dirname(candidate);
902
+ }
903
+ throw new Error(`Package root is missing while resolving: ${segments.join("/")}`);
904
+ }
905
+ function readPackageManifest() {
906
+ const parsed = JSON.parse(readFileSync(resolvePackagedResource("package.json"), "utf8"));
907
+ if (!isRecord(parsed)) throw new Error("Package metadata is invalid.");
908
+ return parsed;
909
+ }
910
+ function readPackageVersion() {
911
+ const manifest = readPackageManifest();
912
+ if (typeof manifest.version !== "string") throw new Error("Package version is missing or invalid.");
913
+ return manifest.version;
914
+ }
915
+ //#endregion
916
+ //#region src/core/policy.ts
917
+ function readContextTreePolicy() {
918
+ return {
919
+ content: readFileSync(resolvePackagedResource("policy", "context-tree-policy.md"), "utf8"),
920
+ schemaVersion: 1
921
+ };
922
+ }
923
+ //#endregion
924
+ //#region src/core/read.ts
925
+ function normalizeTreeTarget(value) {
926
+ if (!value || value === ".") return "";
927
+ const normalized = posix.normalize(toPosixPath(value).replace(/^\.\//u, ""));
928
+ if (normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/")) throw new Error(`Read target is outside the Context Tree: ${value}`);
929
+ return normalized.replace(/\/$/u, "");
930
+ }
931
+ function canonicalTarget(root, path) {
932
+ const requested = normalizeTreeTarget(path);
933
+ const semanticPath = requested === "NODE.md" ? "" : requested.endsWith("/NODE.md") ? dirname(requested) : requested;
934
+ if (classifyContextContent(semanticPath) === "repo-infra") throw new Error(`Read target is repository infrastructure: ${requested || "."}`);
935
+ const absolutePath = resolve(root, semanticPath);
936
+ if (!isPathInside(root, absolutePath)) throw new Error("Read target escapes the Context Tree root.");
937
+ const entry = lstatSync(absolutePath);
938
+ if (entry.isSymbolicLink() || !entry.isDirectory() && !entry.isFile()) throw new Error(`Read target must be a real file or directory: ${requested || "."}`);
939
+ if (realpathSync(absolutePath) !== absolutePath) throw new Error(`Read target must not traverse a symlink: ${requested || "."}`);
940
+ const relativePath = toPosixPath(relative(root, absolutePath));
941
+ if (entry.isFile() && !absolutePath.endsWith(".md")) throw new Error(`Read target must be a Markdown file or indexed directory: ${requested || "."}`);
942
+ return {
943
+ absolutePath,
944
+ kind: entry.isDirectory() ? "directory" : "file",
945
+ relativePath
946
+ };
947
+ }
948
+ function readNode(path, relativePath, kind) {
949
+ const documentPath = kind === "directory" ? join(path, "NODE.md") : path;
950
+ const entry = lstatSync(documentPath);
951
+ if (entry.isSymbolicLink() || !entry.isFile()) throw new Error(`Context Tree document must be a regular file: ${relativePath || "NODE.md"}`);
952
+ const document = readNodeDocument(documentPath);
953
+ if (document === null) throw new Error(`Context Tree document has invalid or missing metadata: ${relativePath || "."}`);
954
+ return {
955
+ body: document.body,
956
+ contentClass: classifyContextContent(relativePath),
957
+ frontmatter: document.frontmatter,
958
+ kind,
959
+ path: relativePath || "."
960
+ };
961
+ }
962
+ function childSummary(root, parentPath, name) {
963
+ const absolutePath = join(parentPath, name);
964
+ const relativePath = toPosixPath(relative(root, absolutePath));
965
+ const contentClass = classifyContextContent(relativePath);
966
+ if (contentClass === "repo-infra") return null;
967
+ const entry = lstatSync(absolutePath);
968
+ if (entry.isSymbolicLink()) return null;
969
+ const kind = entry.isDirectory() ? "directory" : entry.isFile() && name.endsWith(".md") && name !== "NODE.md" ? "file" : null;
970
+ if (kind === null) return null;
971
+ const document = readNodeDocument(kind === "directory" ? join(absolutePath, "NODE.md") : absolutePath);
972
+ if (document === null) throw new Error(`Context Tree child has invalid or missing metadata: ${relativePath}`);
973
+ return {
974
+ contentClass,
975
+ ...document.description === void 0 ? {} : { description: document.description },
976
+ kind,
977
+ path: relativePath,
978
+ title: document.title
979
+ };
980
+ }
981
+ function readTree(treePath, path) {
982
+ const root = resolveTreeRoot(treePath);
983
+ const target = canonicalTarget(root, path);
984
+ const node = readNode(target.absolutePath, target.relativePath, target.kind);
985
+ return {
986
+ children: target.kind === "file" ? [] : readdirSync(target.absolutePath).map((name) => childSummary(root, target.absolutePath, name)).filter((child) => child !== null).sort((left, right) => left.path.localeCompare(right.path)),
987
+ node,
988
+ root,
989
+ schemaVersion: 1,
990
+ target: target.relativePath || "."
991
+ };
992
+ }
993
+ //#endregion
994
+ //#region src/core/scaffold.ts
995
+ function template(name, values) {
996
+ let result = readFileSync(resolvePackagedResource("templates", name), "utf8");
997
+ for (const [key, value] of Object.entries(values)) result = result.replaceAll(`{{${key}}}`, value);
998
+ return result;
999
+ }
1000
+ function initializeGitRepository(root, repository) {
1001
+ const initialized = spawnSync("git", [
1002
+ "init",
1003
+ "--quiet",
1004
+ root
1005
+ ], { stdio: "ignore" });
1006
+ if (initialized.error !== void 0 || initialized.status !== 0) throw new Error("Failed to initialize Git repository.");
1007
+ const branch = spawnSync("git", [
1008
+ "-C",
1009
+ root,
1010
+ "symbolic-ref",
1011
+ "--short",
1012
+ "HEAD"
1013
+ ], {
1014
+ encoding: "utf8",
1015
+ stdio: [
1016
+ "ignore",
1017
+ "pipe",
1018
+ "ignore"
1019
+ ]
1020
+ });
1021
+ const name = branch.stdout.replace(/\r?\n$/u, "");
1022
+ if (branch.error !== void 0 || branch.status !== 0 || name.length === 0) throw new Error("Failed to resolve the initial Git branch during repository initialization.");
1023
+ const remote = spawnSync("git", [
1024
+ "-C",
1025
+ root,
1026
+ "remote",
1027
+ "add",
1028
+ "origin",
1029
+ canonicalGitHubRepositoryUrl(repository)
1030
+ ], { stdio: "ignore" });
1031
+ if (remote.error !== void 0 || remote.status !== 0) throw new Error("Failed to configure the credential-free Context Tree origin.");
1032
+ return name;
1033
+ }
1034
+ function scaffoldTree(options) {
1035
+ const title = parseGitHubRepositoryIdentity(options.repository);
1036
+ const root = resolve(options.path);
1037
+ const destination = lstatSync(root, { throwIfNoEntry: false });
1038
+ if (destination !== void 0) {
1039
+ if (destination.isSymbolicLink() || !destination.isDirectory()) throw new Error(`Refusing to scaffold into a symlink or non-directory destination: ${root}`);
1040
+ if (readdirSync(root).length > 0) throw new Error(`Refusing to scaffold into a non-empty directory: ${root}`);
1041
+ }
1042
+ const initialBranch = initializeGitRepository(root, options.repository);
1043
+ const values = {
1044
+ branchJson: JSON.stringify(initialBranch),
1045
+ packageVersion: readPackageVersion(),
1046
+ title,
1047
+ titleJson: JSON.stringify(title)
1048
+ };
1049
+ const regularFiles = [
1050
+ ["NODE.md", "root-node.md"],
1051
+ ["AGENTS.md", "AGENTS.md"],
1052
+ [".github/workflows/validate-context-tree.yml", "validate-context-tree.yml"]
1053
+ ];
1054
+ const files = [
1055
+ "NODE.md",
1056
+ "AGENTS.md",
1057
+ "CLAUDE.md",
1058
+ ".github/workflows/validate-context-tree.yml"
1059
+ ];
1060
+ for (const [relativePath, source] of regularFiles.slice(0, 2)) {
1061
+ const path = join(root, relativePath);
1062
+ mkdirSync(dirname(path), { recursive: true });
1063
+ writeFileSync(path, template(source, values), {
1064
+ encoding: "utf8",
1065
+ flag: "wx",
1066
+ mode: 420
1067
+ });
1068
+ }
1069
+ symlinkSync("AGENTS.md", join(root, "CLAUDE.md"), "file");
1070
+ for (const [relativePath, source] of regularFiles.slice(2)) {
1071
+ const path = join(root, relativePath);
1072
+ mkdirSync(dirname(path), { recursive: true });
1073
+ writeFileSync(path, template(source, values), {
1074
+ encoding: "utf8",
1075
+ flag: "wx",
1076
+ mode: 420
1077
+ });
1078
+ }
1079
+ return {
1080
+ files,
1081
+ root,
1082
+ schemaVersion: 1,
1083
+ verification: verifyTree(root)
1084
+ };
1085
+ }
1086
+ //#endregion
1087
+ export { inspectContextTreeDiff, linkProject, readContextTreePolicy, readTree, refreshProject, resolveLink, scaffoldTree, stageContextWrite, verifyTree };