barry-cache 0.3.2 → 0.4.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/README.md +3 -1
- package/dist/cli.js +175 -62
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ It creates source-backed context files for coding agents, validates them, and gi
|
|
|
12
12
|
|
|
13
13
|
Barry Cache exists because coding agents need durable project context that is shared, reviewable, and smaller than the whole repository. Private assistant memory, ad hoc chat history, and vendor-specific instruction files drift apart; Barry keeps the source of truth in the repo and lets every agent load the same facts.
|
|
14
14
|
|
|
15
|
-
The structure is intentionally layered: canonical context lives in `docs/context/`, operational continuity lives in `.context-state/`, and generated retrieval data lives in `.context-cache/`. Canonical facts stay source-backed and validated, while `route`, `search`, `load`, and `resume` project only the relevant feature pack into an agent session.
|
|
15
|
+
The structure is intentionally layered: canonical context lives in `docs/context/`, operational continuity lives in `.context-state/`, and generated retrieval data lives in `.context-cache/`. Canonical facts stay source-backed and validated, while `route`, `search`, `load`, and `resume` project only the relevant feature pack into an agent session. Repeated retrieval commands reuse `.context-cache/context-index.json` when the source context file manifest is unchanged.
|
|
16
16
|
|
|
17
17
|
That gives Barry three core advantages: it is agent-agnostic, because adapters point to one canonical context; auditable, because facts carry stable IDs and source references; and context-efficient, because agents start from a small routed slice instead of rereading the whole codebase.
|
|
18
18
|
|
|
@@ -297,6 +297,8 @@ bun run barry -- load --route editor-media-runtime
|
|
|
297
297
|
|
|
298
298
|
This keeps the agent from reading every context file in the repo.
|
|
299
299
|
|
|
300
|
+
Barry also keeps a disposable parsed context index in `.context-cache/context-index.json`. It is keyed by the source context files and directories, so repeated `resume`, `load`, `route`, `search`, and `review --json` commands reuse parsed context until a context source file changes.
|
|
301
|
+
|
|
300
302
|
If an agent ignores the repo instructions, prompt it explicitly:
|
|
301
303
|
|
|
302
304
|
```text
|
package/dist/cli.js
CHANGED
|
@@ -263,7 +263,11 @@ function unquote(input) {
|
|
|
263
263
|
|
|
264
264
|
// src/core/context.ts
|
|
265
265
|
import { appendFile, mkdir as mkdir2 } from "node:fs/promises";
|
|
266
|
-
import { basename as basename2, join as
|
|
266
|
+
import { basename as basename2, join as join5 } from "node:path";
|
|
267
|
+
|
|
268
|
+
// src/core/context-cache.ts
|
|
269
|
+
import { stat as stat2 } from "node:fs/promises";
|
|
270
|
+
import { join as join4 } from "node:path";
|
|
267
271
|
|
|
268
272
|
// src/core/validate.ts
|
|
269
273
|
import { join as join3 } from "node:path";
|
|
@@ -338,17 +342,155 @@ function validateFact(value) {
|
|
|
338
342
|
return null;
|
|
339
343
|
}
|
|
340
344
|
|
|
345
|
+
// src/core/context-cache.ts
|
|
346
|
+
var cacheVersion = 1;
|
|
347
|
+
var contextCachePath = ".context-cache/context-index.json";
|
|
348
|
+
async function readContextSnapshot(repo) {
|
|
349
|
+
const cached = await readFreshContextCache(repo);
|
|
350
|
+
if (cached)
|
|
351
|
+
return hydrateSnapshot(repo, cached);
|
|
352
|
+
const snapshot = await readContextSnapshotFromDisk(repo);
|
|
353
|
+
await writeContextCache(repo, snapshot);
|
|
354
|
+
return snapshot;
|
|
355
|
+
}
|
|
356
|
+
async function readFreshContextCache(repo) {
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(await readTextIfExists(repoPath(repo, contextCachePath)));
|
|
359
|
+
if (!isContextCacheFile(parsed))
|
|
360
|
+
return null;
|
|
361
|
+
return await manifestIsFresh(repo, parsed.manifest) ? parsed : null;
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function writeContextCache(repo, snapshot) {
|
|
367
|
+
const cache = {
|
|
368
|
+
version: cacheVersion,
|
|
369
|
+
generated_at: new Date().toISOString(),
|
|
370
|
+
manifest: await buildManifest(repo, snapshot),
|
|
371
|
+
features: snapshot.features.map((feature) => ({
|
|
372
|
+
...feature,
|
|
373
|
+
dir: rel(repo, feature.dir)
|
|
374
|
+
})),
|
|
375
|
+
adrs: snapshot.adrs
|
|
376
|
+
};
|
|
377
|
+
try {
|
|
378
|
+
await writeText(repoPath(repo, contextCachePath), `${JSON.stringify(cache, null, 2)}
|
|
379
|
+
`);
|
|
380
|
+
} catch {}
|
|
381
|
+
}
|
|
382
|
+
async function readContextSnapshotFromDisk(repo) {
|
|
383
|
+
return {
|
|
384
|
+
features: await readFeaturePacksFromDisk(repo),
|
|
385
|
+
adrs: (await readAdrCatalog(repo)).adrs
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function readFeaturePacksFromDisk(repo) {
|
|
389
|
+
const root = repoPath(repo, "docs/context/features");
|
|
390
|
+
const slugs = await listDirs(root);
|
|
391
|
+
const features = [];
|
|
392
|
+
for (const slug2 of slugs) {
|
|
393
|
+
const dir = join4(root, slug2);
|
|
394
|
+
features.push({
|
|
395
|
+
slug: slug2,
|
|
396
|
+
dir,
|
|
397
|
+
readme: await readTextIfExists(join4(dir, "README.md")),
|
|
398
|
+
idmap: await readTextIfExists(join4(dir, "IDMAP.md")),
|
|
399
|
+
graph: await readTextIfExists(join4(dir, "KG.adj")),
|
|
400
|
+
facts: await readFacts(join4(dir, "FACTS.jsonl"))
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
return features;
|
|
404
|
+
}
|
|
405
|
+
async function readFacts(path) {
|
|
406
|
+
const rows = (await readTextIfExists(path)).split(/\r?\n/);
|
|
407
|
+
const facts = [];
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
if (row.trim().length === 0)
|
|
410
|
+
continue;
|
|
411
|
+
const parsed = JSON.parse(row);
|
|
412
|
+
if (validateFact(parsed) === null)
|
|
413
|
+
facts.push(parsed);
|
|
414
|
+
}
|
|
415
|
+
return facts;
|
|
416
|
+
}
|
|
417
|
+
async function buildManifest(repo, snapshot) {
|
|
418
|
+
const paths = new Map;
|
|
419
|
+
paths.set("docs/context/features", "dir");
|
|
420
|
+
paths.set("docs/context/adrs", "dir");
|
|
421
|
+
for (const feature of snapshot.features) {
|
|
422
|
+
const dir = rel(repo, feature.dir);
|
|
423
|
+
paths.set(dir, "dir");
|
|
424
|
+
paths.set(`${dir}/README.md`, "file");
|
|
425
|
+
paths.set(`${dir}/IDMAP.md`, "file");
|
|
426
|
+
paths.set(`${dir}/KG.adj`, "file");
|
|
427
|
+
paths.set(`${dir}/FACTS.jsonl`, "file");
|
|
428
|
+
}
|
|
429
|
+
for (const adr of snapshot.adrs) {
|
|
430
|
+
paths.set(adr.path, "file");
|
|
431
|
+
}
|
|
432
|
+
const entries = [...paths.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
433
|
+
return await Promise.all(entries.map(([path, kind]) => statManifestEntry(repo, path, kind)));
|
|
434
|
+
}
|
|
435
|
+
async function manifestIsFresh(repo, manifest) {
|
|
436
|
+
for (const entry of manifest) {
|
|
437
|
+
const current = await statManifestEntry(repo, entry.path, entry.kind);
|
|
438
|
+
if (!sameManifestEntry(entry, current))
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
async function statManifestEntry(repo, path, kind) {
|
|
444
|
+
try {
|
|
445
|
+
const value = await stat2(repoPath(repo, path));
|
|
446
|
+
const matchesKind = kind === "dir" ? value.isDirectory() : value.isFile();
|
|
447
|
+
if (!matchesKind)
|
|
448
|
+
return { path, kind, exists: false };
|
|
449
|
+
return {
|
|
450
|
+
path,
|
|
451
|
+
kind,
|
|
452
|
+
exists: true,
|
|
453
|
+
mtimeMs: value.mtimeMs,
|
|
454
|
+
size: value.size
|
|
455
|
+
};
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error.code === "ENOENT")
|
|
458
|
+
return { path, kind, exists: false };
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function sameManifestEntry(left, right) {
|
|
463
|
+
if (left.path !== right.path || left.kind !== right.kind || left.exists !== right.exists)
|
|
464
|
+
return false;
|
|
465
|
+
if (!left.exists || !right.exists)
|
|
466
|
+
return true;
|
|
467
|
+
return left.mtimeMs === right.mtimeMs && left.size === right.size;
|
|
468
|
+
}
|
|
469
|
+
function hydrateSnapshot(repo, cache) {
|
|
470
|
+
return {
|
|
471
|
+
features: cache.features.map((feature) => ({
|
|
472
|
+
...feature,
|
|
473
|
+
dir: repoPath(repo, feature.dir)
|
|
474
|
+
})),
|
|
475
|
+
adrs: cache.adrs
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function isContextCacheFile(value) {
|
|
479
|
+
if (typeof value !== "object" || value === null)
|
|
480
|
+
return false;
|
|
481
|
+
const candidate = value;
|
|
482
|
+
return candidate.version === cacheVersion && Array.isArray(candidate.manifest) && Array.isArray(candidate.features) && Array.isArray(candidate.adrs);
|
|
483
|
+
}
|
|
484
|
+
|
|
341
485
|
// src/core/context.ts
|
|
342
486
|
async function routeTask({ repo, task }) {
|
|
343
|
-
const features = await
|
|
344
|
-
const adrs = await listAdrs({ repo });
|
|
487
|
+
const { features, adrs } = await readContextSnapshot(repo);
|
|
345
488
|
const taskTokens = tokens(task);
|
|
346
489
|
const routes = features.map((feature) => scoreFeature(feature, taskTokens, adrs)).filter((route) => route.score > 0).sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
|
|
347
490
|
return { task, routes };
|
|
348
491
|
}
|
|
349
492
|
async function searchContext({ repo, query }) {
|
|
350
|
-
const features = await
|
|
351
|
-
const adrs = await listAdrs({ repo });
|
|
493
|
+
const { features, adrs } = await readContextSnapshot(repo);
|
|
352
494
|
const queryTokens = tokens(query);
|
|
353
495
|
const results = [];
|
|
354
496
|
for (const feature of features) {
|
|
@@ -374,7 +516,7 @@ async function searchContext({ repo, query }) {
|
|
|
374
516
|
route: feature.slug,
|
|
375
517
|
score,
|
|
376
518
|
text: factText,
|
|
377
|
-
source: `${rel(repo,
|
|
519
|
+
source: `${rel(repo, join5(feature.dir, "FACTS.jsonl"))}#${fact.id}`
|
|
378
520
|
});
|
|
379
521
|
}
|
|
380
522
|
}
|
|
@@ -396,8 +538,7 @@ async function searchContext({ repo, query }) {
|
|
|
396
538
|
return { query, results };
|
|
397
539
|
}
|
|
398
540
|
async function loadContext({ repo, route }) {
|
|
399
|
-
const features = await
|
|
400
|
-
const adrs = await listAdrs({ repo });
|
|
541
|
+
const { features, adrs } = await readContextSnapshot(repo);
|
|
401
542
|
const feature = features.find((item) => item.slug === route) ?? null;
|
|
402
543
|
if (!feature)
|
|
403
544
|
return { feature: null, facts: [], sources: [], adrs: [] };
|
|
@@ -405,10 +546,10 @@ async function loadContext({ repo, route }) {
|
|
|
405
546
|
feature,
|
|
406
547
|
facts: feature.facts,
|
|
407
548
|
sources: [
|
|
408
|
-
rel(repo,
|
|
409
|
-
rel(repo,
|
|
410
|
-
rel(repo,
|
|
411
|
-
rel(repo,
|
|
549
|
+
rel(repo, join5(feature.dir, "README.md")),
|
|
550
|
+
rel(repo, join5(feature.dir, "IDMAP.md")),
|
|
551
|
+
rel(repo, join5(feature.dir, "KG.adj")),
|
|
552
|
+
rel(repo, join5(feature.dir, "FACTS.jsonl"))
|
|
412
553
|
],
|
|
413
554
|
adrs: linkedAdrsForSources(feature.facts.flatMap((fact) => fact.src), adrs)
|
|
414
555
|
};
|
|
@@ -432,7 +573,7 @@ async function resumeProject({ repo, task }) {
|
|
|
432
573
|
async function finalizeProject(options) {
|
|
433
574
|
const dir = repoPath(options.repo, ".context-state/handoffs");
|
|
434
575
|
await mkdir2(dir, { recursive: true });
|
|
435
|
-
const path =
|
|
576
|
+
const path = join5(dir, "handoffs.jsonl");
|
|
436
577
|
const record = {
|
|
437
578
|
id: `handoff-${new Date().toISOString()}`,
|
|
438
579
|
updated_at: new Date().toISOString(),
|
|
@@ -445,35 +586,6 @@ async function finalizeProject(options) {
|
|
|
445
586
|
`);
|
|
446
587
|
return { saved: true, path: rel(options.repo, path), summary: options.summary };
|
|
447
588
|
}
|
|
448
|
-
async function readFeaturePacks(repo) {
|
|
449
|
-
const root = repoPath(repo, "docs/context/features");
|
|
450
|
-
const slugs = await listDirs(root);
|
|
451
|
-
const features = [];
|
|
452
|
-
for (const slug2 of slugs) {
|
|
453
|
-
const dir = join4(root, slug2);
|
|
454
|
-
features.push({
|
|
455
|
-
slug: slug2,
|
|
456
|
-
dir,
|
|
457
|
-
readme: await readTextIfExists(join4(dir, "README.md")),
|
|
458
|
-
idmap: await readTextIfExists(join4(dir, "IDMAP.md")),
|
|
459
|
-
graph: await readTextIfExists(join4(dir, "KG.adj")),
|
|
460
|
-
facts: await readFacts(join4(dir, "FACTS.jsonl"))
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
return features;
|
|
464
|
-
}
|
|
465
|
-
async function readFacts(path) {
|
|
466
|
-
const rows = (await readTextIfExists(path)).split(/\r?\n/);
|
|
467
|
-
const facts = [];
|
|
468
|
-
for (const row of rows) {
|
|
469
|
-
if (row.trim().length === 0)
|
|
470
|
-
continue;
|
|
471
|
-
const parsed = JSON.parse(row);
|
|
472
|
-
if (validateFact(parsed) === null)
|
|
473
|
-
facts.push(parsed);
|
|
474
|
-
}
|
|
475
|
-
return facts;
|
|
476
|
-
}
|
|
477
589
|
function scoreFeature(feature, taskTokens, adrs) {
|
|
478
590
|
const linkedAdrs = linkedAdrsForSources(feature.facts.flatMap((fact) => fact.src), adrs);
|
|
479
591
|
const text = [
|
|
@@ -516,7 +628,7 @@ function firstLine(value) {
|
|
|
516
628
|
}
|
|
517
629
|
|
|
518
630
|
// src/core/import-pulpcut.ts
|
|
519
|
-
import { join as
|
|
631
|
+
import { join as join6 } from "node:path";
|
|
520
632
|
async function importPulpcutKb(options) {
|
|
521
633
|
const dryRun = options.dryRun ?? false;
|
|
522
634
|
const result = {
|
|
@@ -531,19 +643,19 @@ async function importPulpcutKb(options) {
|
|
|
531
643
|
warnings: []
|
|
532
644
|
};
|
|
533
645
|
const sourceDocs = repoPath(options.from, "docs");
|
|
534
|
-
const kbIndex = await readTextIfExists(
|
|
646
|
+
const kbIndex = await readTextIfExists(join6(sourceDocs, "KB_INDEX.md"));
|
|
535
647
|
if (kbIndex.trim().length === 0) {
|
|
536
|
-
throw new Error(`PulpCut KB index not found at ${
|
|
648
|
+
throw new Error(`PulpCut KB index not found at ${join6(sourceDocs, "KB_INDEX.md")}`);
|
|
537
649
|
}
|
|
538
650
|
const routes = parseKbIndex(kbIndex);
|
|
539
651
|
const slugs = await listDirs(sourceDocs);
|
|
540
652
|
for (const slug2 of slugs) {
|
|
541
|
-
const sourceDir =
|
|
542
|
-
const rawFacts = await readTextIfExists(
|
|
653
|
+
const sourceDir = join6(sourceDocs, slug2);
|
|
654
|
+
const rawFacts = await readTextIfExists(join6(sourceDir, "FACTS.jsonl"));
|
|
543
655
|
if (rawFacts.trim().length === 0)
|
|
544
656
|
continue;
|
|
545
|
-
const idmap = await readTextIfExists(
|
|
546
|
-
const graph = await readTextIfExists(
|
|
657
|
+
const idmap = await readTextIfExists(join6(sourceDir, "IDMAP.md"));
|
|
658
|
+
const graph = await readTextIfExists(join6(sourceDir, "KG.adj"));
|
|
547
659
|
if (idmap.trim().length === 0)
|
|
548
660
|
result.warnings.push(`${slug2}: missing IDMAP.md`);
|
|
549
661
|
if (graph.trim().length === 0)
|
|
@@ -728,7 +840,7 @@ function stringOrStringArray(value) {
|
|
|
728
840
|
|
|
729
841
|
// src/core/init.ts
|
|
730
842
|
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
731
|
-
import { join as
|
|
843
|
+
import { join as join7 } from "node:path";
|
|
732
844
|
|
|
733
845
|
// src/core/templates.ts
|
|
734
846
|
var managedStart = "<!-- barry-cache:start -->";
|
|
@@ -761,6 +873,8 @@ Barry Cache remembers this repo through source-backed context files.
|
|
|
761
873
|
|
|
762
874
|
${commandNote}
|
|
763
875
|
|
|
876
|
+
Repeated retrieval commands use the disposable parsed index in \`.context-cache/context-index.json\` when source context files have not changed.
|
|
877
|
+
|
|
764
878
|
Start task context with:
|
|
765
879
|
|
|
766
880
|
\`\`\`bash
|
|
@@ -855,7 +969,7 @@ Barry Cache keeps repo context source-backed, validated, and easy for agents to
|
|
|
855
969
|
|
|
856
970
|
This directory is the canonical project memory for Barry Cache. It keeps durable implementation context in Git so humans and agents can review the same source-backed facts instead of relying on private assistant memory or stale chat history.
|
|
857
971
|
|
|
858
|
-
Barry separates three concerns: \`docs/context/\` is reviewed truth, \`.context-state/\` is operational session continuity, and \`.context-cache/\` is disposable retrieval data. Use this structure to explain existing behavior, route tasks, validate facts, and resume agent work without loading the whole repo.
|
|
972
|
+
Barry separates three concerns: \`docs/context/\` is reviewed truth, \`.context-state/\` is operational session continuity, and \`.context-cache/\` is disposable retrieval data. Barry stores a parsed context index in \`.context-cache/context-index.json\` and reuses it while the source context manifest is unchanged. Use this structure to explain existing behavior, route tasks, validate facts, and resume agent work without loading the whole repo.
|
|
859
973
|
`;
|
|
860
974
|
var adrReadmeMd = `# Architecture Decision Records
|
|
861
975
|
|
|
@@ -876,7 +990,7 @@ Barry Cache separates canonical context, operational state, and generated caches
|
|
|
876
990
|
|
|
877
991
|
- Canonical context lives in \`docs/context/\`.
|
|
878
992
|
- Operational continuity lives in \`.context-state/\`.
|
|
879
|
-
- Generated retrieval data lives in \`.context-cache
|
|
993
|
+
- Generated retrieval data lives in \`.context-cache/\`, including the disposable parsed context index.
|
|
880
994
|
`;
|
|
881
995
|
var factSchema = {
|
|
882
996
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
@@ -975,11 +1089,11 @@ async function initProject(options) {
|
|
|
975
1089
|
await patchAgentInstructions(repo, dryRun, result, options.agents, commandPrefix, packageManager);
|
|
976
1090
|
await patchGitignore(repo, dryRun, result);
|
|
977
1091
|
if (!dryRun) {
|
|
978
|
-
await mkdir3(
|
|
979
|
-
await mkdir3(
|
|
980
|
-
await mkdir3(
|
|
981
|
-
await mkdir3(
|
|
982
|
-
await mkdir3(
|
|
1092
|
+
await mkdir3(join7(repo, ".context-state/work/threads"), { recursive: true });
|
|
1093
|
+
await mkdir3(join7(repo, ".context-state/handoffs"), { recursive: true });
|
|
1094
|
+
await mkdir3(join7(repo, ".context-state/failures"), { recursive: true });
|
|
1095
|
+
await mkdir3(join7(repo, ".context-state/strategies"), { recursive: true });
|
|
1096
|
+
await mkdir3(join7(repo, ".context-cache"), { recursive: true });
|
|
983
1097
|
}
|
|
984
1098
|
result.changed = result.written.length > 0 || result.updated.length > 0;
|
|
985
1099
|
return result;
|
|
@@ -1102,7 +1216,7 @@ function llmsTxt() {
|
|
|
1102
1216
|
}
|
|
1103
1217
|
|
|
1104
1218
|
// src/core/review-model.ts
|
|
1105
|
-
import { join as
|
|
1219
|
+
import { join as join8 } from "node:path";
|
|
1106
1220
|
|
|
1107
1221
|
// src/core/review-tree.ts
|
|
1108
1222
|
function buildReviewTree(facts) {
|
|
@@ -1254,8 +1368,7 @@ async function buildReviewModel({ repo }) {
|
|
|
1254
1368
|
const warnings = [];
|
|
1255
1369
|
const facts = [];
|
|
1256
1370
|
const timeline = [];
|
|
1257
|
-
const features = await
|
|
1258
|
-
const adrs = await listAdrs({ repo });
|
|
1371
|
+
const { features, adrs } = await readContextSnapshot(repo);
|
|
1259
1372
|
for (const adr of adrs)
|
|
1260
1373
|
addAdr(graph, adr);
|
|
1261
1374
|
for (const feature of features) {
|
|
@@ -1265,7 +1378,7 @@ async function buildReviewModel({ repo }) {
|
|
|
1265
1378
|
for (const fact of feature.facts) {
|
|
1266
1379
|
facts.push({
|
|
1267
1380
|
route: feature.slug,
|
|
1268
|
-
source: `${rel(repo,
|
|
1381
|
+
source: `${rel(repo, join8(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
|
|
1269
1382
|
fact
|
|
1270
1383
|
});
|
|
1271
1384
|
addFact(graph, repo, feature, fact, sourceMap, adrs);
|
|
@@ -1347,7 +1460,7 @@ function addFact(graph, repo, feature, fact, sourceMap, adrs) {
|
|
|
1347
1460
|
kind: "fact",
|
|
1348
1461
|
label: fact.id,
|
|
1349
1462
|
subtitle: `${fact.subject} ${fact.predicate} ${fact.object}`,
|
|
1350
|
-
source: `${rel(repo,
|
|
1463
|
+
source: `${rel(repo, join8(feature.dir, "FACTS.jsonl"))}#${fact.id}`,
|
|
1351
1464
|
meta: {
|
|
1352
1465
|
route: feature.slug,
|
|
1353
1466
|
status: fact.status,
|