truthmark 1.2.2 → 1.3.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.de.md +155 -55
- package/README.es.md +149 -49
- package/README.md +125 -43
- package/README.ru.md +148 -48
- package/README.zh.md +146 -46
- package/dist/main.js +2974 -863
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -43,7 +43,7 @@ var renderJson = (result) => {
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
// src/config/command.ts
|
|
46
|
-
import
|
|
46
|
+
import fs3 from "fs/promises";
|
|
47
47
|
|
|
48
48
|
// src/fs/paths.ts
|
|
49
49
|
import path from "path";
|
|
@@ -51,19 +51,37 @@ import fs from "fs/promises";
|
|
|
51
51
|
var isPathInsideRoot = (rootDir, targetPath) => {
|
|
52
52
|
return targetPath === rootDir || targetPath.startsWith(`${rootDir}${path.sep}`);
|
|
53
53
|
};
|
|
54
|
+
var isNodeErrorWithCode = (error, code) => {
|
|
55
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
56
|
+
};
|
|
57
|
+
var joinMissingSegments = (resolvedPath, missingSegments) => {
|
|
58
|
+
return missingSegments.reduce((currentResolvedPath, segment) => {
|
|
59
|
+
return path.join(currentResolvedPath, segment);
|
|
60
|
+
}, resolvedPath);
|
|
61
|
+
};
|
|
54
62
|
var resolveThroughExistingAncestor = async (targetPath) => {
|
|
55
63
|
let currentPath = path.resolve(targetPath);
|
|
56
64
|
const missingSegments = [];
|
|
57
65
|
while (true) {
|
|
58
66
|
try {
|
|
59
67
|
const resolvedExistingPath = await fs.realpath(currentPath);
|
|
60
|
-
return
|
|
61
|
-
return path.join(resolvedPath, segment);
|
|
62
|
-
}, resolvedExistingPath);
|
|
68
|
+
return joinMissingSegments(resolvedExistingPath, missingSegments);
|
|
63
69
|
} catch (error) {
|
|
64
|
-
if (!(error
|
|
70
|
+
if (!isNodeErrorWithCode(error, "ENOENT")) {
|
|
65
71
|
throw error;
|
|
66
72
|
}
|
|
73
|
+
try {
|
|
74
|
+
const currentStat = await fs.lstat(currentPath);
|
|
75
|
+
if (currentStat.isSymbolicLink()) {
|
|
76
|
+
const linkTarget = await fs.readlink(currentPath);
|
|
77
|
+
const resolvedLinkTarget = path.resolve(path.dirname(currentPath), linkTarget);
|
|
78
|
+
return joinMissingSegments(resolvedLinkTarget, missingSegments);
|
|
79
|
+
}
|
|
80
|
+
} catch (lstatError) {
|
|
81
|
+
if (!isNodeErrorWithCode(lstatError, "ENOENT")) {
|
|
82
|
+
throw lstatError;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
67
85
|
const parentPath = path.dirname(currentPath);
|
|
68
86
|
if (parentPath === currentPath) {
|
|
69
87
|
return path.resolve(targetPath);
|
|
@@ -227,6 +245,7 @@ var resolveWorktreePath = (repository, relativePath) => {
|
|
|
227
245
|
};
|
|
228
246
|
|
|
229
247
|
// src/templates/init-files.ts
|
|
248
|
+
import path3 from "path";
|
|
230
249
|
import { stringify } from "yaml";
|
|
231
250
|
|
|
232
251
|
// src/config/schema.ts
|
|
@@ -247,7 +266,7 @@ var DEFAULT_PLATFORMS = [
|
|
|
247
266
|
var truthmarkConfigSchema = {
|
|
248
267
|
type: "object",
|
|
249
268
|
additionalProperties: false,
|
|
250
|
-
required: ["version", "authority"
|
|
269
|
+
required: ["version", "authority"],
|
|
251
270
|
properties: {
|
|
252
271
|
version: {
|
|
253
272
|
type: "integer",
|
|
@@ -343,16 +362,6 @@ var truthmarkConfigSchema = {
|
|
|
343
362
|
items: {
|
|
344
363
|
type: "string"
|
|
345
364
|
}
|
|
346
|
-
},
|
|
347
|
-
realization: {
|
|
348
|
-
type: "object",
|
|
349
|
-
additionalProperties: false,
|
|
350
|
-
required: ["enabled"],
|
|
351
|
-
properties: {
|
|
352
|
-
enabled: {
|
|
353
|
-
type: "boolean"
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
365
|
}
|
|
357
366
|
}
|
|
358
367
|
};
|
|
@@ -364,7 +373,7 @@ var DEFAULT_DOCS_HIERARCHY = {
|
|
|
364
373
|
ai: "docs/ai",
|
|
365
374
|
standards: "docs/standards",
|
|
366
375
|
architecture: "docs/architecture",
|
|
367
|
-
|
|
376
|
+
truth: "docs/truth"
|
|
368
377
|
},
|
|
369
378
|
routing: {
|
|
370
379
|
root_index: "docs/truthmark/areas.md",
|
|
@@ -374,13 +383,12 @@ var DEFAULT_DOCS_HIERARCHY = {
|
|
|
374
383
|
}
|
|
375
384
|
};
|
|
376
385
|
var DEFAULT_AUTHORITY = [
|
|
377
|
-
"TRUTHMARK.md",
|
|
378
386
|
DEFAULT_DOCS_HIERARCHY.routing.root_index,
|
|
379
387
|
`${DEFAULT_DOCS_HIERARCHY.routing.area_files_root}/**/*.md`,
|
|
380
388
|
`${DEFAULT_DOCS_HIERARCHY.roots.ai}/**/*.md`,
|
|
381
389
|
`${DEFAULT_DOCS_HIERARCHY.roots.standards}/**/*.md`,
|
|
382
390
|
`${DEFAULT_DOCS_HIERARCHY.roots.architecture}/**/*.md`,
|
|
383
|
-
`${DEFAULT_DOCS_HIERARCHY.roots.
|
|
391
|
+
`${DEFAULT_DOCS_HIERARCHY.roots.truth}/**/*.md`
|
|
384
392
|
];
|
|
385
393
|
var DEFAULT_INSTRUCTION_TARGETS = ["AGENTS.md"];
|
|
386
394
|
var createDefaultRawConfig = () => ({
|
|
@@ -397,10 +405,7 @@ var createDefaultRawConfig = () => ({
|
|
|
397
405
|
required: [],
|
|
398
406
|
recommended: ["status", "doc_type", "last_reviewed", "source_of_truth"]
|
|
399
407
|
},
|
|
400
|
-
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
401
|
-
realization: {
|
|
402
|
-
enabled: true
|
|
403
|
-
}
|
|
408
|
+
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
404
409
|
});
|
|
405
410
|
var createDefaultConfig = () => ({
|
|
406
411
|
version: 1,
|
|
@@ -421,43 +426,302 @@ var createDefaultConfig = () => ({
|
|
|
421
426
|
required: [],
|
|
422
427
|
recommended: ["status", "doc_type", "last_reviewed", "source_of_truth"]
|
|
423
428
|
},
|
|
424
|
-
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
425
|
-
realization: {
|
|
426
|
-
enabled: true
|
|
427
|
-
}
|
|
429
|
+
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
428
430
|
});
|
|
429
431
|
|
|
430
|
-
// src/
|
|
431
|
-
import
|
|
432
|
-
var
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
432
|
+
// src/routing/areas.ts
|
|
433
|
+
import { parse } from "yaml";
|
|
434
|
+
var TRUTH_DOCUMENT_KINDS = [
|
|
435
|
+
"behavior",
|
|
436
|
+
"contract",
|
|
437
|
+
"architecture",
|
|
438
|
+
"workflow",
|
|
439
|
+
"operations",
|
|
440
|
+
"test-behavior"
|
|
441
|
+
];
|
|
442
|
+
var slugify = (value) => {
|
|
443
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
444
|
+
};
|
|
445
|
+
var createAreaDiagnostic = (message, area, severity = "error") => {
|
|
446
|
+
return {
|
|
447
|
+
category: "area-index",
|
|
448
|
+
severity,
|
|
449
|
+
message,
|
|
450
|
+
area
|
|
451
|
+
};
|
|
452
|
+
};
|
|
453
|
+
var parseListSection = (sectionLines) => {
|
|
454
|
+
return sectionLines.map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim()).filter((line) => line.length > 0);
|
|
455
|
+
};
|
|
456
|
+
var isTruthDocumentKind = (value) => {
|
|
457
|
+
return typeof value === "string" && TRUTH_DOCUMENT_KINDS.includes(value);
|
|
458
|
+
};
|
|
459
|
+
var inferTruthDocumentKindFromPath = (documentPath, options = {}) => {
|
|
460
|
+
const normalizedPath = documentPath.replaceAll("\\", "/");
|
|
461
|
+
const truthDocsRoot = options.truthDocsRoot?.replaceAll("\\", "/").replace(/\/+$/u, "");
|
|
462
|
+
if (truthDocsRoot && normalizedPath.startsWith(`${truthDocsRoot}/`) || normalizedPath.startsWith("docs/truth/")) {
|
|
463
|
+
return "behavior";
|
|
464
|
+
}
|
|
465
|
+
if (normalizedPath.startsWith("docs/contracts/") || normalizedPath.startsWith("docs/contract/") || normalizedPath.startsWith("docs/api/")) {
|
|
466
|
+
return "contract";
|
|
467
|
+
}
|
|
468
|
+
if (normalizedPath.startsWith("docs/architecture/")) {
|
|
469
|
+
return "architecture";
|
|
470
|
+
}
|
|
471
|
+
if (normalizedPath.startsWith("docs/workflows/") || normalizedPath.startsWith("docs/workflow/")) {
|
|
472
|
+
return "workflow";
|
|
473
|
+
}
|
|
474
|
+
if (normalizedPath.startsWith("docs/operations/") || normalizedPath.startsWith("docs/platform/")) {
|
|
475
|
+
return "operations";
|
|
476
|
+
}
|
|
477
|
+
if (normalizedPath.startsWith("docs/testing/") || normalizedPath.startsWith("docs/tests/")) {
|
|
478
|
+
return "test-behavior";
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
};
|
|
482
|
+
var findTruthDocumentsYamlFenceRange = (sectionLines) => {
|
|
483
|
+
const trimmedLines = sectionLines.map((line) => line.trim());
|
|
484
|
+
const openingFenceIndex = trimmedLines.findIndex((line) => /^```(?:yaml|yml)?$/u.test(line));
|
|
485
|
+
if (openingFenceIndex === -1) {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
const closingFenceIndex = trimmedLines.findIndex(
|
|
489
|
+
(line, index) => index > openingFenceIndex && line === "```"
|
|
490
|
+
);
|
|
491
|
+
return {
|
|
492
|
+
openingFenceIndex,
|
|
493
|
+
closingFenceIndex: closingFenceIndex === -1 ? null : closingFenceIndex
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
var parseTruthDocumentsFromList = (sectionLines, areaName, options) => {
|
|
497
|
+
const diagnostics = [];
|
|
498
|
+
const truthDocuments = parseListSection(sectionLines);
|
|
499
|
+
const truthDocumentEntries = truthDocuments.map((documentPath) => {
|
|
500
|
+
const inferredKind = inferTruthDocumentKindFromPath(documentPath, options);
|
|
501
|
+
if (!inferredKind) {
|
|
502
|
+
diagnostics.push(
|
|
503
|
+
createAreaDiagnostic(
|
|
504
|
+
`Truth document ${documentPath} does not match a known kind path convention; defaulting to behavior.`,
|
|
505
|
+
areaName,
|
|
506
|
+
"review"
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
path: documentPath,
|
|
512
|
+
kind: inferredKind ?? "behavior",
|
|
513
|
+
kindSource: inferredKind ? "inferred" : "defaulted"
|
|
514
|
+
};
|
|
515
|
+
});
|
|
516
|
+
return {
|
|
517
|
+
truthDocuments,
|
|
518
|
+
truthDocumentEntries,
|
|
519
|
+
diagnostics
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
var parseTruthDocumentsFromYaml = (sectionLines, areaName) => {
|
|
523
|
+
const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
|
|
524
|
+
if (!yamlFenceRange) {
|
|
525
|
+
return {
|
|
526
|
+
truthDocuments: [],
|
|
527
|
+
truthDocumentEntries: [],
|
|
528
|
+
diagnostics: []
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
if (yamlFenceRange.closingFenceIndex === null) {
|
|
532
|
+
return {
|
|
533
|
+
truthDocuments: [],
|
|
534
|
+
truthDocumentEntries: [],
|
|
535
|
+
diagnostics: [
|
|
536
|
+
createAreaDiagnostic(
|
|
537
|
+
`Area ${areaName} has an unterminated fenced YAML Truth documents block.`,
|
|
538
|
+
areaName
|
|
539
|
+
)
|
|
540
|
+
]
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
let parsedBlock;
|
|
544
|
+
try {
|
|
545
|
+
parsedBlock = parse(
|
|
546
|
+
sectionLines.slice(yamlFenceRange.openingFenceIndex + 1, yamlFenceRange.closingFenceIndex).join("\n")
|
|
547
|
+
);
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return {
|
|
550
|
+
truthDocuments: [],
|
|
551
|
+
truthDocumentEntries: [],
|
|
552
|
+
diagnostics: [
|
|
553
|
+
createAreaDiagnostic(
|
|
554
|
+
`Area ${areaName} has invalid YAML truth document metadata: ${error instanceof Error ? error.message : String(error)}.`,
|
|
555
|
+
areaName
|
|
556
|
+
)
|
|
557
|
+
]
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const rawEntries = parsedBlock && typeof parsedBlock === "object" && "truth_documents" in parsedBlock ? parsedBlock.truth_documents : null;
|
|
561
|
+
if (!Array.isArray(rawEntries)) {
|
|
562
|
+
return {
|
|
563
|
+
truthDocuments: [],
|
|
564
|
+
truthDocumentEntries: [],
|
|
565
|
+
diagnostics: [
|
|
566
|
+
createAreaDiagnostic(
|
|
567
|
+
`Area ${areaName} must define a truth_documents array inside the fenced YAML block.`,
|
|
568
|
+
areaName
|
|
569
|
+
)
|
|
570
|
+
]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const diagnostics = [];
|
|
574
|
+
const truthDocumentEntries = [];
|
|
575
|
+
for (const rawEntry of rawEntries) {
|
|
576
|
+
const path12 = rawEntry && typeof rawEntry === "object" && "path" in rawEntry ? rawEntry.path : null;
|
|
577
|
+
const kind = rawEntry && typeof rawEntry === "object" && "kind" in rawEntry ? rawEntry.kind : null;
|
|
578
|
+
if (typeof path12 !== "string" || path12.trim().length === 0 || !isTruthDocumentKind(kind)) {
|
|
579
|
+
diagnostics.push(
|
|
580
|
+
createAreaDiagnostic(
|
|
581
|
+
`Area ${areaName} truth_documents entries must include non-empty path and valid kind fields.`,
|
|
582
|
+
areaName
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
truthDocumentEntries.push({
|
|
588
|
+
path: path12.trim(),
|
|
589
|
+
kind,
|
|
590
|
+
kindSource: "explicit"
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
truthDocuments: truthDocumentEntries.map((entry) => entry.path),
|
|
595
|
+
truthDocumentEntries,
|
|
596
|
+
diagnostics
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
var parseTruthDocumentsSection = (sectionLines, areaName, options) => {
|
|
600
|
+
const yamlFenceRange = findTruthDocumentsYamlFenceRange(sectionLines);
|
|
601
|
+
if (!yamlFenceRange) {
|
|
602
|
+
return parseTruthDocumentsFromList(sectionLines, areaName, options);
|
|
603
|
+
}
|
|
604
|
+
const yamlResult = parseTruthDocumentsFromYaml(sectionLines, areaName);
|
|
605
|
+
if (yamlResult.diagnostics.length > 0 || yamlFenceRange.closingFenceIndex === null) {
|
|
606
|
+
return yamlResult;
|
|
607
|
+
}
|
|
608
|
+
return yamlResult;
|
|
609
|
+
};
|
|
610
|
+
var parseAreasMarkdown = (source, options = {}) => {
|
|
611
|
+
const lines = source.split("\n");
|
|
612
|
+
const diagnostics = [];
|
|
613
|
+
const areas = [];
|
|
614
|
+
const truthDocumentReferences = [];
|
|
615
|
+
const areaFileReferences = [];
|
|
616
|
+
let areaIndex = 0;
|
|
617
|
+
let currentAreaName = null;
|
|
618
|
+
let currentSections = /* @__PURE__ */ new Map();
|
|
619
|
+
let currentSectionName = null;
|
|
620
|
+
const flushArea = () => {
|
|
621
|
+
if (!currentAreaName) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const truthDocumentResult = parseTruthDocumentsSection(
|
|
625
|
+
currentSections.get("Truth documents") ?? [],
|
|
626
|
+
currentAreaName,
|
|
627
|
+
options
|
|
628
|
+
);
|
|
629
|
+
const { truthDocuments, truthDocumentEntries } = truthDocumentResult;
|
|
630
|
+
const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
|
|
631
|
+
const codeSurface = parseListSection(currentSections.get("Code surface") ?? []);
|
|
632
|
+
const updateTruthWhen = parseListSection(currentSections.get("Update truth when") ?? []);
|
|
633
|
+
const areaKey = slugify(currentAreaName);
|
|
634
|
+
const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
|
|
635
|
+
const hasTruthDocuments = truthDocuments.length > 0;
|
|
636
|
+
const hasAreaFiles = areaFiles.length > 0;
|
|
637
|
+
areaIndex += 1;
|
|
638
|
+
diagnostics.push(...truthDocumentResult.diagnostics);
|
|
639
|
+
if (hasTruthDocuments) {
|
|
640
|
+
truthDocumentReferences.push({
|
|
641
|
+
id: areaId,
|
|
642
|
+
name: currentAreaName,
|
|
643
|
+
key: areaKey,
|
|
644
|
+
truthDocuments,
|
|
645
|
+
truthDocumentEntries
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
|
|
649
|
+
diagnostics.push(
|
|
650
|
+
createAreaDiagnostic(
|
|
651
|
+
`Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
|
|
652
|
+
currentAreaName
|
|
653
|
+
)
|
|
654
|
+
);
|
|
655
|
+
} else if (hasAreaFiles) {
|
|
656
|
+
areaFileReferences.push({
|
|
657
|
+
id: areaId,
|
|
658
|
+
name: currentAreaName,
|
|
659
|
+
key: areaKey,
|
|
660
|
+
areaFiles,
|
|
661
|
+
codeSurface,
|
|
662
|
+
updateTruthWhen
|
|
663
|
+
});
|
|
664
|
+
} else {
|
|
665
|
+
areas.push({
|
|
666
|
+
id: areaId,
|
|
667
|
+
name: currentAreaName,
|
|
668
|
+
key: areaKey,
|
|
669
|
+
truthDocuments,
|
|
670
|
+
truthDocumentEntries,
|
|
671
|
+
codeSurface,
|
|
672
|
+
updateTruthWhen
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
currentAreaName = null;
|
|
676
|
+
currentSections = /* @__PURE__ */ new Map();
|
|
677
|
+
currentSectionName = null;
|
|
678
|
+
};
|
|
679
|
+
for (const line of lines) {
|
|
680
|
+
const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
|
|
681
|
+
if (areaHeadingMatch) {
|
|
682
|
+
flushArea();
|
|
683
|
+
currentAreaName = areaHeadingMatch[1]?.trim() ?? null;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (!currentAreaName) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(line.trim())) {
|
|
690
|
+
currentSectionName = line.trim().slice(0, -1);
|
|
691
|
+
currentSections.set(currentSectionName, []);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (currentSectionName) {
|
|
695
|
+
currentSections.get(currentSectionName)?.push(line);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
flushArea();
|
|
699
|
+
return {
|
|
700
|
+
areas,
|
|
701
|
+
truthDocumentReferences,
|
|
702
|
+
areaFileReferences,
|
|
703
|
+
diagnostics
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/truth/docs.ts
|
|
708
|
+
var DEFAULT_TRUTH_DOCS_ROOT = DEFAULT_DOCS_HIERARCHY.roots.truth;
|
|
709
|
+
var resolveTruthDocsRoot = (config) => {
|
|
710
|
+
return config.docs.roots.truth ?? DEFAULT_TRUTH_DOCS_ROOT;
|
|
711
|
+
};
|
|
436
712
|
|
|
437
713
|
// src/templates/init-files.ts
|
|
714
|
+
var asRelativePath = (value) => {
|
|
715
|
+
return value.split(path3.sep).join("/");
|
|
716
|
+
};
|
|
717
|
+
var currentDate = () => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
718
|
+
var resolveRelativePath = (fromPath, toPath) => {
|
|
719
|
+
return asRelativePath(path3.relative(path3.dirname(fromPath), toPath));
|
|
720
|
+
};
|
|
721
|
+
var truthRoot = resolveTruthDocsRoot;
|
|
438
722
|
var renderConfigTemplate = () => {
|
|
439
723
|
return stringify(createDefaultRawConfig());
|
|
440
724
|
};
|
|
441
|
-
var renderTruthmarkTemplate = () => {
|
|
442
|
-
return `---
|
|
443
|
-
status: active
|
|
444
|
-
doc_type: workflow-contract
|
|
445
|
-
last_reviewed: 2026-05-10
|
|
446
|
-
source_of_truth:
|
|
447
|
-
- .truthmark/config.yml
|
|
448
|
-
---
|
|
449
|
-
|
|
450
|
-
# Truthmark
|
|
451
|
-
|
|
452
|
-
Markdown in the current checkout is authoritative for this branch.
|
|
453
|
-
|
|
454
|
-
Installed workflow surfaces include a Truthmark ${TRUTHMARK_VERSION} version marker. After upgrading Truthmark, rerun \`truthmark init\` and review generated workflow diffs.
|
|
455
|
-
|
|
456
|
-
Workflow runtime lives in installed skills and managed instruction blocks. Agents inspect the checkout directly; \`truthmark check\` is optional validation.
|
|
457
|
-
|
|
458
|
-
Truth Sync follows code; Truth Realize follows docs. Truth Sync may update mapped truth docs; Truth Realize never edits truth docs or routing.
|
|
459
|
-
`;
|
|
460
|
-
};
|
|
461
725
|
var titleCase = (value) => {
|
|
462
726
|
return value.split(/[-_\s]+/u).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
463
727
|
};
|
|
@@ -465,13 +729,17 @@ var renderHierarchicalAreasIndexTemplate = (config) => {
|
|
|
465
729
|
const defaultArea = config.docs.routing.defaultArea;
|
|
466
730
|
const childPath = `${config.docs.routing.areaFilesRoot}/${defaultArea}.md`;
|
|
467
731
|
const title = titleCase(defaultArea);
|
|
732
|
+
const sourceOfTruth = resolveRelativePath(
|
|
733
|
+
config.docs.routing.rootIndex,
|
|
734
|
+
".truthmark/config.yml"
|
|
735
|
+
);
|
|
468
736
|
return [
|
|
469
737
|
"---",
|
|
470
738
|
"status: active",
|
|
471
739
|
"doc_type: route-index",
|
|
472
|
-
|
|
740
|
+
`last_reviewed: ${currentDate()}`,
|
|
473
741
|
"source_of_truth:",
|
|
474
|
-
|
|
742
|
+
` - ${sourceOfTruth}`,
|
|
475
743
|
"---",
|
|
476
744
|
"",
|
|
477
745
|
"# Truthmark Areas",
|
|
@@ -493,15 +761,17 @@ var renderHierarchicalAreasIndexTemplate = (config) => {
|
|
|
493
761
|
var renderChildAreaTemplate = (config) => {
|
|
494
762
|
const defaultArea = config.docs.routing.defaultArea;
|
|
495
763
|
const title = titleCase(defaultArea);
|
|
496
|
-
const
|
|
497
|
-
const leafTruthDoc = `${
|
|
764
|
+
const truthDocsRoot = truthRoot(config);
|
|
765
|
+
const leafTruthDoc = `${truthDocsRoot}/${defaultArea}/overview.md`;
|
|
766
|
+
const templatePath = `${config.docs.routing.areaFilesRoot}/${defaultArea}.md`;
|
|
767
|
+
const sourceOfTruth = resolveRelativePath(templatePath, ".truthmark/config.yml");
|
|
498
768
|
return [
|
|
499
769
|
"---",
|
|
500
770
|
"status: active",
|
|
501
771
|
"doc_type: area-route",
|
|
502
|
-
|
|
772
|
+
`last_reviewed: ${currentDate()}`,
|
|
503
773
|
"source_of_truth:",
|
|
504
|
-
|
|
774
|
+
` - ${sourceOfTruth}`,
|
|
505
775
|
"---",
|
|
506
776
|
"",
|
|
507
777
|
`# ${title} Areas`,
|
|
@@ -509,7 +779,11 @@ var renderChildAreaTemplate = (config) => {
|
|
|
509
779
|
`## ${title}`,
|
|
510
780
|
"",
|
|
511
781
|
"Truth documents:",
|
|
512
|
-
|
|
782
|
+
"```yaml",
|
|
783
|
+
"truth_documents:",
|
|
784
|
+
` - path: ${leafTruthDoc}`,
|
|
785
|
+
" kind: behavior",
|
|
786
|
+
"```",
|
|
513
787
|
"",
|
|
514
788
|
"Code surface:",
|
|
515
789
|
"- src/**",
|
|
@@ -519,41 +793,51 @@ var renderChildAreaTemplate = (config) => {
|
|
|
519
793
|
""
|
|
520
794
|
].join("\n");
|
|
521
795
|
};
|
|
522
|
-
var
|
|
796
|
+
var renderTruthRootReadmeTemplate = (config = createDefaultConfig()) => {
|
|
797
|
+
const templatePath = `${truthRoot(config)}/README.md`;
|
|
798
|
+
const sourceOfTruth = resolveRelativePath(
|
|
799
|
+
templatePath,
|
|
800
|
+
config.docs.routing.rootIndex
|
|
801
|
+
);
|
|
523
802
|
return [
|
|
524
803
|
"---",
|
|
525
804
|
"status: active",
|
|
526
805
|
"doc_type: index",
|
|
527
|
-
|
|
806
|
+
`last_reviewed: ${currentDate()}`,
|
|
528
807
|
"source_of_truth:",
|
|
529
|
-
|
|
808
|
+
` - ${sourceOfTruth}`,
|
|
530
809
|
"---",
|
|
531
810
|
"",
|
|
532
|
-
"#
|
|
811
|
+
"# Truth Docs",
|
|
533
812
|
"",
|
|
534
|
-
"This directory is an index for current
|
|
813
|
+
"This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
|
|
535
814
|
"",
|
|
536
|
-
"README.md files are indexes, not Truth Sync targets. Keep
|
|
815
|
+
"README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs under `<domain>/<behavior>.md`.",
|
|
537
816
|
""
|
|
538
817
|
].join("\n");
|
|
539
818
|
};
|
|
540
|
-
var
|
|
819
|
+
var renderTruthDomainReadmeTemplate = (config) => {
|
|
541
820
|
const defaultArea = config.docs.routing.defaultArea;
|
|
542
821
|
const title = titleCase(defaultArea);
|
|
822
|
+
const templatePath = `${truthRoot(config)}/${defaultArea}/README.md`;
|
|
823
|
+
const sourceOfTruth = resolveRelativePath(
|
|
824
|
+
templatePath,
|
|
825
|
+
`${config.docs.routing.areaFilesRoot}/${defaultArea}.md`
|
|
826
|
+
);
|
|
543
827
|
return [
|
|
544
828
|
"---",
|
|
545
829
|
"status: active",
|
|
546
830
|
"doc_type: index",
|
|
547
|
-
|
|
831
|
+
`last_reviewed: ${currentDate()}`,
|
|
548
832
|
"source_of_truth:",
|
|
549
|
-
` -
|
|
833
|
+
` - ${sourceOfTruth}`,
|
|
550
834
|
"---",
|
|
551
835
|
"",
|
|
552
|
-
`# ${title}
|
|
836
|
+
`# ${title} Truth Docs`,
|
|
553
837
|
"",
|
|
554
|
-
`This directory indexes bounded ${title.toLowerCase()}
|
|
838
|
+
`This directory indexes bounded ${title.toLowerCase()} truth docs.`,
|
|
555
839
|
"",
|
|
556
|
-
"README.md files are indexes, not Truth Sync targets. Keep
|
|
840
|
+
"README.md files are indexes, not Truth Sync targets. Keep bounded truth in leaf docs in this directory.",
|
|
557
841
|
"",
|
|
558
842
|
"Current leaf docs:",
|
|
559
843
|
"",
|
|
@@ -561,62 +845,252 @@ var renderFeatureDomainReadmeTemplate = (config) => {
|
|
|
561
845
|
""
|
|
562
846
|
].join("\n");
|
|
563
847
|
};
|
|
564
|
-
var
|
|
565
|
-
|
|
566
|
-
|
|
848
|
+
var BEHAVIOR_DOC_TEMPLATE_PATH = "docs/templates/behavior-doc.md";
|
|
849
|
+
var CONTRACT_DOC_TEMPLATE_PATH = "docs/templates/contract-doc.md";
|
|
850
|
+
var ARCHITECTURE_DOC_TEMPLATE_PATH = "docs/templates/architecture-doc.md";
|
|
851
|
+
var WORKFLOW_DOC_TEMPLATE_PATH = "docs/templates/workflow-doc.md";
|
|
852
|
+
var OPERATIONS_DOC_TEMPLATE_PATH = "docs/templates/operations-doc.md";
|
|
853
|
+
var TEST_BEHAVIOR_DOC_TEMPLATE_PATH = "docs/templates/test-behavior-doc.md";
|
|
854
|
+
var renderBehaviorDocTemplateFile = () => {
|
|
567
855
|
return [
|
|
568
856
|
"---",
|
|
569
857
|
"status: active",
|
|
570
|
-
"doc_type:
|
|
571
|
-
"
|
|
858
|
+
"doc_type: behavior",
|
|
859
|
+
"truth_kind: behavior",
|
|
860
|
+
`last_reviewed: ${currentDate()}`,
|
|
572
861
|
"source_of_truth:",
|
|
573
|
-
|
|
862
|
+
" - {{source_of_truth}}",
|
|
574
863
|
"---",
|
|
575
864
|
"",
|
|
576
|
-
|
|
865
|
+
"# {{title}}",
|
|
866
|
+
"",
|
|
867
|
+
"## Purpose",
|
|
868
|
+
"",
|
|
869
|
+
"<!-- State why this feature exists, the user or system outcome it protects, and the problem it solves. Keep roadmap or implementation plans out of this section. -->",
|
|
870
|
+
"",
|
|
871
|
+
"{{purpose}}",
|
|
577
872
|
"",
|
|
578
873
|
"## Scope",
|
|
579
874
|
"",
|
|
580
|
-
|
|
875
|
+
"{{scope}}",
|
|
876
|
+
"",
|
|
877
|
+
"<!--",
|
|
878
|
+
"This doc must own one coherent behavior surface.",
|
|
879
|
+
"Split into another leaf doc when content introduces:",
|
|
880
|
+
"- a distinct user or system outcome",
|
|
881
|
+
"- a separate lifecycle or state machine",
|
|
882
|
+
"- an unrelated rule family",
|
|
883
|
+
"- a different external contract",
|
|
884
|
+
"- code that should route through a different owner",
|
|
885
|
+
"Keep README.md files as indexes only.",
|
|
886
|
+
"-->",
|
|
887
|
+
"",
|
|
888
|
+
"This doc was created from the editable behavior-doc template at {{template_path}}.",
|
|
581
889
|
"",
|
|
582
890
|
"## Current Behavior",
|
|
583
891
|
"",
|
|
584
|
-
"
|
|
892
|
+
"<!-- Describe implemented behavior in present tense. Do not include desired future behavior. -->",
|
|
893
|
+
"",
|
|
894
|
+
"{{current_behavior}}",
|
|
895
|
+
"",
|
|
896
|
+
"## Core Rules",
|
|
897
|
+
"",
|
|
898
|
+
"<!-- Capture stable business rules, invariants, precedence rules, validation rules, and must-never constraints. Omit incidental implementation details. -->",
|
|
899
|
+
"",
|
|
900
|
+
"{{core_rules}}",
|
|
901
|
+
"",
|
|
902
|
+
"## Flows And States",
|
|
903
|
+
"",
|
|
904
|
+
"<!-- Use for route switches, state transitions, lifecycle stages, retries, fallbacks, and important error paths. Write 'None beyond current behavior.' when no distinct flow or state model exists. -->",
|
|
905
|
+
"",
|
|
906
|
+
"{{flows_and_states}}",
|
|
907
|
+
"",
|
|
908
|
+
"## Contracts",
|
|
909
|
+
"",
|
|
910
|
+
"<!-- Capture user-visible or integration contracts: CLI/API shape, inputs, outputs, diagnostics, files, events, permissions, or links to canonical contract docs. Avoid duplicating a separate canonical contract doc. -->",
|
|
911
|
+
"",
|
|
912
|
+
"{{contracts}}",
|
|
585
913
|
"",
|
|
586
914
|
"## Product Decisions",
|
|
587
915
|
"",
|
|
588
|
-
"
|
|
916
|
+
"<!-- Keep active decisions only. Replace stale decisions instead of appending historical logs. -->",
|
|
917
|
+
"",
|
|
918
|
+
"{{decision}}",
|
|
589
919
|
"",
|
|
590
920
|
"## Rationale",
|
|
591
921
|
"",
|
|
592
|
-
"
|
|
922
|
+
"<!-- Explain why the current behavior and active decisions are this way, including tradeoffs. -->",
|
|
923
|
+
"",
|
|
924
|
+
"{{rationale}}",
|
|
925
|
+
"",
|
|
926
|
+
"## Non-Goals",
|
|
927
|
+
"",
|
|
928
|
+
"<!-- Name adjacent behavior this doc intentionally does not own, especially tempting future expansions. -->",
|
|
929
|
+
"",
|
|
930
|
+
"{{non_goals}}",
|
|
931
|
+
"",
|
|
932
|
+
"## Maintenance Notes",
|
|
933
|
+
"",
|
|
934
|
+
"<!-- List related tests, routing cautions, migration notes, and common drift risks for future agents. Keep this operational, not historical. -->",
|
|
935
|
+
"",
|
|
936
|
+
"{{maintenance_notes}}",
|
|
593
937
|
""
|
|
594
938
|
].join("\n");
|
|
595
939
|
};
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
940
|
+
var renderTypedTruthDocTemplate = (truthKind, docType, title, sections) => {
|
|
941
|
+
const placeholderNameForSection = (section) => {
|
|
942
|
+
return section.replace(/^#+\s+/u, "").toLowerCase().replaceAll(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
943
|
+
};
|
|
944
|
+
return [
|
|
945
|
+
"---",
|
|
946
|
+
"status: active",
|
|
947
|
+
`doc_type: ${docType}`,
|
|
948
|
+
`truth_kind: ${truthKind}`,
|
|
949
|
+
`last_reviewed: ${currentDate()}`,
|
|
950
|
+
"source_of_truth:",
|
|
951
|
+
" - {{source_of_truth}}",
|
|
952
|
+
"---",
|
|
953
|
+
"",
|
|
954
|
+
`# ${title}`,
|
|
955
|
+
"",
|
|
956
|
+
"## Purpose",
|
|
957
|
+
"",
|
|
958
|
+
"{{purpose}}",
|
|
959
|
+
"",
|
|
960
|
+
"## Scope",
|
|
961
|
+
"",
|
|
962
|
+
"{{scope}}",
|
|
963
|
+
"",
|
|
964
|
+
...sections.flatMap((section) => [
|
|
965
|
+
section,
|
|
966
|
+
"",
|
|
967
|
+
`{{${placeholderNameForSection(section)}}}`,
|
|
968
|
+
""
|
|
969
|
+
]),
|
|
970
|
+
"## Product Decisions",
|
|
971
|
+
"",
|
|
972
|
+
"{{decision}}",
|
|
973
|
+
"",
|
|
974
|
+
"## Rationale",
|
|
975
|
+
"",
|
|
976
|
+
"{{rationale}}",
|
|
977
|
+
"",
|
|
978
|
+
"## Non-Goals",
|
|
979
|
+
"",
|
|
980
|
+
"{{non_goals}}",
|
|
981
|
+
"",
|
|
982
|
+
"## Maintenance Notes",
|
|
983
|
+
"",
|
|
984
|
+
"{{maintenance_notes}}",
|
|
985
|
+
""
|
|
986
|
+
].join("\n");
|
|
987
|
+
};
|
|
988
|
+
var renderContractDocTemplateFile = () => {
|
|
989
|
+
return renderTypedTruthDocTemplate("contract", "contract", "{{title}}", [
|
|
990
|
+
"## Contract Surface",
|
|
991
|
+
"## Inputs",
|
|
992
|
+
"## Outputs",
|
|
993
|
+
"## Errors And Diagnostics",
|
|
994
|
+
"## Compatibility Rules",
|
|
995
|
+
"## Versioning And Migration"
|
|
996
|
+
]);
|
|
997
|
+
};
|
|
998
|
+
var renderArchitectureDocTemplateFile = () => {
|
|
999
|
+
return renderTypedTruthDocTemplate("architecture", "architecture", "{{title}}", [
|
|
1000
|
+
"## System Role",
|
|
1001
|
+
"## Boundaries",
|
|
1002
|
+
"## Components",
|
|
1003
|
+
"## Data And Control Flow",
|
|
1004
|
+
"## Ownership",
|
|
1005
|
+
"## Cross-Cutting Constraints"
|
|
1006
|
+
]);
|
|
1007
|
+
};
|
|
1008
|
+
var renderWorkflowDocTemplateFile = () => {
|
|
1009
|
+
return renderTypedTruthDocTemplate("workflow", "behavior", "{{title}}", [
|
|
1010
|
+
"## Triggers",
|
|
1011
|
+
"## Inputs",
|
|
1012
|
+
"## Execution Model",
|
|
1013
|
+
"## Steps",
|
|
1014
|
+
"## State, Retry, And Failure Behavior",
|
|
1015
|
+
"## Outputs"
|
|
1016
|
+
]);
|
|
1017
|
+
};
|
|
1018
|
+
var renderOperationsDocTemplateFile = () => {
|
|
1019
|
+
return renderTypedTruthDocTemplate("operations", "behavior", "{{title}}", [
|
|
1020
|
+
"## Operational Surface",
|
|
1021
|
+
"## Runtime Topology",
|
|
1022
|
+
"## Configuration",
|
|
1023
|
+
"## Permissions",
|
|
1024
|
+
"## Deployment And Rollback",
|
|
1025
|
+
"## Availability And Observability"
|
|
1026
|
+
]);
|
|
1027
|
+
};
|
|
1028
|
+
var renderTestBehaviorDocTemplateFile = () => {
|
|
1029
|
+
return renderTypedTruthDocTemplate("test-behavior", "behavior", "{{title}}", [
|
|
1030
|
+
"## Test Surface",
|
|
1031
|
+
"## Fixtures And Data Model",
|
|
1032
|
+
"## Execution Model",
|
|
1033
|
+
"## Assertions And Invariants",
|
|
1034
|
+
"## Isolation Rules",
|
|
1035
|
+
"## Reporting And Failure Semantics"
|
|
1036
|
+
]);
|
|
1037
|
+
};
|
|
1038
|
+
var renderTemplate = (template, values) => {
|
|
1039
|
+
return Object.entries(values).reduce((rendered, [key, value]) => {
|
|
1040
|
+
return rendered.split(`{{${key}}}`).join(value);
|
|
1041
|
+
}, template);
|
|
1042
|
+
};
|
|
1043
|
+
var renderBehaviorLeafDocTemplate = (config, template = renderBehaviorDocTemplateFile()) => {
|
|
1044
|
+
const defaultArea = config.docs.routing.defaultArea;
|
|
1045
|
+
const title = titleCase(defaultArea);
|
|
1046
|
+
const templatePath = `${truthRoot(config)}/${defaultArea}/overview.md`;
|
|
1047
|
+
const sourceOfTruth = resolveRelativePath(
|
|
1048
|
+
templatePath,
|
|
1049
|
+
`${config.docs.routing.areaFilesRoot}/${defaultArea}.md`
|
|
1050
|
+
);
|
|
1051
|
+
const today = currentDate();
|
|
1052
|
+
return renderTemplate(template, {
|
|
1053
|
+
area: defaultArea,
|
|
1054
|
+
contracts: "- External contracts should link to the nearest canonical contract doc when one exists.",
|
|
1055
|
+
core_rules: "- Truth README files are indexes; behavior truth belongs in bounded leaf docs.",
|
|
1056
|
+
current_behavior: "- Document current behavior here when implementation changes make repository truth incomplete.",
|
|
1057
|
+
decision: `- Decision (${today}): Truth README files are indexes; behavior truth belongs in bounded leaf docs.`,
|
|
1058
|
+
flows_and_states: "- None beyond current behavior.",
|
|
1059
|
+
maintenance_notes: "- Update this doc when routed implementation changes alter current behavior, rules, contracts, or decisions.",
|
|
1060
|
+
non_goals: "- This doc is not a catch-all for unrelated repository behavior.",
|
|
1061
|
+
purpose: `Describe why the default ${title.toLowerCase()} behavior surface exists and what outcome it protects.`,
|
|
1062
|
+
rationale: "Bounded leaf docs keep agent context focused and prevent large products from accumulating unreviewable feature manuals.",
|
|
1063
|
+
scope: `This bounded leaf truth doc owns the default ${title.toLowerCase()} behavior surface created by Truthmark.`,
|
|
1064
|
+
source_of_truth: sourceOfTruth,
|
|
1065
|
+
template_path: BEHAVIOR_DOC_TEMPLATE_PATH,
|
|
1066
|
+
title: `${title} Overview`,
|
|
1067
|
+
truth_kind: "behavior"
|
|
1068
|
+
});
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
// src/config/command.ts
|
|
1072
|
+
var CONFIG_PATH = ".truthmark/config.yml";
|
|
1073
|
+
var configExists = async (rootDir) => {
|
|
1074
|
+
try {
|
|
1075
|
+
await fs3.stat(resolveRepoPath(rootDir, CONFIG_PATH));
|
|
1076
|
+
return true;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
var runConfig = async (cwd, options = {}) => {
|
|
1085
|
+
const repository = await getGitRepository(cwd);
|
|
1086
|
+
const content = renderConfigTemplate();
|
|
1087
|
+
if (options.stdout) {
|
|
1088
|
+
return {
|
|
1089
|
+
command: "config",
|
|
1090
|
+
summary: "Rendered default Truthmark config.",
|
|
1091
|
+
diagnostics: [],
|
|
1092
|
+
data: {
|
|
1093
|
+
repositoryRoot: repository.repositoryRoot,
|
|
620
1094
|
worktreePath: repository.worktreePath,
|
|
621
1095
|
branchName: repository.branchName,
|
|
622
1096
|
isDetached: repository.isDetached,
|
|
@@ -671,12 +1145,12 @@ var runConfig = async (cwd, options = {}) => {
|
|
|
671
1145
|
};
|
|
672
1146
|
|
|
673
1147
|
// src/init/init.ts
|
|
674
|
-
import
|
|
1148
|
+
import fs7 from "fs/promises";
|
|
675
1149
|
|
|
676
1150
|
// src/config/load.ts
|
|
677
|
-
import
|
|
1151
|
+
import fs4 from "fs/promises";
|
|
678
1152
|
import { Ajv } from "ajv";
|
|
679
|
-
import { parse } from "yaml";
|
|
1153
|
+
import { parse as parse2 } from "yaml";
|
|
680
1154
|
var ajv = new Ajv({ allErrors: true });
|
|
681
1155
|
var validateTruthmarkConfig = ajv.compile(truthmarkConfigSchema);
|
|
682
1156
|
var toConfigDiagnostic = (message, file) => {
|
|
@@ -693,12 +1167,13 @@ var normalizeConfig = (rawConfig) => {
|
|
|
693
1167
|
roots: { ...DEFAULT_DOCS_HIERARCHY.roots },
|
|
694
1168
|
routing: { ...DEFAULT_DOCS_HIERARCHY.routing }
|
|
695
1169
|
};
|
|
1170
|
+
const roots = { ...DEFAULT_DOCS_HIERARCHY.roots, ...rawDocs.roots };
|
|
696
1171
|
return {
|
|
697
1172
|
version: rawConfig.version,
|
|
698
1173
|
platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
|
|
699
1174
|
docs: {
|
|
700
1175
|
layout: rawDocs.layout,
|
|
701
|
-
roots
|
|
1176
|
+
roots,
|
|
702
1177
|
routing: {
|
|
703
1178
|
rootIndex: rawDocs.routing.root_index,
|
|
704
1179
|
areaFilesRoot: rawDocs.routing.area_files_root,
|
|
@@ -712,10 +1187,7 @@ var normalizeConfig = (rawConfig) => {
|
|
|
712
1187
|
required: rawConfig.frontmatter?.required ?? [],
|
|
713
1188
|
recommended: rawConfig.frontmatter?.recommended ?? []
|
|
714
1189
|
},
|
|
715
|
-
ignore: rawConfig.ignore ?? []
|
|
716
|
-
realization: {
|
|
717
|
-
enabled: rawConfig.realization.enabled
|
|
718
|
-
}
|
|
1190
|
+
ignore: rawConfig.ignore ?? []
|
|
719
1191
|
};
|
|
720
1192
|
};
|
|
721
1193
|
var loadConfig = async (rootDir) => {
|
|
@@ -723,7 +1195,7 @@ var loadConfig = async (rootDir) => {
|
|
|
723
1195
|
const absolutePath = resolveRepoPath(rootDir, configPath);
|
|
724
1196
|
let source;
|
|
725
1197
|
try {
|
|
726
|
-
source = await
|
|
1198
|
+
source = await fs4.readFile(absolutePath, "utf8");
|
|
727
1199
|
} catch (error) {
|
|
728
1200
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
729
1201
|
return {
|
|
@@ -737,7 +1209,7 @@ var loadConfig = async (rootDir) => {
|
|
|
737
1209
|
}
|
|
738
1210
|
let parsedConfig;
|
|
739
1211
|
try {
|
|
740
|
-
parsedConfig =
|
|
1212
|
+
parsedConfig = parse2(source);
|
|
741
1213
|
} catch (error) {
|
|
742
1214
|
return {
|
|
743
1215
|
status: "invalid",
|
|
@@ -773,10 +1245,10 @@ var loadConfig = async (rootDir) => {
|
|
|
773
1245
|
};
|
|
774
1246
|
|
|
775
1247
|
// src/init/hierarchy.ts
|
|
1248
|
+
import fs5 from "fs/promises";
|
|
776
1249
|
import fg from "fast-glob";
|
|
777
1250
|
var KNOWN_DEFAULT_ROOTS = [
|
|
778
|
-
DEFAULT_DOCS_HIERARCHY.roots.
|
|
779
|
-
"docs/features/current",
|
|
1251
|
+
DEFAULT_DOCS_HIERARCHY.roots.truth,
|
|
780
1252
|
"docs/api",
|
|
781
1253
|
DEFAULT_DOCS_HIERARCHY.roots.architecture,
|
|
782
1254
|
DEFAULT_DOCS_HIERARCHY.roots.standards,
|
|
@@ -790,10 +1262,28 @@ var hasMarkdownFiles = async (rootDir, root) => {
|
|
|
790
1262
|
});
|
|
791
1263
|
return matches.length > 0;
|
|
792
1264
|
};
|
|
1265
|
+
var truthRoot2 = resolveTruthDocsRoot;
|
|
1266
|
+
var rootIndexReferencesChildRoute = async (rootDir, rootIndexPath, childRoutePath) => {
|
|
1267
|
+
const rootIndexSource = await fs5.readFile(resolveRepoPath(rootDir, rootIndexPath), "utf8");
|
|
1268
|
+
const parsedRootIndex = parseAreasMarkdown(rootIndexSource);
|
|
1269
|
+
return parsedRootIndex.areaFileReferences.some(
|
|
1270
|
+
(areaReference) => areaReference.areaFiles.includes(childRoutePath)
|
|
1271
|
+
);
|
|
1272
|
+
};
|
|
1273
|
+
var readBehaviorDocTemplate = async (rootDir) => {
|
|
1274
|
+
try {
|
|
1275
|
+
return await fs5.readFile(resolveRepoPath(rootDir, BEHAVIOR_DOC_TEMPLATE_PATH), "utf8");
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
1278
|
+
return renderBehaviorDocTemplateFile();
|
|
1279
|
+
}
|
|
1280
|
+
throw error;
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
793
1283
|
var scaffoldHierarchy = async (rootDir, config) => {
|
|
794
1284
|
const results = [];
|
|
795
|
-
const
|
|
796
|
-
const
|
|
1285
|
+
const truthDocsRoot = truthRoot2(config);
|
|
1286
|
+
const truthDomainRoot = `${truthDocsRoot}/${config.docs.routing.defaultArea}`;
|
|
797
1287
|
const childRoutePath = `${config.docs.routing.areaFilesRoot}/${config.docs.routing.defaultArea}.md`;
|
|
798
1288
|
results.push(
|
|
799
1289
|
await ensureRepoFile(
|
|
@@ -802,26 +1292,55 @@ var scaffoldHierarchy = async (rootDir, config) => {
|
|
|
802
1292
|
renderHierarchicalAreasIndexTemplate(config)
|
|
803
1293
|
)
|
|
804
1294
|
);
|
|
805
|
-
|
|
1295
|
+
if (await rootIndexReferencesChildRoute(rootDir, config.docs.routing.rootIndex, childRoutePath)) {
|
|
1296
|
+
results.push(await ensureRepoFile(rootDir, childRoutePath, renderChildAreaTemplate(config)));
|
|
1297
|
+
}
|
|
1298
|
+
results.push(
|
|
1299
|
+
await ensureRepoFile(
|
|
1300
|
+
rootDir,
|
|
1301
|
+
`${truthDocsRoot}/README.md`,
|
|
1302
|
+
renderTruthRootReadmeTemplate(config)
|
|
1303
|
+
)
|
|
1304
|
+
);
|
|
1305
|
+
results.push(
|
|
1306
|
+
await ensureRepoFile(
|
|
1307
|
+
rootDir,
|
|
1308
|
+
`${truthDomainRoot}/README.md`,
|
|
1309
|
+
renderTruthDomainReadmeTemplate(config)
|
|
1310
|
+
)
|
|
1311
|
+
);
|
|
1312
|
+
results.push(
|
|
1313
|
+
await ensureRepoFile(rootDir, BEHAVIOR_DOC_TEMPLATE_PATH, renderBehaviorDocTemplateFile())
|
|
1314
|
+
);
|
|
1315
|
+
results.push(
|
|
1316
|
+
await ensureRepoFile(rootDir, CONTRACT_DOC_TEMPLATE_PATH, renderContractDocTemplateFile())
|
|
1317
|
+
);
|
|
806
1318
|
results.push(
|
|
807
1319
|
await ensureRepoFile(
|
|
808
1320
|
rootDir,
|
|
809
|
-
|
|
810
|
-
|
|
1321
|
+
ARCHITECTURE_DOC_TEMPLATE_PATH,
|
|
1322
|
+
renderArchitectureDocTemplateFile()
|
|
811
1323
|
)
|
|
812
1324
|
);
|
|
1325
|
+
results.push(
|
|
1326
|
+
await ensureRepoFile(rootDir, WORKFLOW_DOC_TEMPLATE_PATH, renderWorkflowDocTemplateFile())
|
|
1327
|
+
);
|
|
1328
|
+
results.push(
|
|
1329
|
+
await ensureRepoFile(rootDir, OPERATIONS_DOC_TEMPLATE_PATH, renderOperationsDocTemplateFile())
|
|
1330
|
+
);
|
|
813
1331
|
results.push(
|
|
814
1332
|
await ensureRepoFile(
|
|
815
1333
|
rootDir,
|
|
816
|
-
|
|
817
|
-
|
|
1334
|
+
TEST_BEHAVIOR_DOC_TEMPLATE_PATH,
|
|
1335
|
+
renderTestBehaviorDocTemplateFile()
|
|
818
1336
|
)
|
|
819
1337
|
);
|
|
1338
|
+
const behaviorDocTemplate = await readBehaviorDocTemplate(rootDir);
|
|
820
1339
|
results.push(
|
|
821
1340
|
await ensureRepoFile(
|
|
822
1341
|
rootDir,
|
|
823
|
-
`${
|
|
824
|
-
|
|
1342
|
+
`${truthDomainRoot}/overview.md`,
|
|
1343
|
+
renderBehaviorLeafDocTemplate(config, behaviorDocTemplate)
|
|
825
1344
|
)
|
|
826
1345
|
);
|
|
827
1346
|
return results;
|
|
@@ -845,196 +1364,440 @@ var detectHierarchyMigrationDiagnostics = async (rootDir, config) => {
|
|
|
845
1364
|
return diagnostics;
|
|
846
1365
|
};
|
|
847
1366
|
|
|
1367
|
+
// src/truth/evidence.ts
|
|
1368
|
+
var renderClaimEvidenceCheckedSection = (items) => {
|
|
1369
|
+
return [
|
|
1370
|
+
"Evidence checked:",
|
|
1371
|
+
...items.map((item) => {
|
|
1372
|
+
return [
|
|
1373
|
+
`- Claim: ${item.claim}`,
|
|
1374
|
+
` Evidence: ${item.evidence.join(" / ")}`,
|
|
1375
|
+
` Result: ${item.result}`
|
|
1376
|
+
].join("\n");
|
|
1377
|
+
})
|
|
1378
|
+
].join("\n");
|
|
1379
|
+
};
|
|
1380
|
+
var renderAuditEvidenceCheckedSection = (items) => {
|
|
1381
|
+
return [
|
|
1382
|
+
"Evidence checked:",
|
|
1383
|
+
...items.map((item) => {
|
|
1384
|
+
return [
|
|
1385
|
+
`- Finding: ${item.finding}`,
|
|
1386
|
+
` Evidence: ${item.evidence.join(" / ")}`,
|
|
1387
|
+
` Suggested fix: ${item.suggestedFix}`,
|
|
1388
|
+
` Confidence: ${item.confidence}`
|
|
1389
|
+
].join("\n");
|
|
1390
|
+
})
|
|
1391
|
+
].join("\n");
|
|
1392
|
+
};
|
|
1393
|
+
|
|
848
1394
|
// src/agents/shared.ts
|
|
849
1395
|
var DECISION_TRUTH_INSTRUCTIONS = [
|
|
850
|
-
"Decision truth lives in the canonical doc it governs.",
|
|
851
|
-
"
|
|
852
|
-
"
|
|
853
|
-
"Replace old active decisions instead of appending separate timestamped decision logs; Git history is the audit trail.",
|
|
854
|
-
"Update Product Decisions and Rationale when a behavior change comes from a decision change."
|
|
1396
|
+
"Decision truth lives in the canonical doc it governs; date active decisions inline when added or changed.",
|
|
1397
|
+
"Do not create separate active-decision ADR/planning logs; replace the active decision and let Git history carry the audit trail.",
|
|
1398
|
+
"Update Product Decisions and Rationale when a decision changes behavior."
|
|
855
1399
|
].join("\n");
|
|
856
|
-
var EVIDENCE_AUTHORITY_INSTRUCTIONS =
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
var
|
|
861
|
-
|
|
1400
|
+
var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
|
|
1401
|
+
"Repository instruction docs such as docs/ai/repo-rules.md remain instruction authority.",
|
|
1402
|
+
"Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
|
|
1403
|
+
].join("\n");
|
|
1404
|
+
var REPOSITORY_INTELLIGENCE_INSTRUCTIONS = [
|
|
1405
|
+
"Repository intelligence artifacts are optional derived context: RepoIndex, RouteMap, ImpactSet, and ContextPack may guide routing, context selection, and verification planning when available.",
|
|
1406
|
+
"They do not override checkout evidence, canonical truth docs, route files, or workflow write boundaries.",
|
|
1407
|
+
"If unavailable, inspect .truthmark/config.yml, route files, source files, truth docs, and tests directly, then report that repository-intelligence artifacts were not generated."
|
|
1408
|
+
].join("\n");
|
|
1409
|
+
var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
|
|
1410
|
+
"When creating or updating a truth doc, inspect the routed truth kind and use the matching `docs/templates/<kind>-doc.md` template.",
|
|
1411
|
+
"Supported kinds: behavior, contract, architecture, workflow, operations, and test-behavior.",
|
|
1412
|
+
"Align existing docs to that template while preserving accurate authored content.",
|
|
1413
|
+
"If the template is missing, use Scope, Product Decisions, Rationale, and the kind-specific current-truth section.",
|
|
1414
|
+
"Teams may edit the template files under docs/templates/ to define their local truth-doc standards."
|
|
1415
|
+
].join("\n");
|
|
1416
|
+
var renderTruthDocOwnershipGateSection = (subject, outcome) => {
|
|
862
1417
|
return [
|
|
863
|
-
"
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
`-
|
|
867
|
-
|
|
1418
|
+
"Truth-doc ownership gate:",
|
|
1419
|
+
`- before editing or relying on ${subject}, verify each target/source truth doc is a bounded owner for the behavior`,
|
|
1420
|
+
"- if a target/source doc mixes independent owners, spans unrelated behaviors, acts as an index, or needs cross-owner edits, do not patch or in-place repair it",
|
|
1421
|
+
`- ${outcome}`,
|
|
1422
|
+
"- report Ownership reviewed, Structure required, Truth docs split, Truth docs restructured, or Blocked reason as applicable"
|
|
868
1423
|
].join("\n");
|
|
869
1424
|
};
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1425
|
+
var TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS = [
|
|
1426
|
+
"Product Decisions/Rationale preservation gate:",
|
|
1427
|
+
"- before any truth-doc split, restructure, or shape repair, inventory existing Product Decisions and Rationale sections in every source or touched truth doc",
|
|
1428
|
+
"- preserve each current decision and rationale in the bounded owner doc it governs; when splitting, move it to the new owner doc rather than deleting it or leaving it in an index",
|
|
1429
|
+
"- remove or narrow a decision or rationale only when checkout evidence shows it is stale or unsupported, and report the exact claim, evidence, and result",
|
|
1430
|
+
"- if ownership of a decision or rationale is unclear, block with manual-review files instead of deleting it or guessing",
|
|
1431
|
+
"- after the edit, verify every touched truth doc still has Product Decisions and Rationale sections and every pre-existing entry is preserved, moved, narrowed, removed with evidence, or blocked"
|
|
1432
|
+
].join("\n");
|
|
1433
|
+
var renderTruthDocRestructureGateSection = (scope) => {
|
|
1434
|
+
return [
|
|
1435
|
+
"Truth-doc shape repair gate:",
|
|
1436
|
+
`- ${scope}`,
|
|
1437
|
+
"- repair shape in place only after the ownership gate confirms the doc is the right bounded owner",
|
|
1438
|
+
"- use Truth Structure for ownership splits; do not treat broad or mixed-owner docs as in-place repair work",
|
|
1439
|
+
"- repair shape when a narrow edit would make truth worse: missing template sections, stale evidence conflicts, cross-section updates within one owner, or wrong frontmatter/source/headings",
|
|
1440
|
+
"- preserve supported claims; remove, narrow, or block unsupported or stale claims",
|
|
1441
|
+
"- report docs restructured and why a narrow edit was not sufficient"
|
|
1442
|
+
].join("\n");
|
|
875
1443
|
};
|
|
876
|
-
var
|
|
1444
|
+
var ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS = [
|
|
1445
|
+
"Maintain architecture docs only for structure-level changes: system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, or generated-surface ownership.",
|
|
1446
|
+
"Keep ordinary behavior, endpoints, UI copy, validation rules, and bug fixes in behavior or contract docs unless they change those boundaries."
|
|
1447
|
+
].join("\n");
|
|
1448
|
+
var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
|
|
877
1449
|
return [
|
|
878
|
-
"
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1450
|
+
"Evidence Gate:",
|
|
1451
|
+
`- route-first: map ${subject} to bounded route owners and primary canonical docs`,
|
|
1452
|
+
"- review new or changed behavior-bearing claims only in touched docs, route ownership, Product Decisions, and Rationale",
|
|
1453
|
+
"- support claims with primary checkout evidence: implementation, config, routing, generated templates, schemas, or contract definitions",
|
|
1454
|
+
"- tests/examples/canonical docs corroborate; they are not sole proof when implementation conflicts",
|
|
1455
|
+
"- remove, narrow, or block unsupported claims",
|
|
1456
|
+
`- ${noImpactedDocOutcome}`
|
|
1457
|
+
].join("\n");
|
|
883
1458
|
};
|
|
884
|
-
var
|
|
885
|
-
const sections = [
|
|
886
|
-
"Truth Sync: blocked",
|
|
887
|
-
renderBulletSection("Reason", [input.reason])
|
|
888
|
-
];
|
|
889
|
-
if ((input.manualReviewFiles?.length ?? 0) > 0) {
|
|
890
|
-
sections.push(renderBulletSection("Files requiring manual review", input.manualReviewFiles));
|
|
891
|
-
}
|
|
892
|
-
sections.push(renderBulletSection("Next action", [input.nextAction]));
|
|
1459
|
+
var renderTopologyEvidenceGateSection = () => {
|
|
893
1460
|
return [
|
|
894
|
-
|
|
895
|
-
|
|
1461
|
+
"Evidence Gate:",
|
|
1462
|
+
"- apply the Evidence Gate before finishing when Truth Structure writes routed docs, ownership claims, Product Decisions, or Rationale",
|
|
1463
|
+
"- support ownership/behavior claims with topology or primary checkout evidence from layout, implementation boundaries, docs, config, route files, tests, templates, schemas, or contracts",
|
|
1464
|
+
"- tests/examples/canonical docs corroborate; remove, narrow, or block unsupported claims"
|
|
1465
|
+
].join("\n");
|
|
896
1466
|
};
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1467
|
+
var renderAuditEvidenceGateSection = () => {
|
|
1468
|
+
return [
|
|
1469
|
+
"Evidence Gate:",
|
|
1470
|
+
"- support each finding and suggested fix with evidence from config, route files, canonical docs, implementation, templates, or tests",
|
|
1471
|
+
"- canonical docs are context, not sole proof when implementation conflicts",
|
|
1472
|
+
"- remove unsupported findings or mark open questions; validate changed claims if you edit docs"
|
|
1473
|
+
].join("\n");
|
|
902
1474
|
};
|
|
903
|
-
var
|
|
904
|
-
return
|
|
905
|
-
The parent provides the task focus and any repository context already gathered.
|
|
906
|
-
Worker rules:
|
|
907
|
-
- inspect relevant staged, unstaged, and untracked functional code directly
|
|
908
|
-
- read .truthmark/config.yml, TRUTHMARK.md, docs/truthmark/areas.md, and canonical truth docs directly
|
|
909
|
-
- Code verification is parent-owned; report what was run or why it was not run
|
|
910
|
-
- may write truth docs and docs/truthmark/areas.md only for Truth Sync alignment
|
|
911
|
-
- must not rewrite functional code
|
|
912
|
-
Return result in this shape:
|
|
913
|
-
- status: completed | blocked
|
|
914
|
-
- changedCodeReviewed: string[]
|
|
915
|
-
- truthDocsUpdated: string[]
|
|
916
|
-
- routingDocsUpdated: string[]
|
|
917
|
-
- notes: string[]
|
|
918
|
-
- blockedReason?: string
|
|
919
|
-
- manualReviewFiles?: string[]`;
|
|
1475
|
+
var defaultAgentConfig = () => {
|
|
1476
|
+
return createDefaultConfig();
|
|
920
1477
|
};
|
|
921
|
-
var
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
Use this skill automatically before finishing when functional code changed since the last successful Truth Sync. Also run it immediately when the user explicitly invokes Truth Sync.
|
|
931
|
-
Invocations: ${TRUTH_SYNC_EXPLICIT_INVOCATIONS}
|
|
932
|
-
Explicit invocation runs immediately. Later functional-code changes reopen the finish-time requirement, and an earlier explicit run satisfies the finish gate only if no later functional-code changes occur.
|
|
933
|
-
Parent workflow:
|
|
934
|
-
1. Inspect git status, staged changes, unstaged changes, and untracked files directly.
|
|
935
|
-
2. Read .truthmark/config.yml, TRUTHMARK.md, the configured root route index at ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, and relevant canonical docs.
|
|
936
|
-
3. Identify functional-code changes and the nearest truth docs or routing repairs.
|
|
937
|
-
4. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
938
|
-
5. Code verification is parent-owned: follow repository instructions and task context, and report what ran or why it did not run.
|
|
939
|
-
6. Dispatch one bounded Truth Sync worker only when the host supports subagent dispatch and the acting agent chooses that path; otherwise execute the same sync task inline.
|
|
940
|
-
Topology quality gate:
|
|
941
|
-
- before updating truth docs, verify the changed code resolves to a specific behavior-owned area
|
|
942
|
-
- if routing is broad, overloaded, or catch-all route only, do not create another generic feature doc
|
|
943
|
-
- run or recommend Truth Structure before syncing when topology repair is needed
|
|
944
|
-
- block when topology repair is unsafe, ambiguous, or outside the current task boundary
|
|
945
|
-
- report the broad route files and changed code paths that require structure repair
|
|
946
|
-
- README.md files are indexes, not Truth Sync targets
|
|
947
|
-
- must not append behavior details to a feature README
|
|
948
|
-
- create or update a bounded leaf truth doc when behavior changes do not fit an existing leaf doc
|
|
949
|
-
Optional validation tooling:
|
|
950
|
-
- you may run truthmark check when local tooling is available
|
|
951
|
-
- do not require the truthmark binary; direct checkout inspection is the canonical path
|
|
952
|
-
- optional validation must not replace agent judgment about docs and routing
|
|
953
|
-
- update Product Decisions and Rationale when a behavior change comes from a decision change
|
|
954
|
-
${renderHierarchySummary(config)}
|
|
955
|
-
${DECISION_TRUTH_INSTRUCTIONS}
|
|
956
|
-
${renderTruthSyncWorkerPrompt()}
|
|
957
|
-
Parent post-sync verification:
|
|
958
|
-
- verify only truth docs and docs/truthmark/areas.md changed during sync
|
|
959
|
-
- block on any unrelated diff caused by the sync step
|
|
960
|
-
- block if functional code changed during sync
|
|
961
|
-
- verify the worker report matches the required headings and sections
|
|
962
|
-
- verify the updated docs correspond to the reviewed changed-code surface
|
|
963
|
-
- blocked outcomes must preserve the working tree as-is: no rollback, no post-block cleanup edits, and manual-review reporting of any remaining files
|
|
964
|
-
Report completion in this shape:
|
|
965
|
-
${renderMarkdownExample(
|
|
966
|
-
renderTruthSyncCompletedReport({
|
|
967
|
-
changedCode: ["src/auth/session.ts"],
|
|
968
|
-
truthDocsUpdated: ["docs/features/repository/overview.md"],
|
|
969
|
-
notes: ["Updated session timeout behavior."]
|
|
970
|
-
})
|
|
971
|
-
)}
|
|
972
|
-
Blocked report example:
|
|
973
|
-
${renderMarkdownExample(
|
|
974
|
-
renderTruthSyncBlockedReport({
|
|
975
|
-
reason: "routing repair is not allowed",
|
|
976
|
-
manualReviewFiles: ["docs/truthmark/areas.md"],
|
|
977
|
-
nextAction: "update routing metadata and rerun Truth Sync"
|
|
978
|
-
})
|
|
979
|
-
)}`;
|
|
1478
|
+
var renderHierarchySummary = (config) => {
|
|
1479
|
+
const truthRoot3 = resolveTruthDocsRoot(config);
|
|
1480
|
+
return [
|
|
1481
|
+
"Truthmark hierarchy:",
|
|
1482
|
+
"- Config: .truthmark/config.yml",
|
|
1483
|
+
`- Root route index: ${config.docs.routing.rootIndex}`,
|
|
1484
|
+
`- Area route files: ${config.docs.routing.areaFilesRoot}/**/*.md`,
|
|
1485
|
+
`- Truth docs: ${truthRoot3}/**/*.md`
|
|
1486
|
+
].join("\n");
|
|
980
1487
|
};
|
|
981
1488
|
|
|
982
|
-
// src/
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
"
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
"no functional code changes"
|
|
989
|
-
];
|
|
1489
|
+
// src/version.ts
|
|
1490
|
+
import fs6 from "fs";
|
|
1491
|
+
var packageJson = JSON.parse(
|
|
1492
|
+
fs6.readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
1493
|
+
);
|
|
1494
|
+
var TRUTHMARK_VERSION = packageJson.version;
|
|
990
1495
|
|
|
991
1496
|
// src/templates/agents-block.ts
|
|
992
1497
|
var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
|
|
993
1498
|
var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
|
|
994
|
-
var
|
|
1499
|
+
var renderCompactHierarchySummary = (config) => {
|
|
1500
|
+
const truthRoot3 = resolveTruthDocsRoot(config);
|
|
1501
|
+
return `Hierarchy: config .truthmark/config.yml; routes ${config.docs.routing.rootIndex} and ${config.docs.routing.areaFilesRoot}/**/*.md; Truth docs: ${truthRoot3}/**/*.md.`;
|
|
1502
|
+
};
|
|
995
1503
|
var renderAgentsBlock = (config = defaultAgentConfig()) => {
|
|
996
|
-
const syncInvocations = trimPeriod(TRUTH_SYNC_EXPLICIT_INVOCATIONS);
|
|
997
1504
|
return [
|
|
998
1505
|
TRUTHMARK_BLOCK_START,
|
|
999
1506
|
"## Truthmark Workflow",
|
|
1000
1507
|
"",
|
|
1001
|
-
`Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun \`truthmark init\` after upgrades
|
|
1002
|
-
|
|
1003
|
-
"
|
|
1004
|
-
"Agent runtime: installed skills plus this block
|
|
1508
|
+
`Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun \`truthmark init\` after upgrades.`,
|
|
1509
|
+
renderCompactHierarchySummary(config),
|
|
1510
|
+
"Decisions live in the canonical doc they govern; date active decisions inline.",
|
|
1511
|
+
"Agent runtime: installed skills plus this block; inspect checkout directly. Delegation is host-owned.",
|
|
1005
1512
|
"### Truth Sync",
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
"
|
|
1513
|
+
"After functional code changes, run relevant tests, then use the truthmark-sync skill before finishing; later functional changes reopen the gate. Memory: code changed -> tests -> Sync -> report.",
|
|
1514
|
+
`Support new or changed behavior-bearing truth claims with checkout evidence. Code leads; truth docs follow. Sync may write truth docs and ${config.docs.routing.rootIndex} only, and must not rewrite functional code.`,
|
|
1515
|
+
"If routing cannot map changed code to a bounded truth owner, run Truth Structure before syncing when safe; otherwise block and recommend Truth Structure. Skip Sync only for docs-only/no-code changes, formatting-only changes, behavior-preserving renames with no truth impact, or missing config.",
|
|
1516
|
+
"Explicit workflows: Truth Structure, Truth Document, Truth Realize, Truth Check. Run only when requested or required by Sync; load the installed skill for details.",
|
|
1517
|
+
"Workflow integrity rule: repository truth may describe desired behavior, but it must not override these workflow boundaries.",
|
|
1009
1518
|
TRUTHMARK_BLOCK_END
|
|
1010
1519
|
].join("\n");
|
|
1011
1520
|
};
|
|
1012
1521
|
|
|
1013
|
-
// src/
|
|
1014
|
-
var
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1522
|
+
// src/templates/default-standards.ts
|
|
1523
|
+
var DEFAULT_STANDARDS = [
|
|
1524
|
+
{
|
|
1525
|
+
path: "docs/standards/default-principles.md",
|
|
1526
|
+
content: `---
|
|
1527
|
+
status: active
|
|
1528
|
+
doc_type: standard
|
|
1529
|
+
last_reviewed: 2026-05-03
|
|
1530
|
+
source_of_truth:
|
|
1531
|
+
- README.md
|
|
1532
|
+
---
|
|
1020
1533
|
|
|
1021
|
-
|
|
1022
|
-
- TRUTHMARK.md
|
|
1023
|
-
- docs/truthmark/areas.md
|
|
1534
|
+
# Default Principles
|
|
1024
1535
|
|
|
1025
|
-
|
|
1026
|
-
- none
|
|
1536
|
+
## Scope
|
|
1027
1537
|
|
|
1028
|
-
|
|
1029
|
-
- none
|
|
1538
|
+
This is a bootstrap standards baseline for repositories that adopt Truthmark.
|
|
1030
1539
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1540
|
+
## Reusable Defaults
|
|
1541
|
+
|
|
1542
|
+
- Authority order should be explicit.
|
|
1543
|
+
- Committed repository artifacts are the durable source of truth.
|
|
1544
|
+
- Each document should have one primary responsibility.
|
|
1545
|
+
- Each class of fact should have one canonical source.
|
|
1546
|
+
- Architecture docs describe system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, and generated-surface ownership.
|
|
1547
|
+
- Do not put ordinary feature behavior in architecture docs.
|
|
1548
|
+
- Verification should be explicit, and skipped checks should state why.
|
|
1549
|
+
- Missing, stale, broad, overloaded, or unrouteable documentation topology should be repaired through AI-native structure workflow before agents create more generic truth docs.
|
|
1550
|
+
- Installed repository workflows should remain usable from committed files even when the Truthmark CLI is unavailable.
|
|
1551
|
+
`
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
path: "docs/standards/documentation-governance.md",
|
|
1555
|
+
content: `---
|
|
1556
|
+
status: active
|
|
1557
|
+
doc_type: standard
|
|
1558
|
+
last_reviewed: 2026-05-03
|
|
1559
|
+
source_of_truth:
|
|
1560
|
+
- README.md
|
|
1561
|
+
---
|
|
1562
|
+
|
|
1563
|
+
# Documentation Governance
|
|
1564
|
+
|
|
1565
|
+
## Core Rules
|
|
1566
|
+
|
|
1567
|
+
- Each document should have one primary responsibility.
|
|
1568
|
+
- Each class of fact should have one canonical source.
|
|
1569
|
+
- Current implementation, reusable standards, and future proposals should be stored separately.
|
|
1570
|
+
- Generated helper output is never canonical truth.
|
|
1571
|
+
- Architecture docs describe structure and ownership; truth docs describe current product behavior.
|
|
1572
|
+
|
|
1573
|
+
## Truthmark Implications
|
|
1574
|
+
|
|
1575
|
+
- Truth Sync should extend mapped docs first, create an area-local doc second, and create a new area only as a last resort.
|
|
1576
|
+
- Weak routing produces weak truth maintenance.
|
|
1577
|
+
- Missing, stale, broad, overloaded, or unrouteable routing should trigger Truth Structure before more generic truth docs are created.
|
|
1578
|
+
`
|
|
1579
|
+
}
|
|
1580
|
+
];
|
|
1581
|
+
var renderDefaultStandards = (documents) => {
|
|
1582
|
+
const existingPaths = new Set(documents.map((document) => document.path));
|
|
1583
|
+
return DEFAULT_STANDARDS.filter((template) => !existingPaths.has(template.path));
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
// src/agents/workflow-manifest.ts
|
|
1587
|
+
var TRUTHMARK_WORKFLOW_MANIFEST = {
|
|
1588
|
+
"truthmark-sync": {
|
|
1589
|
+
id: "truthmark-sync",
|
|
1590
|
+
displayName: "Truthmark Sync",
|
|
1591
|
+
description: "Use automatically at finish-time after functional code changes, or explicit /truthmark-sync, $truthmark-sync, or /truthmark:sync. Skip docs-only, formatting-only, behavior-preserving renames, missing config, and no-code changes. Not for doc-first realization or manual topology design.",
|
|
1592
|
+
shortDescription: "Sync truth docs from functional code changes; skip docs-only/no-code changes",
|
|
1593
|
+
defaultPrompt: "Use $truthmark-sync after functional code changes; skip docs-only/no-code changes.",
|
|
1594
|
+
allowImplicitInvocation: true,
|
|
1595
|
+
positiveTriggers: [
|
|
1596
|
+
"functional code changed since last successful Truth Sync",
|
|
1597
|
+
"explicit /truthmark-sync, $truthmark-sync, or /truthmark:sync"
|
|
1598
|
+
],
|
|
1599
|
+
negativeTriggers: [
|
|
1600
|
+
"documentation-only change",
|
|
1601
|
+
"formatting-only change",
|
|
1602
|
+
"behavior-preserving rename",
|
|
1603
|
+
"missing Truthmark config",
|
|
1604
|
+
"no functional code changes"
|
|
1605
|
+
],
|
|
1606
|
+
forbiddenAdjacency: [
|
|
1607
|
+
"doc-first implementation belongs to Truth Realize",
|
|
1608
|
+
"manual topology design belongs to Truth Structure"
|
|
1609
|
+
],
|
|
1610
|
+
requiredGates: [
|
|
1611
|
+
"topology quality",
|
|
1612
|
+
"truth-doc ownership",
|
|
1613
|
+
"Product Decisions/Rationale preservation",
|
|
1614
|
+
"truth-doc shape repair when restructuring",
|
|
1615
|
+
"Evidence Gate"
|
|
1616
|
+
],
|
|
1617
|
+
allowedWrites: ["canonical truth docs", "truth routing files"],
|
|
1618
|
+
reportSections: [
|
|
1619
|
+
"Changed code reviewed",
|
|
1620
|
+
"Ownership reviewed",
|
|
1621
|
+
"Structure required",
|
|
1622
|
+
"Truth docs updated",
|
|
1623
|
+
"Truth docs split",
|
|
1624
|
+
"Evidence checked",
|
|
1625
|
+
"Notes"
|
|
1626
|
+
]
|
|
1627
|
+
},
|
|
1628
|
+
"truthmark-structure": {
|
|
1629
|
+
id: "truthmark-structure",
|
|
1630
|
+
displayName: "Truthmark Structure",
|
|
1631
|
+
description: "Use when routing or truth ownership is missing, stale, broad, overloaded, catch-all, unrouteable, mixed-owner, or needs split/repair. Not for documenting implemented behavior, syncing a code diff, or realizing docs into code.",
|
|
1632
|
+
shortDescription: "Design or repair Truthmark area routing",
|
|
1633
|
+
defaultPrompt: "Use $truthmark-structure to design or repair Truthmark area routing.",
|
|
1634
|
+
allowImplicitInvocation: false,
|
|
1635
|
+
positiveTriggers: [
|
|
1636
|
+
"split broad repository routing into bounded areas",
|
|
1637
|
+
"repair missing, stale, catch-all, unrouteable, or mixed-owner truth ownership"
|
|
1638
|
+
],
|
|
1639
|
+
negativeTriggers: [
|
|
1640
|
+
"document existing implemented behavior",
|
|
1641
|
+
"sync truth after a functional code diff",
|
|
1642
|
+
"realize truth docs into code"
|
|
1643
|
+
],
|
|
1644
|
+
forbiddenAdjacency: [
|
|
1645
|
+
"must not implement functional code",
|
|
1646
|
+
"must not patch mixed-owner docs as shape repair"
|
|
1647
|
+
],
|
|
1648
|
+
requiredGates: [
|
|
1649
|
+
"truth-doc ownership",
|
|
1650
|
+
"Product Decisions/Rationale preservation",
|
|
1651
|
+
"truth-doc shape repair when restructuring",
|
|
1652
|
+
"Evidence Gate"
|
|
1653
|
+
],
|
|
1654
|
+
allowedWrites: ["truth routing files", "starter canonical truth docs"],
|
|
1655
|
+
reportSections: [
|
|
1656
|
+
"Topology reviewed",
|
|
1657
|
+
"Areas reviewed",
|
|
1658
|
+
"Routing updated",
|
|
1659
|
+
"Truth docs created",
|
|
1660
|
+
"Truth docs split",
|
|
1661
|
+
"Truth docs restructured",
|
|
1662
|
+
"Evidence checked",
|
|
1663
|
+
"Topology decisions",
|
|
1664
|
+
"Notes"
|
|
1665
|
+
]
|
|
1666
|
+
},
|
|
1667
|
+
"truthmark-document": {
|
|
1668
|
+
id: "truthmark-document",
|
|
1669
|
+
displayName: "Truthmark Document",
|
|
1670
|
+
description: "Use when the user asks to document existing implemented behavior, or Sync, Check, or Structure finds implemented behavior missing canonical truth. Not for functional-code changes, doc-first implementation, or topology repair that needs Structure.",
|
|
1671
|
+
shortDescription: "Document existing implemented behavior",
|
|
1672
|
+
defaultPrompt: "Use $truthmark-document to document existing implemented behavior.",
|
|
1673
|
+
allowImplicitInvocation: false,
|
|
1674
|
+
positiveTriggers: [
|
|
1675
|
+
"document existing implemented behavior",
|
|
1676
|
+
"handoff finds implemented behavior missing canonical truth"
|
|
1677
|
+
],
|
|
1678
|
+
negativeTriggers: [
|
|
1679
|
+
"functional-code change that requires Truth Sync",
|
|
1680
|
+
"doc-first implementation",
|
|
1681
|
+
"topology repair that needs Truth Structure"
|
|
1682
|
+
],
|
|
1683
|
+
forbiddenAdjacency: [
|
|
1684
|
+
"must not edit functional code",
|
|
1685
|
+
"must not repair mixed-owner docs in place"
|
|
1686
|
+
],
|
|
1687
|
+
requiredGates: [
|
|
1688
|
+
"truth-doc ownership",
|
|
1689
|
+
"Product Decisions/Rationale preservation",
|
|
1690
|
+
"Evidence Gate",
|
|
1691
|
+
"truth-doc shape repair when restructuring"
|
|
1692
|
+
],
|
|
1693
|
+
allowedWrites: ["canonical truth docs", "truth routing files"],
|
|
1694
|
+
reportSections: [
|
|
1695
|
+
"Implementation reviewed",
|
|
1696
|
+
"Ownership reviewed",
|
|
1697
|
+
"Structure required",
|
|
1698
|
+
"Truth docs created",
|
|
1699
|
+
"Truth docs updated",
|
|
1700
|
+
"Truth docs restructured",
|
|
1701
|
+
"Routing updated",
|
|
1702
|
+
"Evidence checked",
|
|
1703
|
+
"Notes"
|
|
1704
|
+
]
|
|
1705
|
+
},
|
|
1706
|
+
"truthmark-realize": {
|
|
1707
|
+
id: "truthmark-realize",
|
|
1708
|
+
displayName: "Truthmark Realize",
|
|
1709
|
+
description: "Use when the user explicitly asks to realize Truthmark truth docs into code, including /truthmark-realize, $truthmark-realize, or /truthmark:realize. Not for syncing docs after code changes, documenting existing code, topology repair, or truth audits.",
|
|
1710
|
+
shortDescription: "Realize truth docs into code",
|
|
1711
|
+
defaultPrompt: "Use $truthmark-realize to realize the updated truth docs into code.",
|
|
1712
|
+
allowImplicitInvocation: false,
|
|
1713
|
+
positiveTriggers: ["explicitly realize truth docs into functional code"],
|
|
1714
|
+
negativeTriggers: [
|
|
1715
|
+
"sync docs after code changes",
|
|
1716
|
+
"document existing implemented behavior",
|
|
1717
|
+
"topology repair",
|
|
1718
|
+
"truth audit"
|
|
1719
|
+
],
|
|
1720
|
+
forbiddenAdjacency: [
|
|
1721
|
+
"must not edit truth docs",
|
|
1722
|
+
"must not edit truth routing"
|
|
1723
|
+
],
|
|
1724
|
+
requiredGates: ["truth-doc ownership"],
|
|
1725
|
+
allowedWrites: ["functional code"],
|
|
1726
|
+
reportSections: ["Truth docs used", "Code updated", "Verification"]
|
|
1727
|
+
},
|
|
1728
|
+
"truthmark-check": {
|
|
1729
|
+
id: "truthmark-check",
|
|
1730
|
+
displayName: "Truthmark Check",
|
|
1731
|
+
description: "Use when the user asks to audit repository truth health, routing, ownership, or canonical docs. Not for normal lint/test/typecheck/code-review verification, finish-time Sync, or silently rewriting docs.",
|
|
1732
|
+
shortDescription: "Audit repository truth health",
|
|
1733
|
+
defaultPrompt: "Use $truthmark-check to audit repository truth health.",
|
|
1734
|
+
allowImplicitInvocation: false,
|
|
1735
|
+
positiveTriggers: [
|
|
1736
|
+
"audit repository truth health",
|
|
1737
|
+
"audit routing, ownership, or canonical docs"
|
|
1738
|
+
],
|
|
1739
|
+
negativeTriggers: [
|
|
1740
|
+
"normal lint/test/typecheck verification",
|
|
1741
|
+
"code review",
|
|
1742
|
+
"finish-time Truth Sync"
|
|
1743
|
+
],
|
|
1744
|
+
forbiddenAdjacency: [
|
|
1745
|
+
"must not replace ordinary verification",
|
|
1746
|
+
"must not silently rewrite docs"
|
|
1747
|
+
],
|
|
1748
|
+
requiredGates: ["audit Evidence Gate"],
|
|
1749
|
+
allowedWrites: ["none by default"],
|
|
1750
|
+
reportSections: [
|
|
1751
|
+
"Files reviewed",
|
|
1752
|
+
"Issues found",
|
|
1753
|
+
"Fixes suggested",
|
|
1754
|
+
"Evidence checked",
|
|
1755
|
+
"Validation"
|
|
1756
|
+
]
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
var TRUTHMARK_WORKFLOW_IDS = Object.keys(
|
|
1760
|
+
TRUTHMARK_WORKFLOW_MANIFEST
|
|
1761
|
+
);
|
|
1762
|
+
var getTruthmarkWorkflow = (id) => {
|
|
1763
|
+
return TRUTHMARK_WORKFLOW_MANIFEST[id];
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
// src/agents/truth-check.ts
|
|
1767
|
+
var renderMarkdownExample = (content) => {
|
|
1768
|
+
return ["```md", content, "```"].join("\n");
|
|
1769
|
+
};
|
|
1770
|
+
var TRUTH_CHECK_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-check; Codex /truthmark-check or $truthmark-check; Claude Code /truthmark-check; GitHub Copilot /truthmark-check; Gemini CLI /truthmark:check.";
|
|
1771
|
+
var renderTruthCheckReportExample = (config = defaultAgentConfig()) => {
|
|
1772
|
+
const rootRouteIndex = config.docs.routing.rootIndex;
|
|
1773
|
+
return `Truth Check: completed
|
|
1774
|
+
|
|
1775
|
+
Files reviewed:
|
|
1776
|
+
- ${rootRouteIndex}
|
|
1777
|
+
|
|
1778
|
+
Issues found:
|
|
1779
|
+
- none
|
|
1780
|
+
|
|
1781
|
+
Fixes suggested:
|
|
1782
|
+
- none
|
|
1783
|
+
|
|
1784
|
+
${renderAuditEvidenceCheckedSection([
|
|
1785
|
+
{
|
|
1786
|
+
finding: "The root route index is present and maps repository truth owners.",
|
|
1787
|
+
evidence: [".truthmark/config.yml:1", `${rootRouteIndex}:1`],
|
|
1788
|
+
suggestedFix: "none",
|
|
1789
|
+
confidence: "high"
|
|
1790
|
+
}
|
|
1791
|
+
])}
|
|
1792
|
+
|
|
1793
|
+
Validation:
|
|
1794
|
+
- truthmark check`;
|
|
1033
1795
|
};
|
|
1034
1796
|
var renderTruthCheckSkillBody = (config = defaultAgentConfig()) => {
|
|
1797
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1035
1798
|
return `---
|
|
1036
1799
|
name: truthmark-check
|
|
1037
|
-
description:
|
|
1800
|
+
description: ${workflow.description}
|
|
1038
1801
|
argument-hint: Optional area, doc path, or audit focus
|
|
1039
1802
|
user-invocable: true
|
|
1040
1803
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1048,22 +1811,115 @@ Invocations: ${TRUTH_CHECK_EXPLICIT_INVOCATIONS}
|
|
|
1048
1811
|
|
|
1049
1812
|
Truth Check is agent-led:
|
|
1050
1813
|
|
|
1051
|
-
- inspect .truthmark/config.yml,
|
|
1814
|
+
- inspect .truthmark/config.yml, ${config.docs.routing.rootIndex}, canonical docs, and relevant implementation directly
|
|
1052
1815
|
- ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1053
1816
|
- inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/
|
|
1054
1817
|
- check that current docs describe current code rather than historical plans
|
|
1055
|
-
- check that docs
|
|
1818
|
+
- check that ${config.docs.routing.rootIndex} routes code surfaces to canonical truth docs
|
|
1819
|
+
- check for broad, catch-all, index-like, or mixed-owner truth docs and report them as topology issues requiring Truth Structure
|
|
1056
1820
|
- check that canonical behavior docs keep active Product Decisions and Rationale sections
|
|
1057
1821
|
- optionally run truthmark check when local tooling is available
|
|
1058
1822
|
- must not require the truthmark binary; direct inspection is always valid
|
|
1059
1823
|
- report issues and suggested fixes without silently rewriting unrelated files
|
|
1824
|
+
- if follow-up docs edits are needed for mixed-owner docs, run or recommend Truth Structure before editing
|
|
1825
|
+
${renderAuditEvidenceGateSection()}
|
|
1060
1826
|
|
|
1061
1827
|
${renderHierarchySummary(config)}
|
|
1062
1828
|
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1063
1829
|
|
|
1064
1830
|
Report completion in this shape:
|
|
1065
1831
|
|
|
1066
|
-
${
|
|
1832
|
+
${renderMarkdownExample(renderTruthCheckReportExample(config))}`;
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
// src/agents/truth-document.ts
|
|
1836
|
+
var renderMarkdownExample2 = (content) => {
|
|
1837
|
+
return ["```md", content, "```"].join("\n");
|
|
1838
|
+
};
|
|
1839
|
+
var TRUTH_DOCUMENT_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-document; Codex /truthmark-document or $truthmark-document; Claude Code /truthmark-document; GitHub Copilot /truthmark-document; Gemini CLI /truthmark:document.";
|
|
1840
|
+
var renderTruthDocumentReportExample = (config = defaultAgentConfig()) => {
|
|
1841
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
1842
|
+
return `Truth Document: completed
|
|
1843
|
+
|
|
1844
|
+
Implementation reviewed:
|
|
1845
|
+
- src/routing/area-resolver.ts
|
|
1846
|
+
|
|
1847
|
+
Truth docs created:
|
|
1848
|
+
- ${truthDocsRoot}/contracts.md
|
|
1849
|
+
|
|
1850
|
+
Truth docs updated:
|
|
1851
|
+
- ${truthDocsRoot}/check-diagnostics.md
|
|
1852
|
+
|
|
1853
|
+
Truth docs restructured:
|
|
1854
|
+
- ${truthDocsRoot}/check-diagnostics.md
|
|
1855
|
+
|
|
1856
|
+
Routing updated:
|
|
1857
|
+
- ${config.docs.routing.rootIndex}
|
|
1858
|
+
|
|
1859
|
+
${renderClaimEvidenceCheckedSection([
|
|
1860
|
+
{
|
|
1861
|
+
claim: "Route resolution behavior is documented in the contracts truth doc.",
|
|
1862
|
+
evidence: [
|
|
1863
|
+
"src/routing/area-resolver.ts:14",
|
|
1864
|
+
`${config.docs.routing.rootIndex}:9`
|
|
1865
|
+
],
|
|
1866
|
+
result: "supported"
|
|
1867
|
+
}
|
|
1868
|
+
])}
|
|
1869
|
+
|
|
1870
|
+
Notes:
|
|
1871
|
+
- Documented routing and behavior from route handlers and tests.`;
|
|
1872
|
+
};
|
|
1873
|
+
var renderTruthDocumentSkillBody = (config = defaultAgentConfig()) => {
|
|
1874
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
1875
|
+
return `---
|
|
1876
|
+
name: truthmark-document
|
|
1877
|
+
description: ${workflow.description}
|
|
1878
|
+
argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document
|
|
1879
|
+
user-invocable: true
|
|
1880
|
+
truthmark-version: ${TRUTHMARK_VERSION}
|
|
1881
|
+
---
|
|
1882
|
+
|
|
1883
|
+
# Truthmark Document
|
|
1884
|
+
|
|
1885
|
+
Use this skill to document existing implemented behavior when no functional-code changes are required for the task.
|
|
1886
|
+
Invocations: ${TRUTH_DOCUMENT_EXPLICIT_INVOCATIONS}
|
|
1887
|
+
|
|
1888
|
+
Truth Document is manual and implementation-first:
|
|
1889
|
+
|
|
1890
|
+
- run only when the user explicitly asks to generate or update truth docs for existing behavior, or when Truth Sync, Truth Check, or Truth Structure reports implemented behavior that lacks canonical truth docs
|
|
1891
|
+
- inspect .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, existing canonical docs, implementation code, and tests directly
|
|
1892
|
+
- ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1893
|
+
- document current implemented behavior; do not invent future behavior or planned endpoints
|
|
1894
|
+
- may write canonical truth docs and ${config.docs.routing.rootIndex} or relevant child route files only
|
|
1895
|
+
- must not write functional code
|
|
1896
|
+
- when routing is missing, stale, broad, overloaded, catch-all, or cannot map the behavior to a bounded truth owner, run Truth Structure first when routing repair is safe and in scope
|
|
1897
|
+
- block and recommend Truth Structure when routing repair is unsafe, ambiguous, or outside the task boundary
|
|
1898
|
+
- keep feature README.md files as indexes rather than truth-document targets
|
|
1899
|
+
- create or update bounded leaf truth docs when behavior does not fit an existing leaf doc
|
|
1900
|
+
- keep behavior truth docs behavior-oriented, not endpoint-oriented, unless the endpoint itself is the behavior boundary
|
|
1901
|
+
- keep API endpoint details in the nearest contract truth doc when such a doc owns the API contract
|
|
1902
|
+
- preserve unrelated authored content
|
|
1903
|
+
${renderTruthDocOwnershipGateSection(
|
|
1904
|
+
"the implemented behavior and candidate truth docs",
|
|
1905
|
+
"if the target doc is broad, mixed-owner, index-like, or the documented behavior spans independent owners, run Truth Structure first when safe and in scope; otherwise block and recommend Truth Structure"
|
|
1906
|
+
)}
|
|
1907
|
+
${TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS}
|
|
1908
|
+
${renderRouteFirstEvidenceGateSection(
|
|
1909
|
+
"the documented behavior",
|
|
1910
|
+
"if no truth doc changed, report why current truth was already sufficient or why documentation was blocked"
|
|
1911
|
+
)}
|
|
1912
|
+
${REPOSITORY_INTELLIGENCE_INSTRUCTIONS}
|
|
1913
|
+
${FEATURE_DOC_TEMPLATE_INSTRUCTIONS}
|
|
1914
|
+
${renderTruthDocRestructureGateSection(
|
|
1915
|
+
"Truth Document may restructure only truth docs for the implemented behavior being documented."
|
|
1916
|
+
)}
|
|
1917
|
+
${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
|
|
1918
|
+
${renderHierarchySummary(config)}
|
|
1919
|
+
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1920
|
+
|
|
1921
|
+
Report completion in this shape:
|
|
1922
|
+
${renderMarkdownExample2(renderTruthDocumentReportExample(config))}`;
|
|
1067
1923
|
};
|
|
1068
1924
|
|
|
1069
1925
|
// src/agents/truth-structure.ts
|
|
@@ -1071,27 +1927,41 @@ var renderMarkdownExample3 = (content) => {
|
|
|
1071
1927
|
return ["```md", content, "```"].join("\n");
|
|
1072
1928
|
};
|
|
1073
1929
|
var TRUTH_STRUCTURE_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-structure; Codex /truthmark-structure or $truthmark-structure; Claude Code /truthmark-structure; GitHub Copilot /truthmark-structure; Gemini CLI /truthmark:structure.";
|
|
1074
|
-
var renderTruthStructureReportExample = () => {
|
|
1930
|
+
var renderTruthStructureReportExample = (config = defaultAgentConfig()) => {
|
|
1931
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
1075
1932
|
return `Truth Structure: completed
|
|
1076
1933
|
Topology reviewed:
|
|
1077
1934
|
- controllers: src/auth/**
|
|
1078
|
-
- docs root:
|
|
1079
|
-
- route files: docs
|
|
1935
|
+
- docs root: ${truthDocsRoot}
|
|
1936
|
+
- route files: ${config.docs.routing.rootIndex}
|
|
1080
1937
|
Areas reviewed:
|
|
1081
1938
|
- src/auth/**
|
|
1082
1939
|
Routing updated:
|
|
1083
|
-
- docs
|
|
1940
|
+
- ${config.docs.routing.rootIndex}
|
|
1084
1941
|
Truth docs created:
|
|
1085
|
-
-
|
|
1942
|
+
- ${truthDocsRoot}/authentication/session.md
|
|
1943
|
+
Truth docs split:
|
|
1944
|
+
- ${truthDocsRoot}/authentication/README.md -> ${truthDocsRoot}/authentication/session.md
|
|
1945
|
+
Truth docs restructured:
|
|
1946
|
+
- ${truthDocsRoot}/authentication/README.md
|
|
1947
|
+
${renderClaimEvidenceCheckedSection([
|
|
1948
|
+
{
|
|
1949
|
+
claim: "Session behavior belongs to a dedicated Authentication truth owner.",
|
|
1950
|
+
evidence: ["src/auth/**", `${config.docs.routing.rootIndex}:7`],
|
|
1951
|
+
result: "supported"
|
|
1952
|
+
}
|
|
1953
|
+
])}
|
|
1086
1954
|
Topology decisions:
|
|
1087
1955
|
- Added an Authentication area because session behavior has a distinct code surface and truth owner.
|
|
1088
1956
|
Notes:
|
|
1089
1957
|
- Added an Authentication area for session behavior.`;
|
|
1090
1958
|
};
|
|
1091
1959
|
var renderTruthStructureSkillBody = (config = defaultAgentConfig()) => {
|
|
1960
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
1961
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1092
1962
|
return `---
|
|
1093
1963
|
name: truthmark-structure
|
|
1094
|
-
description:
|
|
1964
|
+
description: ${workflow.description}
|
|
1095
1965
|
argument-hint: Optional area, directory, or routing concern
|
|
1096
1966
|
user-invocable: true
|
|
1097
1967
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1100,61 +1970,219 @@ truthmark-version: ${TRUTHMARK_VERSION}
|
|
|
1100
1970
|
Use this skill to design or repair Truthmark area structure.
|
|
1101
1971
|
Invocations: ${TRUTH_STRUCTURE_EXPLICIT_INVOCATIONS}
|
|
1102
1972
|
Truth Structure is agent-native:
|
|
1103
|
-
- inspect repository layout, current docs, .truthmark/config.yml,
|
|
1973
|
+
- inspect repository layout, current docs, .truthmark/config.yml, ${config.docs.routing.rootIndex}, and relevant code directly
|
|
1104
1974
|
- ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1105
1975
|
- inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/
|
|
1106
1976
|
- define areas by product or behavior ownership, not by mechanical directory mirroring
|
|
1107
|
-
- create or repair docs
|
|
1977
|
+
- create or repair ${config.docs.routing.rootIndex}
|
|
1108
1978
|
- create starter truth docs when useful and when they belong in the canonical current-truth surface
|
|
1109
1979
|
- Starter truth docs must use closed YAML frontmatter bounded by opening and closing --- lines; include status, doc_type, last_reviewed, and source_of_truth inside that frontmatter.
|
|
1110
1980
|
- Starter truth docs must include ## Product Decisions and ## Rationale sections.
|
|
1111
|
-
|
|
1981
|
+
${FEATURE_DOC_TEMPLATE_INSTRUCTIONS}
|
|
1982
|
+
- use ${truthDocsRoot}/**, docs/architecture/**, or docs/standards/** for current truth destinations
|
|
1112
1983
|
- use only canonical current-truth destinations for starter truth docs
|
|
1113
1984
|
- keep active Product Decisions and Rationale in the canonical doc that owns the behavior
|
|
1114
1985
|
- preserve unrelated authored content
|
|
1115
1986
|
## Topology Governance
|
|
1116
|
-
Truth Structure owns documentation topology. Do not depend on humans to manually organize ${
|
|
1987
|
+
Truth Structure owns documentation topology. Do not depend on humans to manually organize ${truthDocsRoot}. Treat the configured truth root as a managed semantic root.
|
|
1117
1988
|
Inspect controllers, routes, handlers, services, packages, tests, existing truth docs, and route files; infer product and domain ownership from behavior boundaries, not from mechanical directory mirroring.
|
|
1118
|
-
When topology pressure exists, repair structure before creating or extending
|
|
1989
|
+
When topology pressure exists, repair structure before creating or extending truth docs.
|
|
1990
|
+
${renderTruthDocOwnershipGateSection(
|
|
1991
|
+
"candidate route owners and current truth docs",
|
|
1992
|
+
"if a truth doc mixes independent owners, route ownership is broad, or a split is required for bounded ownership, split and reroute into bounded truth docs when safe; otherwise block with manual-review files"
|
|
1993
|
+
)}
|
|
1994
|
+
${TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS}
|
|
1119
1995
|
Topology pressure signals:
|
|
1120
1996
|
- one area maps broad code such as src/**, app/**, server/**, services/**, or packages/**
|
|
1121
1997
|
- one area maps multiple unrelated controllers, route groups, services, or bounded contexts
|
|
1122
1998
|
- one truth doc owns unrelated behaviors or unrelated endpoint families
|
|
1123
|
-
- the configured
|
|
1999
|
+
- the configured truth root has many direct non-index docs
|
|
1124
2000
|
- a changed controller, route, or service cannot map to a specific behavior doc
|
|
1125
|
-
- Truth Sync would need to create a new generic
|
|
2001
|
+
- Truth Sync would need to create a new generic truth doc because routing is too broad
|
|
1126
2002
|
- endpoint or controller names reveal domains missing from ${config.docs.routing.areaFilesRoot}/**
|
|
1127
2003
|
Use these review thresholds as guidance:
|
|
1128
|
-
- more than 10 direct
|
|
2004
|
+
- more than 10 direct truth docs in one folder
|
|
1129
2005
|
- more than 15 leaf areas in one child route file
|
|
1130
2006
|
- more than 8 truth docs mapped to one area
|
|
1131
2007
|
- more than 5 controllers mapped through one catch-all area
|
|
1132
2008
|
Repair rules:
|
|
1133
|
-
- split broad catch-all areas into behavior-owned child route files
|
|
2009
|
+
- split broad, overloaded, or catch-all areas into behavior-owned child route files
|
|
2010
|
+
- split mixed-owner truth docs into bounded owner docs before adding new behavior claims
|
|
1134
2011
|
- create route files under ${config.docs.routing.areaFilesRoot}/ when a product/domain boundary is clear
|
|
1135
|
-
- create
|
|
2012
|
+
- create behavior truth docs under the configured truth root only when behavior lacks a current doc
|
|
1136
2013
|
- README.md files are indexes, not Truth Sync targets
|
|
1137
|
-
- prefer bounded leaf truth docs at <
|
|
1138
|
-
- keep
|
|
2014
|
+
- prefer bounded leaf truth docs at <truth-root>/<domain>/<behavior>.md
|
|
2015
|
+
- keep behavior truth docs behavior-oriented, not endpoint-oriented
|
|
1139
2016
|
- keep API endpoint details in the nearest contract truth doc when such a doc exists
|
|
1140
2017
|
- update routing so future Truth Sync can target small docs
|
|
1141
2018
|
- preserve existing authored docs; move or rewrite only when needed to remove ambiguity
|
|
2019
|
+
- report Truth docs split when one broad or mixed-owner truth doc becomes multiple bounded docs
|
|
2020
|
+
${renderTruthDocRestructureGateSection(
|
|
2021
|
+
"Truth Structure may restructure broader routed docs when topology, ownership, or doc-shape repair is already in scope."
|
|
2022
|
+
)}
|
|
2023
|
+
${renderTopologyEvidenceGateSection()}
|
|
2024
|
+
${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
|
|
1142
2025
|
- Do not finish topology repair with routed canonical current-truth docs missing Product Decisions or Rationale sections.
|
|
1143
2026
|
- If an existing canonical doc lacks either section, add the missing heading beside Current Behavior with a concise current-state placeholder or active decision.
|
|
1144
2027
|
Portable fallback:
|
|
1145
2028
|
- If this skill surface is unavailable, perform the same workflow directly from committed repository files.
|
|
1146
2029
|
- Do not require the truthmark CLI.
|
|
1147
|
-
- Read .truthmark/config.yml,
|
|
2030
|
+
- Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, canonical docs, and representative implementation code.
|
|
1148
2031
|
- Use a subagent only when the host supports that pattern; otherwise perform the topology repair inline.
|
|
1149
2032
|
${renderHierarchySummary(config)}
|
|
1150
2033
|
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1151
2034
|
Report completion in this shape:
|
|
1152
|
-
${renderMarkdownExample3(renderTruthStructureReportExample())}`;
|
|
2035
|
+
${renderMarkdownExample3(renderTruthStructureReportExample(config))}`;
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
// src/sync/report.ts
|
|
2039
|
+
var renderBulletSection = (title, items) => {
|
|
2040
|
+
return `${title}:
|
|
2041
|
+
${items.map((item) => `- ${item}`).join("\n")}`;
|
|
2042
|
+
};
|
|
2043
|
+
var renderTruthSyncCompletedReport = (input) => {
|
|
2044
|
+
return [
|
|
2045
|
+
"Truth Sync: completed",
|
|
2046
|
+
renderBulletSection("Changed code reviewed", input.changedCode),
|
|
2047
|
+
renderBulletSection("Truth docs updated", input.truthDocsUpdated),
|
|
2048
|
+
renderClaimEvidenceCheckedSection(input.evidenceChecked),
|
|
2049
|
+
renderBulletSection("Notes", input.notes)
|
|
2050
|
+
].join("\n\n");
|
|
2051
|
+
};
|
|
2052
|
+
var renderTruthSyncBlockedReport = (input) => {
|
|
2053
|
+
const sections = [
|
|
2054
|
+
"Truth Sync: blocked",
|
|
2055
|
+
renderBulletSection("Reason", [input.reason])
|
|
2056
|
+
];
|
|
2057
|
+
if ((input.manualReviewFiles?.length ?? 0) > 0) {
|
|
2058
|
+
sections.push(renderBulletSection("Files requiring manual review", input.manualReviewFiles));
|
|
2059
|
+
}
|
|
2060
|
+
sections.push(renderBulletSection("Next action", [input.nextAction]));
|
|
2061
|
+
return [
|
|
2062
|
+
...sections
|
|
2063
|
+
].join("\n\n");
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/agents/truth-sync.ts
|
|
2067
|
+
var TRUTH_SYNC_EXPLICIT_INVOCATIONS = "OpenCode /skill truthmark-sync; Codex /truthmark-sync or $truthmark-sync; Claude Code /truthmark-sync; GitHub Copilot /truthmark-sync; Gemini CLI /truthmark:sync.";
|
|
2068
|
+
var renderMarkdownExample4 = (content) => {
|
|
2069
|
+
return ["```md", content, "```"].join("\n");
|
|
2070
|
+
};
|
|
2071
|
+
var renderTruthSyncWorkerPrompt = (config = defaultAgentConfig()) => {
|
|
2072
|
+
return `### Truth Sync Worker
|
|
2073
|
+
The parent provides the task focus and any repository context already gathered.
|
|
2074
|
+
Worker rules:
|
|
2075
|
+
- inspect relevant staged, unstaged, and untracked functional code directly
|
|
2076
|
+
- read .truthmark/config.yml, ${config.docs.routing.rootIndex}, and canonical truth docs directly
|
|
2077
|
+
- Code verification is parent-owned; report what was run or why it was not run
|
|
2078
|
+
- may write truth docs and ${config.docs.routing.rootIndex} only for Truth Sync alignment
|
|
2079
|
+
- must not rewrite functional code
|
|
2080
|
+
Return result in this shape:
|
|
2081
|
+
- status: completed | blocked
|
|
2082
|
+
- changedCodeReviewed: string[]
|
|
2083
|
+
- ownershipReviewed: string[]
|
|
2084
|
+
- structureRequired?: string[]
|
|
2085
|
+
- truthDocsUpdated: string[]
|
|
2086
|
+
- routingDocsUpdated: string[]
|
|
2087
|
+
- truthDocsSplit?: string[]
|
|
2088
|
+
- evidenceChecked: { claim: string; evidence: string[]; result: supported | narrowed | removed | blocked }[]
|
|
2089
|
+
- notes: string[]
|
|
2090
|
+
- blockedReason?: string
|
|
2091
|
+
- manualReviewFiles?: string[]`;
|
|
2092
|
+
};
|
|
2093
|
+
var renderTruthSyncSkillBody = (config = defaultAgentConfig()) => {
|
|
2094
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
2095
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
2096
|
+
return `---
|
|
2097
|
+
name: truthmark-sync
|
|
2098
|
+
description: ${workflow.description}
|
|
2099
|
+
argument-hint: Optional changed-code area, truth-doc area, or sync focus
|
|
2100
|
+
user-invocable: true
|
|
2101
|
+
truthmark-version: ${TRUTHMARK_VERSION}
|
|
2102
|
+
---
|
|
2103
|
+
|
|
2104
|
+
Use this skill automatically before finishing when functional code changed since the last successful Truth Sync. Also run it immediately when the user explicitly invokes Truth Sync.
|
|
2105
|
+
Invocations: ${TRUTH_SYNC_EXPLICIT_INVOCATIONS}
|
|
2106
|
+
Explicit invocation runs immediately. Later functional-code changes reopen the finish-time requirement, and an earlier explicit run satisfies the finish gate only if no later functional-code changes occur.
|
|
2107
|
+
Skip when changes are documentation-only, formatting-only, clearly behavior-preserving renames with no truth impact, when no Truthmark config exists yet, or when there are no functional code changes.
|
|
2108
|
+
Parent workflow:
|
|
2109
|
+
1. Inspect git status, staged changes, unstaged changes, and untracked files directly.
|
|
2110
|
+
2. Read .truthmark/config.yml, the configured root route index at ${config.docs.routing.rootIndex}, relevant child route files under ${config.docs.routing.areaFilesRoot}/, and relevant canonical docs.
|
|
2111
|
+
3. Identify functional-code changes and the nearest truth docs or routing repairs.
|
|
2112
|
+
4. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
2113
|
+
5. Code verification is parent-owned: follow repository instructions and task context, and report what ran or why it did not run.
|
|
2114
|
+
6. Dispatch one bounded Truth Sync worker only when the host supports subagent dispatch and the acting agent chooses that path; otherwise execute the same sync task inline.
|
|
2115
|
+
Topology quality gate:
|
|
2116
|
+
- before updating truth docs, verify the changed code resolves to a specific behavior-owned area and bounded truth owner
|
|
2117
|
+
- if routing is missing, stale, broad, overloaded, catch-all route only, or cannot map changed code to a bounded truth owner, do not create another generic truth doc
|
|
2118
|
+
- run Truth Structure before syncing when topology repair is safe and in scope
|
|
2119
|
+
- block and recommend Truth Structure when topology repair is unsafe, ambiguous, or outside the current task boundary
|
|
2120
|
+
- report the route files and changed code paths that require structure repair
|
|
2121
|
+
- README.md files are indexes, not Truth Sync targets
|
|
2122
|
+
- must not append behavior details to a README.md index
|
|
2123
|
+
- create or update a bounded leaf truth doc when behavior changes do not fit an existing leaf doc
|
|
2124
|
+
${renderTruthDocOwnershipGateSection(
|
|
2125
|
+
"changed functional files and impacted truth docs",
|
|
2126
|
+
"if an impacted doc is broad, mixed-owner, index-like, or the update spans independent behavior owners, run Truth Structure before syncing when safe and in scope; otherwise block and recommend Truth Structure"
|
|
2127
|
+
)}
|
|
2128
|
+
${TRUTH_DOC_DECISION_RATIONALE_PRESERVATION_INSTRUCTIONS}
|
|
2129
|
+
${FEATURE_DOC_TEMPLATE_INSTRUCTIONS}
|
|
2130
|
+
${renderTruthDocRestructureGateSection(
|
|
2131
|
+
"Truth Sync may restructure only truth docs impacted by the current functional-code change."
|
|
2132
|
+
)}
|
|
2133
|
+
${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
|
|
2134
|
+
${renderRouteFirstEvidenceGateSection(
|
|
2135
|
+
"changed functional files",
|
|
2136
|
+
"if no impacted doc changed, report why truth was already current or why sync was skipped"
|
|
2137
|
+
)}
|
|
2138
|
+
${REPOSITORY_INTELLIGENCE_INSTRUCTIONS}
|
|
2139
|
+
Optional validation tooling:
|
|
2140
|
+
- you may run truthmark check when local tooling is available
|
|
2141
|
+
- do not require the truthmark binary; direct checkout inspection is the canonical path
|
|
2142
|
+
- optional validation must not replace agent judgment about docs and routing
|
|
2143
|
+
- update Product Decisions and Rationale when a behavior change comes from a decision change
|
|
2144
|
+
${renderHierarchySummary(config)}
|
|
2145
|
+
${DECISION_TRUTH_INSTRUCTIONS}
|
|
2146
|
+
${renderTruthSyncWorkerPrompt(config)}
|
|
2147
|
+
Parent post-sync verification:
|
|
2148
|
+
- verify only truth docs and ${config.docs.routing.rootIndex} changed during sync
|
|
2149
|
+
- block on any unrelated diff caused by the sync step
|
|
2150
|
+
- block if functional code changed during sync
|
|
2151
|
+
- verify the worker report matches the required headings and sections
|
|
2152
|
+
- validate the final report against the structured Truth Sync report contract, including Claim, Evidence, and Result entries under Evidence checked
|
|
2153
|
+
- verify the updated docs correspond to the reviewed changed-code surface
|
|
2154
|
+
- verify the final report records ownership review, structure requirement, split, restructure, or blocked reason when the ownership gate fired
|
|
2155
|
+
- blocked outcomes must preserve the working tree as-is: no rollback, no post-block cleanup edits, and manual-review reporting of any remaining files
|
|
2156
|
+
Report completion in this shape:
|
|
2157
|
+
${renderMarkdownExample4(
|
|
2158
|
+
renderTruthSyncCompletedReport({
|
|
2159
|
+
changedCode: ["src/auth/session.ts"],
|
|
2160
|
+
truthDocsUpdated: [`${truthDocsRoot}/repository/overview.md`],
|
|
2161
|
+
evidenceChecked: [
|
|
2162
|
+
{
|
|
2163
|
+
claim: "Session timeout behavior is documented in the mapped repository truth doc.",
|
|
2164
|
+
evidence: ["src/auth/session.ts:12", `${config.docs.routing.rootIndex}:11`],
|
|
2165
|
+
result: "supported"
|
|
2166
|
+
}
|
|
2167
|
+
],
|
|
2168
|
+
notes: ["Updated session timeout behavior."]
|
|
2169
|
+
})
|
|
2170
|
+
)}
|
|
2171
|
+
Blocked report example:
|
|
2172
|
+
${renderMarkdownExample4(
|
|
2173
|
+
renderTruthSyncBlockedReport({
|
|
2174
|
+
reason: "routing repair is not allowed",
|
|
2175
|
+
manualReviewFiles: [config.docs.routing.rootIndex],
|
|
2176
|
+
nextAction: "update routing metadata and rerun Truth Sync"
|
|
2177
|
+
})
|
|
2178
|
+
)}`;
|
|
1153
2179
|
};
|
|
1154
2180
|
|
|
1155
2181
|
// src/templates/codex-skills.ts
|
|
1156
2182
|
var TRUTHMARK_STRUCTURE_SKILL_PATH = ".codex/skills/truthmark-structure/SKILL.md";
|
|
1157
2183
|
var TRUTHMARK_STRUCTURE_SKILL_METADATA_PATH = ".codex/skills/truthmark-structure/agents/openai.yaml";
|
|
2184
|
+
var TRUTHMARK_DOCUMENT_SKILL_PATH = ".codex/skills/truthmark-document/SKILL.md";
|
|
2185
|
+
var TRUTHMARK_DOCUMENT_SKILL_METADATA_PATH = ".codex/skills/truthmark-document/agents/openai.yaml";
|
|
1158
2186
|
var TRUTHMARK_SYNC_SKILL_PATH = ".codex/skills/truthmark-sync/SKILL.md";
|
|
1159
2187
|
var TRUTHMARK_SYNC_SKILL_METADATA_PATH = ".codex/skills/truthmark-sync/agents/openai.yaml";
|
|
1160
2188
|
var TRUTHMARK_REALIZE_SKILL_PATH = ".codex/skills/truthmark-realize/SKILL.md";
|
|
@@ -1162,10 +2190,12 @@ var TRUTHMARK_REALIZE_SKILL_METADATA_PATH = ".codex/skills/truthmark-realize/age
|
|
|
1162
2190
|
var TRUTHMARK_CHECK_SKILL_PATH = ".codex/skills/truthmark-check/SKILL.md";
|
|
1163
2191
|
var TRUTHMARK_CHECK_SKILL_METADATA_PATH = ".codex/skills/truthmark-check/agents/openai.yaml";
|
|
1164
2192
|
var TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH = ".gemini/commands/truthmark/structure.toml";
|
|
2193
|
+
var TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH = ".gemini/commands/truthmark/document.toml";
|
|
1165
2194
|
var TRUTHMARK_GEMINI_SYNC_COMMAND_PATH = ".gemini/commands/truthmark/sync.toml";
|
|
1166
2195
|
var TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH = ".gemini/commands/truthmark/realize.toml";
|
|
1167
2196
|
var TRUTHMARK_GEMINI_CHECK_COMMAND_PATH = ".gemini/commands/truthmark/check.toml";
|
|
1168
2197
|
var TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH = ".github/prompts/truthmark-structure.prompt.md";
|
|
2198
|
+
var TRUTHMARK_COPILOT_DOCUMENT_PROMPT_PATH = ".github/prompts/truthmark-document.prompt.md";
|
|
1169
2199
|
var TRUTHMARK_COPILOT_SYNC_PROMPT_PATH = ".github/prompts/truthmark-sync.prompt.md";
|
|
1170
2200
|
var TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH = ".github/prompts/truthmark-realize.prompt.md";
|
|
1171
2201
|
var TRUTHMARK_COPILOT_CHECK_PROMPT_PATH = ".github/prompts/truthmark-check.prompt.md";
|
|
@@ -1192,13 +2222,35 @@ var renderTruthmarkStructureLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1192
2222
|
return renderTruthStructureSkillBody(config);
|
|
1193
2223
|
};
|
|
1194
2224
|
var renderTruthmarkStructureSkillMetadata = () => {
|
|
2225
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
2226
|
+
return `interface:
|
|
2227
|
+
display_name: "${workflow.displayName}"
|
|
2228
|
+
short_description: "${workflow.shortDescription}"
|
|
2229
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
2230
|
+
|
|
2231
|
+
policy:
|
|
2232
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
2233
|
+
|
|
2234
|
+
truthmark:
|
|
2235
|
+
version: "${TRUTHMARK_VERSION}"
|
|
2236
|
+
refresh_command: "truthmark init"
|
|
2237
|
+
`;
|
|
2238
|
+
};
|
|
2239
|
+
var renderTruthmarkDocumentSkill = (config = defaultAgentConfig()) => {
|
|
2240
|
+
return renderTruthDocumentSkillBody(config);
|
|
2241
|
+
};
|
|
2242
|
+
var renderTruthmarkDocumentLocalSkill = (config = defaultAgentConfig()) => {
|
|
2243
|
+
return renderTruthDocumentSkillBody(config);
|
|
2244
|
+
};
|
|
2245
|
+
var renderTruthmarkDocumentSkillMetadata = () => {
|
|
2246
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
1195
2247
|
return `interface:
|
|
1196
|
-
display_name: "
|
|
1197
|
-
short_description: "
|
|
1198
|
-
default_prompt: "
|
|
2248
|
+
display_name: "${workflow.displayName}"
|
|
2249
|
+
short_description: "${workflow.shortDescription}"
|
|
2250
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1199
2251
|
|
|
1200
2252
|
policy:
|
|
1201
|
-
allow_implicit_invocation:
|
|
2253
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1202
2254
|
|
|
1203
2255
|
truthmark:
|
|
1204
2256
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1212,23 +2264,26 @@ var renderTruthmarkSyncLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1212
2264
|
return renderTruthSyncSkillBody(config);
|
|
1213
2265
|
};
|
|
1214
2266
|
var renderTruthmarkSyncSkillMetadata = () => {
|
|
2267
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
1215
2268
|
return `interface:
|
|
1216
|
-
display_name: "
|
|
1217
|
-
short_description: "
|
|
1218
|
-
default_prompt: "
|
|
2269
|
+
display_name: "${workflow.displayName}"
|
|
2270
|
+
short_description: "${workflow.shortDescription}"
|
|
2271
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1219
2272
|
|
|
1220
2273
|
policy:
|
|
1221
|
-
allow_implicit_invocation:
|
|
2274
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1222
2275
|
|
|
1223
2276
|
truthmark:
|
|
1224
2277
|
version: "${TRUTHMARK_VERSION}"
|
|
1225
2278
|
refresh_command: "truthmark init"
|
|
1226
2279
|
`;
|
|
1227
2280
|
};
|
|
1228
|
-
var renderTruthmarkRealizeSkillBody = () => {
|
|
2281
|
+
var renderTruthmarkRealizeSkillBody = (config = defaultAgentConfig()) => {
|
|
2282
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
2283
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1229
2284
|
return `---
|
|
1230
2285
|
name: truthmark-realize
|
|
1231
|
-
description:
|
|
2286
|
+
description: ${workflow.description}
|
|
1232
2287
|
argument-hint: Optional truth doc path, area, or desired code behavior to realize
|
|
1233
2288
|
user-invocable: true
|
|
1234
2289
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1248,13 +2303,18 @@ Truth Realize is doc-first:
|
|
|
1248
2303
|
|
|
1249
2304
|
Workflow:
|
|
1250
2305
|
|
|
1251
|
-
1. Read the updated truth docs named by the user, or infer the relevant docs from docs
|
|
1252
|
-
2. Read .truthmark/config.yml,
|
|
2306
|
+
1. Read the updated truth docs named by the user, or infer the relevant docs from ${config.docs.routing.rootIndex}.
|
|
2307
|
+
2. Read .truthmark/config.yml, ${config.docs.routing.rootIndex}, relevant child route files, tests, and the relevant functional code.
|
|
1253
2308
|
3. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1254
|
-
|
|
2309
|
+
${renderTruthDocOwnershipGateSection(
|
|
2310
|
+
"source truth docs before writing code",
|
|
2311
|
+
"if a source truth doc is broad, mixed-owner, index-like, unrouteable, stale, or conflicts with implementation evidence, block before writing code and recommend Truth Structure or Truth Document"
|
|
2312
|
+
)}
|
|
2313
|
+
4. Update functional code only so implementation matches bounded, current truth claims from the source docs.
|
|
1255
2314
|
5. Do not edit truth docs or truth routing while realizing those docs.
|
|
1256
2315
|
6. Run relevant tests for the changed code.
|
|
1257
2316
|
7. Report changed code files and verification steps.
|
|
2317
|
+
${renderHierarchySummary(config)}
|
|
1258
2318
|
|
|
1259
2319
|
Read and write boundaries:
|
|
1260
2320
|
|
|
@@ -1268,7 +2328,7 @@ Report completion in this shape:
|
|
|
1268
2328
|
Truth Realize: completed
|
|
1269
2329
|
|
|
1270
2330
|
Truth docs used:
|
|
1271
|
-
-
|
|
2331
|
+
- ${truthDocsRoot}/authentication/session-timeout.md
|
|
1272
2332
|
|
|
1273
2333
|
Code updated:
|
|
1274
2334
|
- src/auth/session.ts
|
|
@@ -1278,20 +2338,21 @@ Verification:
|
|
|
1278
2338
|
\`\`\`
|
|
1279
2339
|
`;
|
|
1280
2340
|
};
|
|
1281
|
-
var renderTruthmarkRealizeSkill = () => {
|
|
1282
|
-
return renderTruthmarkRealizeSkillBody();
|
|
2341
|
+
var renderTruthmarkRealizeSkill = (config = defaultAgentConfig()) => {
|
|
2342
|
+
return renderTruthmarkRealizeSkillBody(config);
|
|
1283
2343
|
};
|
|
1284
|
-
var renderTruthmarkRealizeLocalSkill = () => {
|
|
1285
|
-
return renderTruthmarkRealizeSkillBody();
|
|
2344
|
+
var renderTruthmarkRealizeLocalSkill = (config = defaultAgentConfig()) => {
|
|
2345
|
+
return renderTruthmarkRealizeSkillBody(config);
|
|
1286
2346
|
};
|
|
1287
2347
|
var renderTruthmarkRealizeSkillMetadata = () => {
|
|
2348
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1288
2349
|
return `interface:
|
|
1289
|
-
display_name: "
|
|
1290
|
-
short_description: "
|
|
1291
|
-
default_prompt: "
|
|
2350
|
+
display_name: "${workflow.displayName}"
|
|
2351
|
+
short_description: "${workflow.shortDescription}"
|
|
2352
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1292
2353
|
|
|
1293
2354
|
policy:
|
|
1294
|
-
allow_implicit_invocation:
|
|
2355
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1295
2356
|
|
|
1296
2357
|
truthmark:
|
|
1297
2358
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1305,13 +2366,14 @@ var renderTruthmarkCheckLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1305
2366
|
return renderTruthCheckSkillBody(config);
|
|
1306
2367
|
};
|
|
1307
2368
|
var renderTruthmarkCheckSkillMetadata = () => {
|
|
2369
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1308
2370
|
return `interface:
|
|
1309
|
-
display_name: "
|
|
1310
|
-
short_description: "
|
|
1311
|
-
default_prompt: "
|
|
2371
|
+
display_name: "${workflow.displayName}"
|
|
2372
|
+
short_description: "${workflow.shortDescription}"
|
|
2373
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1312
2374
|
|
|
1313
2375
|
policy:
|
|
1314
|
-
allow_implicit_invocation:
|
|
2376
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1315
2377
|
|
|
1316
2378
|
truthmark:
|
|
1317
2379
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1319,113 +2381,227 @@ truthmark:
|
|
|
1319
2381
|
`;
|
|
1320
2382
|
};
|
|
1321
2383
|
var renderTruthmarkGeminiStructureCommand = (config = defaultAgentConfig()) => {
|
|
2384
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1322
2385
|
return renderGeminiCommand(
|
|
1323
|
-
|
|
2386
|
+
workflow.description,
|
|
1324
2387
|
renderTruthStructureSkillBody(config)
|
|
1325
2388
|
);
|
|
1326
2389
|
};
|
|
2390
|
+
var renderTruthmarkGeminiDocumentCommand = (config = defaultAgentConfig()) => {
|
|
2391
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
2392
|
+
return renderGeminiCommand(
|
|
2393
|
+
workflow.description,
|
|
2394
|
+
renderTruthDocumentSkillBody(config)
|
|
2395
|
+
);
|
|
2396
|
+
};
|
|
1327
2397
|
var renderTruthmarkGeminiSyncCommand = (config = defaultAgentConfig()) => {
|
|
2398
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
1328
2399
|
return renderGeminiCommand(
|
|
1329
|
-
|
|
2400
|
+
workflow.description,
|
|
1330
2401
|
renderTruthSyncSkillBody(config)
|
|
1331
2402
|
);
|
|
1332
2403
|
};
|
|
1333
|
-
var renderTruthmarkGeminiRealizeCommand = () => {
|
|
2404
|
+
var renderTruthmarkGeminiRealizeCommand = (config = defaultAgentConfig()) => {
|
|
2405
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1334
2406
|
return renderGeminiCommand(
|
|
1335
|
-
|
|
1336
|
-
renderTruthmarkRealizeSkillBody()
|
|
2407
|
+
workflow.description,
|
|
2408
|
+
renderTruthmarkRealizeSkillBody(config)
|
|
1337
2409
|
);
|
|
1338
2410
|
};
|
|
1339
2411
|
var renderTruthmarkGeminiCheckCommand = (config = defaultAgentConfig()) => {
|
|
2412
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1340
2413
|
return renderGeminiCommand(
|
|
1341
|
-
|
|
2414
|
+
workflow.description,
|
|
1342
2415
|
renderTruthCheckSkillBody(config)
|
|
1343
2416
|
);
|
|
1344
2417
|
};
|
|
1345
2418
|
var renderTruthmarkCopilotStructurePrompt = (config = defaultAgentConfig()) => {
|
|
2419
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1346
2420
|
return renderCopilotPromptFile(
|
|
1347
|
-
|
|
2421
|
+
workflow.description,
|
|
1348
2422
|
renderTruthStructureSkillBody(config)
|
|
1349
2423
|
);
|
|
1350
2424
|
};
|
|
2425
|
+
var renderTruthmarkCopilotDocumentPrompt = (config = defaultAgentConfig()) => {
|
|
2426
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
2427
|
+
return renderCopilotPromptFile(
|
|
2428
|
+
workflow.description,
|
|
2429
|
+
renderTruthDocumentSkillBody(config)
|
|
2430
|
+
);
|
|
2431
|
+
};
|
|
1351
2432
|
var renderTruthmarkCopilotSyncPrompt = (config = defaultAgentConfig()) => {
|
|
2433
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
1352
2434
|
return renderCopilotPromptFile(
|
|
1353
|
-
|
|
2435
|
+
workflow.description,
|
|
1354
2436
|
renderTruthSyncSkillBody(config)
|
|
1355
2437
|
);
|
|
1356
2438
|
};
|
|
1357
|
-
var renderTruthmarkCopilotRealizePrompt = () => {
|
|
2439
|
+
var renderTruthmarkCopilotRealizePrompt = (config = defaultAgentConfig()) => {
|
|
2440
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1358
2441
|
return renderCopilotPromptFile(
|
|
1359
|
-
|
|
1360
|
-
renderTruthmarkRealizeSkillBody()
|
|
2442
|
+
workflow.description,
|
|
2443
|
+
renderTruthmarkRealizeSkillBody(config)
|
|
1361
2444
|
);
|
|
1362
2445
|
};
|
|
1363
2446
|
var renderTruthmarkCopilotCheckPrompt = (config = defaultAgentConfig()) => {
|
|
2447
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1364
2448
|
return renderCopilotPromptFile(
|
|
1365
|
-
|
|
2449
|
+
workflow.description,
|
|
1366
2450
|
renderTruthCheckSkillBody(config)
|
|
1367
2451
|
);
|
|
1368
2452
|
};
|
|
1369
2453
|
|
|
1370
|
-
// src/templates/
|
|
1371
|
-
var
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2454
|
+
// src/templates/generated-surfaces.ts
|
|
2455
|
+
var workflowSkillFiles = (basePath, config) => {
|
|
2456
|
+
const files = [
|
|
2457
|
+
{
|
|
2458
|
+
path: `${basePath}/truthmark-structure/SKILL.md`,
|
|
2459
|
+
content: renderTruthmarkStructureLocalSkill(config)
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
path: `${basePath}/truthmark-document/SKILL.md`,
|
|
2463
|
+
content: renderTruthmarkDocumentLocalSkill(config)
|
|
2464
|
+
},
|
|
2465
|
+
{
|
|
2466
|
+
path: `${basePath}/truthmark-sync/SKILL.md`,
|
|
2467
|
+
content: renderTruthmarkSyncLocalSkill(config)
|
|
2468
|
+
},
|
|
2469
|
+
{
|
|
2470
|
+
path: `${basePath}/truthmark-check/SKILL.md`,
|
|
2471
|
+
content: renderTruthmarkCheckLocalSkill(config)
|
|
2472
|
+
},
|
|
2473
|
+
{
|
|
2474
|
+
path: `${basePath}/truthmark-realize/SKILL.md`,
|
|
2475
|
+
content: renderTruthmarkRealizeLocalSkill(config)
|
|
2476
|
+
}
|
|
2477
|
+
];
|
|
2478
|
+
return files;
|
|
2479
|
+
};
|
|
2480
|
+
var codexFiles = (config) => {
|
|
2481
|
+
const files = [
|
|
2482
|
+
{
|
|
2483
|
+
path: TRUTHMARK_STRUCTURE_SKILL_PATH,
|
|
2484
|
+
content: renderTruthmarkStructureSkill(config)
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
path: TRUTHMARK_STRUCTURE_SKILL_METADATA_PATH,
|
|
2488
|
+
content: renderTruthmarkStructureSkillMetadata()
|
|
2489
|
+
},
|
|
2490
|
+
{
|
|
2491
|
+
path: TRUTHMARK_DOCUMENT_SKILL_PATH,
|
|
2492
|
+
content: renderTruthmarkDocumentSkill(config)
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
path: TRUTHMARK_DOCUMENT_SKILL_METADATA_PATH,
|
|
2496
|
+
content: renderTruthmarkDocumentSkillMetadata()
|
|
2497
|
+
},
|
|
2498
|
+
{
|
|
2499
|
+
path: TRUTHMARK_SYNC_SKILL_PATH,
|
|
2500
|
+
content: renderTruthmarkSyncSkill(config)
|
|
2501
|
+
},
|
|
2502
|
+
{
|
|
2503
|
+
path: TRUTHMARK_SYNC_SKILL_METADATA_PATH,
|
|
2504
|
+
content: renderTruthmarkSyncSkillMetadata()
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
path: TRUTHMARK_CHECK_SKILL_PATH,
|
|
2508
|
+
content: renderTruthmarkCheckSkill(config)
|
|
2509
|
+
},
|
|
2510
|
+
{
|
|
2511
|
+
path: TRUTHMARK_CHECK_SKILL_METADATA_PATH,
|
|
2512
|
+
content: renderTruthmarkCheckSkillMetadata()
|
|
2513
|
+
},
|
|
2514
|
+
{
|
|
2515
|
+
path: TRUTHMARK_REALIZE_SKILL_PATH,
|
|
2516
|
+
content: renderTruthmarkRealizeSkill(config)
|
|
2517
|
+
},
|
|
2518
|
+
{
|
|
2519
|
+
path: TRUTHMARK_REALIZE_SKILL_METADATA_PATH,
|
|
2520
|
+
content: renderTruthmarkRealizeSkillMetadata()
|
|
2521
|
+
}
|
|
2522
|
+
];
|
|
2523
|
+
return files;
|
|
2524
|
+
};
|
|
2525
|
+
var copilotFiles = (config, block) => {
|
|
2526
|
+
const files = [
|
|
2527
|
+
...instructionBlockFiles([".github/copilot-instructions.md"], block),
|
|
2528
|
+
{
|
|
2529
|
+
path: TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH,
|
|
2530
|
+
content: renderTruthmarkCopilotStructurePrompt(config)
|
|
2531
|
+
},
|
|
2532
|
+
{
|
|
2533
|
+
path: TRUTHMARK_COPILOT_DOCUMENT_PROMPT_PATH,
|
|
2534
|
+
content: renderTruthmarkCopilotDocumentPrompt(config)
|
|
2535
|
+
},
|
|
2536
|
+
{
|
|
2537
|
+
path: TRUTHMARK_COPILOT_SYNC_PROMPT_PATH,
|
|
2538
|
+
content: renderTruthmarkCopilotSyncPrompt(config)
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
path: TRUTHMARK_COPILOT_CHECK_PROMPT_PATH,
|
|
2542
|
+
content: renderTruthmarkCopilotCheckPrompt(config)
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
path: TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH,
|
|
2546
|
+
content: renderTruthmarkCopilotRealizePrompt(config)
|
|
2547
|
+
}
|
|
2548
|
+
];
|
|
2549
|
+
return files;
|
|
2550
|
+
};
|
|
2551
|
+
var instructionBlockFiles = (paths, block) => {
|
|
2552
|
+
return paths.map((path12) => ({
|
|
2553
|
+
path: path12,
|
|
2554
|
+
content: block,
|
|
2555
|
+
managedBlock: true
|
|
2556
|
+
}));
|
|
2557
|
+
};
|
|
2558
|
+
var filesForPlatform = (platform, config, block) => {
|
|
2559
|
+
switch (platform) {
|
|
2560
|
+
case "codex":
|
|
2561
|
+
return codexFiles(config);
|
|
2562
|
+
case "opencode":
|
|
2563
|
+
return workflowSkillFiles(".opencode/skills", config);
|
|
2564
|
+
case "claude-code":
|
|
2565
|
+
return [
|
|
2566
|
+
...instructionBlockFiles(["CLAUDE.md"], block),
|
|
2567
|
+
...workflowSkillFiles(".claude/skills", config)
|
|
2568
|
+
];
|
|
2569
|
+
case "github-copilot":
|
|
2570
|
+
return copilotFiles(config, block);
|
|
2571
|
+
case "gemini-cli":
|
|
2572
|
+
return [
|
|
2573
|
+
...instructionBlockFiles(["GEMINI.md"], block),
|
|
2574
|
+
{
|
|
2575
|
+
path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH,
|
|
2576
|
+
content: renderTruthmarkGeminiStructureCommand(config)
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
path: TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH,
|
|
2580
|
+
content: renderTruthmarkGeminiDocumentCommand(config)
|
|
2581
|
+
},
|
|
2582
|
+
{
|
|
2583
|
+
path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
|
|
2584
|
+
content: renderTruthmarkGeminiSyncCommand(config)
|
|
2585
|
+
},
|
|
2586
|
+
{
|
|
2587
|
+
path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
|
|
2588
|
+
content: renderTruthmarkGeminiCheckCommand(config)
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
|
|
2592
|
+
content: renderTruthmarkGeminiRealizeCommand(config)
|
|
2593
|
+
}
|
|
2594
|
+
];
|
|
1424
2595
|
}
|
|
1425
|
-
|
|
1426
|
-
var
|
|
1427
|
-
const
|
|
1428
|
-
|
|
2596
|
+
};
|
|
2597
|
+
var renderGeneratedSurfaces = (config, block = renderAgentsBlock(config)) => {
|
|
2598
|
+
const files = [
|
|
2599
|
+
...instructionBlockFiles(config.instructionTargets, block),
|
|
2600
|
+
...config.platforms.flatMap((platform) => filesForPlatform(platform, config, block))
|
|
2601
|
+
];
|
|
2602
|
+
return Array.from(new Map(files.map((file) => [file.path, file])).values()).sort(
|
|
2603
|
+
(left, right) => left.path.localeCompare(right.path)
|
|
2604
|
+
);
|
|
1429
2605
|
};
|
|
1430
2606
|
|
|
1431
2607
|
// src/init/init.ts
|
|
@@ -1472,26 +2648,39 @@ var removeTrailingManagedChunk = (preservedLines) => {
|
|
|
1472
2648
|
preservedLines.splice(startIndex);
|
|
1473
2649
|
}
|
|
1474
2650
|
};
|
|
2651
|
+
var normalizeLegacyInstructionPreamble = (content) => {
|
|
2652
|
+
return content.replaceAll(
|
|
2653
|
+
"Use that file as the primary repository instruction source for Codex.",
|
|
2654
|
+
"Use that file as the primary repository instruction source for this agent."
|
|
2655
|
+
).replaceAll("Codex-specific:", "Agent-specific:").replaceAll(
|
|
2656
|
+
"- Read `docs/README.md` for the canonical docs map.",
|
|
2657
|
+
"- Read `docs/README.md` only when choosing or updating canonical docs."
|
|
2658
|
+
).replaceAll(
|
|
2659
|
+
"- Use `docs/ai/agent-onboarding.md` for quick task routing.",
|
|
2660
|
+
"- Use `docs/ai/agent-onboarding.md` only when task routing is unclear or cross-area."
|
|
2661
|
+
);
|
|
2662
|
+
};
|
|
1475
2663
|
var upsertManagedBlock = (existingContent, block) => {
|
|
1476
2664
|
if (!existingContent || existingContent.trim().length === 0) {
|
|
1477
2665
|
return block;
|
|
1478
2666
|
}
|
|
2667
|
+
const normalizedExistingContent = normalizeLegacyInstructionPreamble(existingContent);
|
|
1479
2668
|
const startMarkerPattern = new RegExp(escapeRegExp(TRUTHMARK_BLOCK_START), "g");
|
|
1480
2669
|
const endMarkerPattern = new RegExp(escapeRegExp(TRUTHMARK_BLOCK_END), "g");
|
|
1481
2670
|
const managedBlockPattern = new RegExp(
|
|
1482
2671
|
`${escapeRegExp(TRUTHMARK_BLOCK_START)}[\\s\\S]*?${escapeRegExp(TRUTHMARK_BLOCK_END)}`,
|
|
1483
2672
|
"g"
|
|
1484
2673
|
);
|
|
1485
|
-
const completeBlocks =
|
|
1486
|
-
const startCount =
|
|
1487
|
-
const endCount =
|
|
2674
|
+
const completeBlocks = normalizedExistingContent.match(managedBlockPattern) ?? [];
|
|
2675
|
+
const startCount = normalizedExistingContent.match(startMarkerPattern)?.length ?? 0;
|
|
2676
|
+
const endCount = normalizedExistingContent.match(endMarkerPattern)?.length ?? 0;
|
|
1488
2677
|
if (startCount === 1 && endCount === 1 && completeBlocks.length === 1) {
|
|
1489
|
-
return
|
|
2678
|
+
return normalizedExistingContent.replace(managedBlockPattern, block);
|
|
1490
2679
|
}
|
|
1491
2680
|
const preservedLines = [];
|
|
1492
2681
|
let insideManagedBlock = false;
|
|
1493
2682
|
let managedLines = [];
|
|
1494
|
-
for (const line of
|
|
2683
|
+
for (const line of normalizedExistingContent.split("\n")) {
|
|
1495
2684
|
const trimmedLine = line.trim();
|
|
1496
2685
|
if (trimmedLine === TRUTHMARK_BLOCK_START) {
|
|
1497
2686
|
if (insideManagedBlock && !isManagedChunk(managedLines, 2)) {
|
|
@@ -1529,18 +2718,18 @@ var upsertManagedBlock = (existingContent, block) => {
|
|
|
1529
2718
|
|
|
1530
2719
|
${block}`;
|
|
1531
2720
|
};
|
|
1532
|
-
var writeManagedAgentsFile = async (rootDir,
|
|
2721
|
+
var writeManagedAgentsFile = async (rootDir, path12 = "AGENTS.md", block) => {
|
|
1533
2722
|
let existingContent = null;
|
|
1534
2723
|
try {
|
|
1535
|
-
existingContent = await
|
|
2724
|
+
existingContent = await fs7.readFile(resolveRepoPath(rootDir, path12), "utf8");
|
|
1536
2725
|
} catch (error) {
|
|
1537
2726
|
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
1538
2727
|
throw error;
|
|
1539
2728
|
}
|
|
1540
2729
|
}
|
|
1541
|
-
return writeRepoFile(rootDir,
|
|
2730
|
+
return writeRepoFile(rootDir, path12, upsertManagedBlock(existingContent, block));
|
|
1542
2731
|
};
|
|
1543
|
-
var diagnosticCategoryForPath = (filePath) => {
|
|
2732
|
+
var diagnosticCategoryForPath = (filePath, config) => {
|
|
1544
2733
|
if (filePath === "AGENTS.md") {
|
|
1545
2734
|
return "truth-sync";
|
|
1546
2735
|
}
|
|
@@ -1550,6 +2739,9 @@ var diagnosticCategoryForPath = (filePath) => {
|
|
|
1550
2739
|
if (filePath.startsWith(".codex/skills/truthmark-structure/")) {
|
|
1551
2740
|
return "truth-sync";
|
|
1552
2741
|
}
|
|
2742
|
+
if (filePath.startsWith(".codex/skills/truthmark-document/")) {
|
|
2743
|
+
return "truth-sync";
|
|
2744
|
+
}
|
|
1553
2745
|
if (filePath.startsWith(".codex/skills/truthmark-sync/")) {
|
|
1554
2746
|
return "truth-sync";
|
|
1555
2747
|
}
|
|
@@ -1565,162 +2757,30 @@ var diagnosticCategoryForPath = (filePath) => {
|
|
|
1565
2757
|
if (filePath.startsWith(".codex/skills/truthmark-check/")) {
|
|
1566
2758
|
return "truth-sync";
|
|
1567
2759
|
}
|
|
1568
|
-
if (filePath ===
|
|
2760
|
+
if (filePath === config.docs.routing.rootIndex) {
|
|
1569
2761
|
return "authority";
|
|
1570
2762
|
}
|
|
1571
2763
|
return "config";
|
|
1572
2764
|
};
|
|
1573
|
-
var
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
path: `${basePath}/truthmark-structure/SKILL.md`,
|
|
1577
|
-
content: renderTruthmarkStructureLocalSkill(config)
|
|
1578
|
-
},
|
|
1579
|
-
{
|
|
1580
|
-
path: `${basePath}/truthmark-sync/SKILL.md`,
|
|
1581
|
-
content: renderTruthmarkSyncLocalSkill(config)
|
|
1582
|
-
},
|
|
1583
|
-
{
|
|
1584
|
-
path: `${basePath}/truthmark-check/SKILL.md`,
|
|
1585
|
-
content: renderTruthmarkCheckLocalSkill(config)
|
|
1586
|
-
}
|
|
1587
|
-
];
|
|
1588
|
-
if (config.realization.enabled) {
|
|
1589
|
-
files.push({
|
|
1590
|
-
path: `${basePath}/truthmark-realize/SKILL.md`,
|
|
1591
|
-
content: renderTruthmarkRealizeLocalSkill()
|
|
1592
|
-
});
|
|
2765
|
+
var writePlatformFile = async (rootDir, file) => {
|
|
2766
|
+
if (file.managedBlock) {
|
|
2767
|
+
return writeManagedAgentsFile(rootDir, file.path, file.content);
|
|
1593
2768
|
}
|
|
1594
|
-
return
|
|
2769
|
+
return writeRepoFile(rootDir, file.path, file.content);
|
|
1595
2770
|
};
|
|
1596
|
-
var
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
path
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
path
|
|
1604
|
-
content: renderTruthmarkStructureSkillMetadata()
|
|
1605
|
-
},
|
|
1606
|
-
{
|
|
1607
|
-
path: TRUTHMARK_SYNC_SKILL_PATH,
|
|
1608
|
-
content: renderTruthmarkSyncSkill(config)
|
|
1609
|
-
},
|
|
1610
|
-
{
|
|
1611
|
-
path: TRUTHMARK_SYNC_SKILL_METADATA_PATH,
|
|
1612
|
-
content: renderTruthmarkSyncSkillMetadata()
|
|
1613
|
-
},
|
|
1614
|
-
{
|
|
1615
|
-
path: TRUTHMARK_CHECK_SKILL_PATH,
|
|
1616
|
-
content: renderTruthmarkCheckSkill(config)
|
|
1617
|
-
},
|
|
1618
|
-
{
|
|
1619
|
-
path: TRUTHMARK_CHECK_SKILL_METADATA_PATH,
|
|
1620
|
-
content: renderTruthmarkCheckSkillMetadata()
|
|
1621
|
-
}
|
|
1622
|
-
];
|
|
1623
|
-
if (config.realization.enabled) {
|
|
1624
|
-
files.push(
|
|
1625
|
-
{
|
|
1626
|
-
path: TRUTHMARK_REALIZE_SKILL_PATH,
|
|
1627
|
-
content: renderTruthmarkRealizeSkill()
|
|
1628
|
-
},
|
|
1629
|
-
{
|
|
1630
|
-
path: TRUTHMARK_REALIZE_SKILL_METADATA_PATH,
|
|
1631
|
-
content: renderTruthmarkRealizeSkillMetadata()
|
|
1632
|
-
}
|
|
1633
|
-
);
|
|
2771
|
+
var messageForWriteResult = (result) => {
|
|
2772
|
+
switch (result.status) {
|
|
2773
|
+
case "created":
|
|
2774
|
+
return `Created ${result.path}.`;
|
|
2775
|
+
case "updated":
|
|
2776
|
+
return `Updated ${result.path}.`;
|
|
2777
|
+
case "unchanged":
|
|
2778
|
+
return `Unchanged ${result.path}.`;
|
|
1634
2779
|
}
|
|
1635
|
-
return files;
|
|
1636
2780
|
};
|
|
1637
|
-
var
|
|
1638
|
-
const files = [
|
|
1639
|
-
...instructionBlockFiles([".github/copilot-instructions.md"], block),
|
|
1640
|
-
{
|
|
1641
|
-
path: TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH,
|
|
1642
|
-
content: renderTruthmarkCopilotStructurePrompt(config)
|
|
1643
|
-
},
|
|
1644
|
-
{
|
|
1645
|
-
path: TRUTHMARK_COPILOT_SYNC_PROMPT_PATH,
|
|
1646
|
-
content: renderTruthmarkCopilotSyncPrompt(config)
|
|
1647
|
-
},
|
|
1648
|
-
{
|
|
1649
|
-
path: TRUTHMARK_COPILOT_CHECK_PROMPT_PATH,
|
|
1650
|
-
content: renderTruthmarkCopilotCheckPrompt(config)
|
|
1651
|
-
}
|
|
1652
|
-
];
|
|
1653
|
-
if (config.realization.enabled) {
|
|
1654
|
-
files.push({
|
|
1655
|
-
path: TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH,
|
|
1656
|
-
content: renderTruthmarkCopilotRealizePrompt()
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
return files;
|
|
1660
|
-
};
|
|
1661
|
-
var instructionBlockFiles = (paths, block) => {
|
|
1662
|
-
return paths.map((path4) => ({
|
|
1663
|
-
path: path4,
|
|
1664
|
-
content: block,
|
|
1665
|
-
managedBlock: true
|
|
1666
|
-
}));
|
|
1667
|
-
};
|
|
1668
|
-
var filesForPlatform = (platform, config, block) => {
|
|
1669
|
-
switch (platform) {
|
|
1670
|
-
case "codex":
|
|
1671
|
-
return codexFiles(config);
|
|
1672
|
-
case "opencode":
|
|
1673
|
-
return workflowSkillFiles(".opencode/skills", config);
|
|
1674
|
-
case "claude-code":
|
|
1675
|
-
return [
|
|
1676
|
-
...instructionBlockFiles(["CLAUDE.md"], block),
|
|
1677
|
-
...workflowSkillFiles(".claude/skills", config)
|
|
1678
|
-
];
|
|
1679
|
-
case "github-copilot":
|
|
1680
|
-
return copilotFiles(config, block);
|
|
1681
|
-
case "gemini-cli":
|
|
1682
|
-
return [
|
|
1683
|
-
...instructionBlockFiles(["GEMINI.md"], block),
|
|
1684
|
-
{
|
|
1685
|
-
path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH,
|
|
1686
|
-
content: renderTruthmarkGeminiStructureCommand(config)
|
|
1687
|
-
},
|
|
1688
|
-
{
|
|
1689
|
-
path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
|
|
1690
|
-
content: renderTruthmarkGeminiSyncCommand(config)
|
|
1691
|
-
},
|
|
1692
|
-
{
|
|
1693
|
-
path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
|
|
1694
|
-
content: renderTruthmarkGeminiCheckCommand(config)
|
|
1695
|
-
},
|
|
1696
|
-
...config.realization.enabled ? [
|
|
1697
|
-
{
|
|
1698
|
-
path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
|
|
1699
|
-
content: renderTruthmarkGeminiRealizeCommand()
|
|
1700
|
-
}
|
|
1701
|
-
] : []
|
|
1702
|
-
];
|
|
1703
|
-
}
|
|
1704
|
-
};
|
|
1705
|
-
var writePlatformFile = async (rootDir, file) => {
|
|
1706
|
-
if (file.managedBlock) {
|
|
1707
|
-
return writeManagedAgentsFile(rootDir, file.path, file.content);
|
|
1708
|
-
}
|
|
1709
|
-
return writeRepoFile(rootDir, file.path, file.content);
|
|
1710
|
-
};
|
|
1711
|
-
var messageForWriteResult = (result) => {
|
|
1712
|
-
switch (result.status) {
|
|
1713
|
-
case "created":
|
|
1714
|
-
return `Created ${result.path}.`;
|
|
1715
|
-
case "updated":
|
|
1716
|
-
return `Updated ${result.path}.`;
|
|
1717
|
-
case "unchanged":
|
|
1718
|
-
return `Unchanged ${result.path}.`;
|
|
1719
|
-
}
|
|
1720
|
-
};
|
|
1721
|
-
var writeDiagnostics = (results) => {
|
|
2781
|
+
var writeDiagnostics = (results, config) => {
|
|
1722
2782
|
return results.map((result) => ({
|
|
1723
|
-
category: diagnosticCategoryForPath(result.path),
|
|
2783
|
+
category: diagnosticCategoryForPath(result.path, config),
|
|
1724
2784
|
severity: "action",
|
|
1725
2785
|
message: messageForWriteResult(result),
|
|
1726
2786
|
file: result.path
|
|
@@ -1749,26 +2809,19 @@ var runInit = async (cwd) => {
|
|
|
1749
2809
|
for (const template of defaultStandards) {
|
|
1750
2810
|
results.push(await ensureRepoFile(rootDir, template.path, template.content));
|
|
1751
2811
|
}
|
|
1752
|
-
results.push(await ensureRepoFile(rootDir, "TRUTHMARK.md", renderTruthmarkTemplate()));
|
|
1753
2812
|
const config = loadedConfig.config;
|
|
1754
2813
|
results.push(...await scaffoldHierarchy(rootDir, config));
|
|
1755
2814
|
const migrationDiagnostics = await detectHierarchyMigrationDiagnostics(rootDir, config);
|
|
1756
2815
|
const block = renderAgentsBlock(config);
|
|
1757
|
-
const platformFiles =
|
|
1758
|
-
|
|
1759
|
-
...config.platforms.flatMap((platform) => filesForPlatform(platform, config, block))
|
|
1760
|
-
];
|
|
1761
|
-
const uniquePlatformFiles = Array.from(
|
|
1762
|
-
new Map(platformFiles.map((file) => [file.path, file])).values()
|
|
1763
|
-
).sort((left, right) => left.path.localeCompare(right.path));
|
|
1764
|
-
for (const file of uniquePlatformFiles) {
|
|
2816
|
+
const platformFiles = renderGeneratedSurfaces(config, block);
|
|
2817
|
+
for (const file of platformFiles) {
|
|
1765
2818
|
results.push(await writePlatformFile(rootDir, file));
|
|
1766
2819
|
}
|
|
1767
2820
|
const changedResults = results.filter((result) => result.status !== "unchanged");
|
|
1768
2821
|
return {
|
|
1769
2822
|
command: "init",
|
|
1770
2823
|
summary: changedResults.length > 0 ? "Initialized or updated the Truthmark repository scaffold." : "Truthmark repository scaffold is already up to date.",
|
|
1771
|
-
diagnostics: [...writeDiagnostics(results), ...migrationDiagnostics],
|
|
2824
|
+
diagnostics: [...writeDiagnostics(results, config), ...migrationDiagnostics],
|
|
1772
2825
|
data: {
|
|
1773
2826
|
repositoryRoot: repository.repositoryRoot,
|
|
1774
2827
|
worktreePath: repository.worktreePath,
|
|
@@ -1780,7 +2833,7 @@ var runInit = async (cwd) => {
|
|
|
1780
2833
|
};
|
|
1781
2834
|
|
|
1782
2835
|
// src/checks/branch-scope.ts
|
|
1783
|
-
import
|
|
2836
|
+
import fs8 from "fs/promises";
|
|
1784
2837
|
import fg2 from "fast-glob";
|
|
1785
2838
|
|
|
1786
2839
|
// src/markdown/hash.ts
|
|
@@ -1798,7 +2851,7 @@ var BranchScopeFileError = class extends Error {
|
|
|
1798
2851
|
this.file = file;
|
|
1799
2852
|
}
|
|
1800
2853
|
};
|
|
1801
|
-
var RELEVANT_BRANCH_SCOPE_FILES = [".truthmark/config.yml"
|
|
2854
|
+
var RELEVANT_BRANCH_SCOPE_FILES = [".truthmark/config.yml"];
|
|
1802
2855
|
var toBranchIdentity = (branchName, headSha) => {
|
|
1803
2856
|
if (branchName && headSha) {
|
|
1804
2857
|
return `${branchName}@${headSha}`;
|
|
@@ -1835,7 +2888,7 @@ var getBranchScopeData = async (cwd) => {
|
|
|
1835
2888
|
}
|
|
1836
2889
|
for (const relativePath of [...relevantFiles].sort()) {
|
|
1837
2890
|
try {
|
|
1838
|
-
const source = await
|
|
2891
|
+
const source = await fs8.readFile(resolveWorktreePath(repository, relativePath), "utf8");
|
|
1839
2892
|
relevantFileHashes[relativePath] = hashText(source);
|
|
1840
2893
|
} catch (error) {
|
|
1841
2894
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -1852,14 +2905,14 @@ var getBranchScopeData = async (cwd) => {
|
|
|
1852
2905
|
};
|
|
1853
2906
|
|
|
1854
2907
|
// src/checks/authority.ts
|
|
1855
|
-
import
|
|
2908
|
+
import fs9 from "fs/promises";
|
|
1856
2909
|
import fg3 from "fast-glob";
|
|
1857
2910
|
var looksLikeGlob = (pattern) => {
|
|
1858
2911
|
return /[*?[\]{}()!+@]/u.test(pattern);
|
|
1859
2912
|
};
|
|
1860
2913
|
var pathExists = async (absolutePath) => {
|
|
1861
2914
|
try {
|
|
1862
|
-
await
|
|
2915
|
+
await fs9.stat(absolutePath);
|
|
1863
2916
|
return true;
|
|
1864
2917
|
} catch (error) {
|
|
1865
2918
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -1948,7 +3001,7 @@ var checkAuthority = async (rootDir, config) => {
|
|
|
1948
3001
|
};
|
|
1949
3002
|
|
|
1950
3003
|
// src/checks/frontmatter.ts
|
|
1951
|
-
import
|
|
3004
|
+
import fs10 from "fs/promises";
|
|
1952
3005
|
|
|
1953
3006
|
// src/markdown/parse.ts
|
|
1954
3007
|
import matter from "gray-matter";
|
|
@@ -1988,15 +3041,19 @@ var parseMarkdownDocument = (source) => {
|
|
|
1988
3041
|
};
|
|
1989
3042
|
|
|
1990
3043
|
// src/checks/frontmatter.ts
|
|
1991
|
-
var
|
|
3044
|
+
var isTruthDocumentKind2 = (value) => TRUTH_DOCUMENT_KINDS.includes(value);
|
|
3045
|
+
var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntries = []) => {
|
|
1992
3046
|
const diagnostics = [];
|
|
3047
|
+
const truthDocumentMap = new Map(
|
|
3048
|
+
truthDocumentEntries.map((entry) => [entry.path, entry])
|
|
3049
|
+
);
|
|
1993
3050
|
for (const markdownPath of markdownPaths) {
|
|
1994
3051
|
if (!markdownPath.endsWith(".md")) {
|
|
1995
3052
|
continue;
|
|
1996
3053
|
}
|
|
1997
3054
|
const absolutePath = resolveRepoPath(rootDir, markdownPath);
|
|
1998
3055
|
await assertRepoContainment(rootDir, absolutePath);
|
|
1999
|
-
const source = await
|
|
3056
|
+
const source = await fs10.readFile(absolutePath, "utf8");
|
|
2000
3057
|
let document;
|
|
2001
3058
|
try {
|
|
2002
3059
|
document = parseMarkdownDocument(source);
|
|
@@ -2029,16 +3086,37 @@ var checkFrontmatter = async (rootDir, config, markdownPaths) => {
|
|
|
2029
3086
|
});
|
|
2030
3087
|
}
|
|
2031
3088
|
}
|
|
3089
|
+
const routedTruthDocument = truthDocumentMap.get(markdownPath);
|
|
3090
|
+
const truthKind = document.frontmatter.truth_kind;
|
|
3091
|
+
if (truthKind !== void 0) {
|
|
3092
|
+
if (typeof truthKind !== "string" || !isTruthDocumentKind2(truthKind)) {
|
|
3093
|
+
diagnostics.push({
|
|
3094
|
+
category: "frontmatter",
|
|
3095
|
+
severity: "error",
|
|
3096
|
+
message: `Frontmatter truth_kind must be one of ${TRUTH_DOCUMENT_KINDS.join(", ")}.`,
|
|
3097
|
+
file: markdownPath
|
|
3098
|
+
});
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
if (routedTruthDocument && routedTruthDocument.kindSource !== "defaulted" && truthKind !== routedTruthDocument.kind) {
|
|
3102
|
+
diagnostics.push({
|
|
3103
|
+
category: "frontmatter",
|
|
3104
|
+
severity: "error",
|
|
3105
|
+
message: `Frontmatter truth_kind ${truthKind} must match routed truth kind ${routedTruthDocument.kind}.`,
|
|
3106
|
+
file: markdownPath
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
2032
3110
|
}
|
|
2033
3111
|
return diagnostics;
|
|
2034
3112
|
};
|
|
2035
3113
|
|
|
2036
3114
|
// src/checks/links.ts
|
|
2037
|
-
import
|
|
2038
|
-
import
|
|
3115
|
+
import fs11 from "fs/promises";
|
|
3116
|
+
import path4 from "path";
|
|
2039
3117
|
var pathExists2 = async (absolutePath) => {
|
|
2040
3118
|
try {
|
|
2041
|
-
await
|
|
3119
|
+
await fs11.stat(absolutePath);
|
|
2042
3120
|
return true;
|
|
2043
3121
|
} catch (error) {
|
|
2044
3122
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2054,7 +3132,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
|
|
|
2054
3132
|
continue;
|
|
2055
3133
|
}
|
|
2056
3134
|
const absolutePath = resolveRepoPath(rootDir, markdownPath);
|
|
2057
|
-
const source = await
|
|
3135
|
+
const source = await fs11.readFile(absolutePath, "utf8");
|
|
2058
3136
|
let document;
|
|
2059
3137
|
try {
|
|
2060
3138
|
document = parseMarkdownDocument(source);
|
|
@@ -2069,7 +3147,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
|
|
|
2069
3147
|
if (targetPath.length === 0) {
|
|
2070
3148
|
continue;
|
|
2071
3149
|
}
|
|
2072
|
-
const absoluteTarget =
|
|
3150
|
+
const absoluteTarget = path4.resolve(path4.dirname(absolutePath), targetPath);
|
|
2073
3151
|
const relativeTarget = toRepoRelativePath(rootDir, absoluteTarget);
|
|
2074
3152
|
try {
|
|
2075
3153
|
await assertRepoContainment(rootDir, absoluteTarget);
|
|
@@ -2096,120 +3174,14 @@ var checkLinks = async (rootDir, markdownPaths) => {
|
|
|
2096
3174
|
};
|
|
2097
3175
|
|
|
2098
3176
|
// src/checks/areas.ts
|
|
2099
|
-
import
|
|
3177
|
+
import fs13 from "fs/promises";
|
|
2100
3178
|
import fg5 from "fast-glob";
|
|
2101
3179
|
import micromatch3 from "micromatch";
|
|
2102
3180
|
|
|
2103
3181
|
// src/routing/area-resolver.ts
|
|
2104
|
-
import
|
|
3182
|
+
import fs12 from "fs/promises";
|
|
2105
3183
|
import fg4 from "fast-glob";
|
|
2106
3184
|
import micromatch from "micromatch";
|
|
2107
|
-
|
|
2108
|
-
// src/routing/areas.ts
|
|
2109
|
-
var slugify = (value) => {
|
|
2110
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2111
|
-
};
|
|
2112
|
-
var createAreaDiagnostic = (message, area) => {
|
|
2113
|
-
return {
|
|
2114
|
-
category: "area-index",
|
|
2115
|
-
severity: "error",
|
|
2116
|
-
message,
|
|
2117
|
-
area
|
|
2118
|
-
};
|
|
2119
|
-
};
|
|
2120
|
-
var parseListSection = (sectionLines) => {
|
|
2121
|
-
return sectionLines.map((line) => line.trim()).filter((line) => line.startsWith("- ")).map((line) => line.slice(2).trim()).filter((line) => line.length > 0);
|
|
2122
|
-
};
|
|
2123
|
-
var parseAreasMarkdown = (source) => {
|
|
2124
|
-
const lines = source.split("\n");
|
|
2125
|
-
const diagnostics = [];
|
|
2126
|
-
const areas = [];
|
|
2127
|
-
const truthDocumentReferences = [];
|
|
2128
|
-
const areaFileReferences = [];
|
|
2129
|
-
let areaIndex = 0;
|
|
2130
|
-
let currentAreaName = null;
|
|
2131
|
-
let currentSections = /* @__PURE__ */ new Map();
|
|
2132
|
-
let currentSectionName = null;
|
|
2133
|
-
const flushArea = () => {
|
|
2134
|
-
if (!currentAreaName) {
|
|
2135
|
-
return;
|
|
2136
|
-
}
|
|
2137
|
-
const truthDocuments = parseListSection(currentSections.get("Truth documents") ?? []);
|
|
2138
|
-
const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
|
|
2139
|
-
const codeSurface = parseListSection(currentSections.get("Code surface") ?? []);
|
|
2140
|
-
const updateTruthWhen = parseListSection(currentSections.get("Update truth when") ?? []);
|
|
2141
|
-
const areaKey = slugify(currentAreaName);
|
|
2142
|
-
const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
|
|
2143
|
-
const hasTruthDocuments = truthDocuments.length > 0;
|
|
2144
|
-
const hasAreaFiles = areaFiles.length > 0;
|
|
2145
|
-
areaIndex += 1;
|
|
2146
|
-
if (hasTruthDocuments) {
|
|
2147
|
-
truthDocumentReferences.push({
|
|
2148
|
-
id: areaId,
|
|
2149
|
-
name: currentAreaName,
|
|
2150
|
-
key: areaKey,
|
|
2151
|
-
truthDocuments
|
|
2152
|
-
});
|
|
2153
|
-
}
|
|
2154
|
-
if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
|
|
2155
|
-
diagnostics.push(
|
|
2156
|
-
createAreaDiagnostic(
|
|
2157
|
-
`Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
|
|
2158
|
-
currentAreaName
|
|
2159
|
-
)
|
|
2160
|
-
);
|
|
2161
|
-
} else if (hasAreaFiles) {
|
|
2162
|
-
areaFileReferences.push({
|
|
2163
|
-
id: areaId,
|
|
2164
|
-
name: currentAreaName,
|
|
2165
|
-
key: areaKey,
|
|
2166
|
-
areaFiles,
|
|
2167
|
-
codeSurface,
|
|
2168
|
-
updateTruthWhen
|
|
2169
|
-
});
|
|
2170
|
-
} else {
|
|
2171
|
-
areas.push({
|
|
2172
|
-
id: areaId,
|
|
2173
|
-
name: currentAreaName,
|
|
2174
|
-
key: areaKey,
|
|
2175
|
-
truthDocuments,
|
|
2176
|
-
codeSurface,
|
|
2177
|
-
updateTruthWhen
|
|
2178
|
-
});
|
|
2179
|
-
}
|
|
2180
|
-
currentAreaName = null;
|
|
2181
|
-
currentSections = /* @__PURE__ */ new Map();
|
|
2182
|
-
currentSectionName = null;
|
|
2183
|
-
};
|
|
2184
|
-
for (const line of lines) {
|
|
2185
|
-
const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
|
|
2186
|
-
if (areaHeadingMatch) {
|
|
2187
|
-
flushArea();
|
|
2188
|
-
currentAreaName = areaHeadingMatch[1]?.trim() ?? null;
|
|
2189
|
-
continue;
|
|
2190
|
-
}
|
|
2191
|
-
if (!currentAreaName) {
|
|
2192
|
-
continue;
|
|
2193
|
-
}
|
|
2194
|
-
if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(line.trim())) {
|
|
2195
|
-
currentSectionName = line.trim().slice(0, -1);
|
|
2196
|
-
currentSections.set(currentSectionName, []);
|
|
2197
|
-
continue;
|
|
2198
|
-
}
|
|
2199
|
-
if (currentSectionName) {
|
|
2200
|
-
currentSections.get(currentSectionName)?.push(line);
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
flushArea();
|
|
2204
|
-
return {
|
|
2205
|
-
areas,
|
|
2206
|
-
truthDocumentReferences,
|
|
2207
|
-
areaFileReferences,
|
|
2208
|
-
diagnostics
|
|
2209
|
-
};
|
|
2210
|
-
};
|
|
2211
|
-
|
|
2212
|
-
// src/routing/area-resolver.ts
|
|
2213
3185
|
var unique = (values) => {
|
|
2214
3186
|
return [...new Set(values)];
|
|
2215
3187
|
};
|
|
@@ -2258,7 +3230,7 @@ var ensureChildPath = async (rootDir, areaFilesRoot, filePath) => {
|
|
|
2258
3230
|
var readRouteFile = async (rootDir, filePath) => {
|
|
2259
3231
|
try {
|
|
2260
3232
|
return {
|
|
2261
|
-
source: await
|
|
3233
|
+
source: await fs12.readFile(resolveRepoPath(rootDir, filePath), "utf8"),
|
|
2262
3234
|
diagnostic: null
|
|
2263
3235
|
};
|
|
2264
3236
|
} catch (error) {
|
|
@@ -2292,7 +3264,9 @@ var resolveAreaRouting = async (rootDir, config) => {
|
|
|
2292
3264
|
diagnostics: [rootRead.diagnostic]
|
|
2293
3265
|
};
|
|
2294
3266
|
}
|
|
2295
|
-
const rootParsed = parseAreasMarkdown(rootRead.source ?? ""
|
|
3267
|
+
const rootParsed = parseAreasMarkdown(rootRead.source ?? "", {
|
|
3268
|
+
truthDocsRoot: config.truthDocsRoot
|
|
3269
|
+
});
|
|
2296
3270
|
diagnostics.push(
|
|
2297
3271
|
...rootParsed.diagnostics.map((diagnostic) => ({
|
|
2298
3272
|
...diagnostic,
|
|
@@ -2329,7 +3303,9 @@ var resolveAreaRouting = async (rootDir, config) => {
|
|
|
2329
3303
|
continue;
|
|
2330
3304
|
}
|
|
2331
3305
|
routeFiles.push(areaFile);
|
|
2332
|
-
const childParsed = parseAreasMarkdown(childRead.source ?? ""
|
|
3306
|
+
const childParsed = parseAreasMarkdown(childRead.source ?? "", {
|
|
3307
|
+
truthDocsRoot: config.truthDocsRoot
|
|
3308
|
+
});
|
|
2333
3309
|
diagnostics.push(
|
|
2334
3310
|
...childParsed.diagnostics.map((diagnostic) => ({
|
|
2335
3311
|
...diagnostic,
|
|
@@ -2558,7 +3534,7 @@ var looksLikeGlob2 = (pattern) => {
|
|
|
2558
3534
|
};
|
|
2559
3535
|
var pathExists3 = async (absolutePath) => {
|
|
2560
3536
|
try {
|
|
2561
|
-
await
|
|
3537
|
+
await fs13.stat(absolutePath);
|
|
2562
3538
|
return true;
|
|
2563
3539
|
} catch (error) {
|
|
2564
3540
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
@@ -2608,7 +3584,8 @@ var isBroadCodeSurface = (pattern) => {
|
|
|
2608
3584
|
var checkAreas = async (rootDir, config) => {
|
|
2609
3585
|
const routing = await resolveAreaRouting(rootDir, {
|
|
2610
3586
|
rootIndex: config.docs.routing.rootIndex,
|
|
2611
|
-
areaFilesRoot: config.docs.routing.areaFilesRoot
|
|
3587
|
+
areaFilesRoot: config.docs.routing.areaFilesRoot,
|
|
3588
|
+
truthDocsRoot: resolveTruthDocsRoot(config)
|
|
2612
3589
|
});
|
|
2613
3590
|
const discoveredCodeFiles = await fg5([...COVERAGE_SCAN_PATTERNS], {
|
|
2614
3591
|
cwd: rootDir,
|
|
@@ -2623,6 +3600,7 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2623
3600
|
const diagnostics = [...routing.diagnostics];
|
|
2624
3601
|
const truthDocumentPaths = [];
|
|
2625
3602
|
const seenTruthDocumentPaths = /* @__PURE__ */ new Set();
|
|
3603
|
+
const truthDocumentEntryMap = /* @__PURE__ */ new Map();
|
|
2626
3604
|
const areaCoverage = routing.areas.map((area) => ({
|
|
2627
3605
|
area,
|
|
2628
3606
|
valid: true,
|
|
@@ -2640,8 +3618,28 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2640
3618
|
const truthReferences = routing.truthDocumentReferences;
|
|
2641
3619
|
for (const area of truthReferences) {
|
|
2642
3620
|
let areaHasTruthDocumentErrors = false;
|
|
3621
|
+
const registerTruthDocumentEntry = (truthDocumentEntry) => {
|
|
3622
|
+
const existingEntry = truthDocumentEntryMap.get(truthDocumentEntry.path);
|
|
3623
|
+
if (existingEntry && existingEntry.kind !== truthDocumentEntry.kind) {
|
|
3624
|
+
diagnostics.push({
|
|
3625
|
+
category: "area-index",
|
|
3626
|
+
severity: "error",
|
|
3627
|
+
message: `Truth document ${truthDocumentEntry.path} is routed with conflicting kinds ${existingEntry.kind} and ${truthDocumentEntry.kind}.`,
|
|
3628
|
+
area: area.name,
|
|
3629
|
+
file: truthDocumentEntry.path
|
|
3630
|
+
});
|
|
3631
|
+
return false;
|
|
3632
|
+
}
|
|
3633
|
+
if (!existingEntry) {
|
|
3634
|
+
truthDocumentEntryMap.set(truthDocumentEntry.path, truthDocumentEntry);
|
|
3635
|
+
}
|
|
3636
|
+
return true;
|
|
3637
|
+
};
|
|
2643
3638
|
for (const truthDocument of area.truthDocuments) {
|
|
2644
3639
|
if (looksLikeGlob2(truthDocument)) {
|
|
3640
|
+
const routedGlobEntry = area.truthDocumentEntries.find(
|
|
3641
|
+
(entry) => entry.path === truthDocument
|
|
3642
|
+
);
|
|
2645
3643
|
const matches = (await fg5([truthDocument], { cwd: rootDir, onlyFiles: true })).sort();
|
|
2646
3644
|
if (matches.length === 0) {
|
|
2647
3645
|
diagnostics.push({
|
|
@@ -2673,6 +3671,12 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2673
3671
|
seenTruthDocumentPaths.add(match);
|
|
2674
3672
|
truthDocumentPaths.push(match);
|
|
2675
3673
|
}
|
|
3674
|
+
if (routedGlobEntry && !registerTruthDocumentEntry({
|
|
3675
|
+
...routedGlobEntry,
|
|
3676
|
+
path: match
|
|
3677
|
+
})) {
|
|
3678
|
+
areaHasTruthDocumentErrors = true;
|
|
3679
|
+
}
|
|
2676
3680
|
}
|
|
2677
3681
|
continue;
|
|
2678
3682
|
}
|
|
@@ -2706,6 +3710,10 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2706
3710
|
seenTruthDocumentPaths.add(truthDocument);
|
|
2707
3711
|
truthDocumentPaths.push(truthDocument);
|
|
2708
3712
|
}
|
|
3713
|
+
const routedEntry = area.truthDocumentEntries.find((entry) => entry.path === truthDocument);
|
|
3714
|
+
if (routedEntry && !registerTruthDocumentEntry(routedEntry)) {
|
|
3715
|
+
areaHasTruthDocumentErrors = true;
|
|
3716
|
+
}
|
|
2709
3717
|
}
|
|
2710
3718
|
if (areaHasTruthDocumentErrors) {
|
|
2711
3719
|
const matchingArea = areaCoverage.find(
|
|
@@ -2818,6 +3826,7 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2818
3826
|
return {
|
|
2819
3827
|
diagnostics,
|
|
2820
3828
|
truthDocumentPaths,
|
|
3829
|
+
truthDocumentEntries: [...truthDocumentEntryMap.values()],
|
|
2821
3830
|
routePrecision: {
|
|
2822
3831
|
leafAreaCount: routing.areas.length,
|
|
2823
3832
|
broadAreaCount
|
|
@@ -2827,38 +3836,99 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2827
3836
|
};
|
|
2828
3837
|
|
|
2829
3838
|
// src/checks/decisions.ts
|
|
2830
|
-
import
|
|
3839
|
+
import fs14 from "fs/promises";
|
|
2831
3840
|
import micromatch4 from "micromatch";
|
|
2832
|
-
var REQUIRED_DECISION_HEADINGS = ["Product Decisions", "Rationale"];
|
|
3841
|
+
var REQUIRED_DECISION_HEADINGS = ["Scope", "Product Decisions", "Rationale"];
|
|
3842
|
+
var isTruthDocumentKind3 = (value) => {
|
|
3843
|
+
return TRUTH_DOCUMENT_KINDS.includes(value);
|
|
3844
|
+
};
|
|
2833
3845
|
var escapeRegExp2 = (value) => {
|
|
2834
3846
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2835
3847
|
};
|
|
2836
3848
|
var hasHeading = (source, heading) => {
|
|
2837
3849
|
return new RegExp(`^#{2,3}\\s+${escapeRegExp2(heading)}\\s*$`, "mu").test(source);
|
|
2838
3850
|
};
|
|
3851
|
+
var kindSpecificHeadingMessages = (source, kind) => {
|
|
3852
|
+
if (kind === null) {
|
|
3853
|
+
return [];
|
|
3854
|
+
}
|
|
3855
|
+
if (kind === "behavior") {
|
|
3856
|
+
return hasHeading(source, "Current Behavior") ? [] : ["Current Behavior"];
|
|
3857
|
+
}
|
|
3858
|
+
if (kind === "contract") {
|
|
3859
|
+
const missingMessages = [];
|
|
3860
|
+
if (!hasHeading(source, "Contract Surface")) {
|
|
3861
|
+
missingMessages.push("Contract Surface");
|
|
3862
|
+
}
|
|
3863
|
+
if (!hasHeading(source, "Inputs") && !hasHeading(source, "Outputs") && !hasHeading(source, "Compatibility Rules")) {
|
|
3864
|
+
missingMessages.push("one of Inputs, Outputs, or Compatibility Rules");
|
|
3865
|
+
}
|
|
3866
|
+
return missingMessages;
|
|
3867
|
+
}
|
|
3868
|
+
if (kind === "architecture") {
|
|
3869
|
+
return hasHeading(source, "Boundaries") || hasHeading(source, "Components") ? [] : ["Boundaries or Components"];
|
|
3870
|
+
}
|
|
3871
|
+
if (kind === "workflow") {
|
|
3872
|
+
const missingMessages = [];
|
|
3873
|
+
if (!hasHeading(source, "Triggers")) {
|
|
3874
|
+
missingMessages.push("Triggers");
|
|
3875
|
+
}
|
|
3876
|
+
if (!hasHeading(source, "Execution Model")) {
|
|
3877
|
+
missingMessages.push("Execution Model");
|
|
3878
|
+
}
|
|
3879
|
+
return missingMessages;
|
|
3880
|
+
}
|
|
3881
|
+
if (kind === "operations") {
|
|
3882
|
+
return hasHeading(source, "Runtime Topology") || hasHeading(source, "Configuration") ? [] : ["Runtime Topology or Configuration"];
|
|
3883
|
+
}
|
|
3884
|
+
if (kind === "test-behavior") {
|
|
3885
|
+
const missingMessages = [];
|
|
3886
|
+
if (!hasHeading(source, "Execution Model")) {
|
|
3887
|
+
missingMessages.push("Execution Model");
|
|
3888
|
+
}
|
|
3889
|
+
if (!hasHeading(source, "Fixtures And Data Model") && !hasHeading(source, "Assertions And Invariants")) {
|
|
3890
|
+
missingMessages.push("Fixtures And Data Model or Assertions And Invariants");
|
|
3891
|
+
}
|
|
3892
|
+
return missingMessages;
|
|
3893
|
+
}
|
|
3894
|
+
return [];
|
|
3895
|
+
};
|
|
2839
3896
|
var decisionTruthGlobs = (config) => {
|
|
2840
3897
|
return [
|
|
2841
3898
|
config.docs.roots.architecture,
|
|
2842
|
-
config
|
|
3899
|
+
resolveTruthDocsRoot(config),
|
|
2843
3900
|
config.docs.roots.api
|
|
2844
3901
|
].filter((root) => Boolean(root)).map((root) => `${root}/**/*.md`);
|
|
2845
3902
|
};
|
|
2846
3903
|
var isDecisionTruthCandidate = (config, filePath) => {
|
|
2847
3904
|
return !filePath.endsWith("/README.md") && micromatch4.isMatch(filePath, decisionTruthGlobs(config));
|
|
2848
3905
|
};
|
|
2849
|
-
var checkDecisionSections = async (rootDir, config, markdownPaths) => {
|
|
3906
|
+
var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocumentEntries = []) => {
|
|
2850
3907
|
const diagnostics = [];
|
|
2851
|
-
const
|
|
3908
|
+
const truthDocumentMap = new Map(
|
|
3909
|
+
truthDocumentEntries.map((entry) => [entry.path, entry])
|
|
3910
|
+
);
|
|
3911
|
+
const candidatePaths = [...new Set(markdownPaths)].filter(
|
|
3912
|
+
(filePath) => truthDocumentMap.has(filePath) || isDecisionTruthCandidate(config, filePath)
|
|
3913
|
+
).sort();
|
|
2852
3914
|
for (const filePath of candidatePaths) {
|
|
2853
|
-
const source = await
|
|
2854
|
-
const
|
|
3915
|
+
const source = await fs14.readFile(resolveRepoPath(rootDir, filePath), "utf8");
|
|
3916
|
+
const document = parseMarkdownDocument(source);
|
|
3917
|
+
const routedTruthDocument = truthDocumentMap.get(filePath);
|
|
3918
|
+
const frontmatterTruthKind = typeof document.frontmatter.truth_kind === "string" ? document.frontmatter.truth_kind : null;
|
|
3919
|
+
const routedTruthKind = routedTruthDocument?.kindSource === "defaulted" ? null : routedTruthDocument?.kind;
|
|
3920
|
+
const truthKind = routedTruthKind ?? (frontmatterTruthKind && isTruthDocumentKind3(frontmatterTruthKind) ? frontmatterTruthKind : inferTruthDocumentKindFromPath(filePath));
|
|
3921
|
+
const missingHeadings = REQUIRED_DECISION_HEADINGS.filter(
|
|
3922
|
+
(heading) => !hasHeading(source, heading)
|
|
3923
|
+
);
|
|
3924
|
+
missingHeadings.push(...kindSpecificHeadingMessages(source, truthKind));
|
|
2855
3925
|
if (missingHeadings.length === 0) {
|
|
2856
3926
|
continue;
|
|
2857
3927
|
}
|
|
2858
3928
|
diagnostics.push({
|
|
2859
3929
|
category: "doc-structure",
|
|
2860
3930
|
severity: "review",
|
|
2861
|
-
message: `Canonical truth doc ${filePath} should include
|
|
3931
|
+
message: `Canonical truth doc ${filePath} should include ${missingHeadings.join(" and ")} section(s). Decisions should live beside current behavior, not in timestamped planning logs.`,
|
|
2862
3932
|
file: filePath
|
|
2863
3933
|
});
|
|
2864
3934
|
}
|
|
@@ -2866,160 +3936,15 @@ var checkDecisionSections = async (rootDir, config, markdownPaths) => {
|
|
|
2866
3936
|
};
|
|
2867
3937
|
|
|
2868
3938
|
// src/checks/generated-surfaces.ts
|
|
2869
|
-
import
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
{
|
|
2875
|
-
|
|
2876
|
-
content: renderTruthmarkStructureLocalSkill(config)
|
|
2877
|
-
},
|
|
2878
|
-
{
|
|
2879
|
-
path: `${basePath}/truthmark-sync/SKILL.md`,
|
|
2880
|
-
content: renderTruthmarkSyncLocalSkill(config)
|
|
2881
|
-
},
|
|
2882
|
-
{
|
|
2883
|
-
path: `${basePath}/truthmark-check/SKILL.md`,
|
|
2884
|
-
content: renderTruthmarkCheckLocalSkill(config)
|
|
3939
|
+
import fs15 from "fs/promises";
|
|
3940
|
+
var readOptionalFile = async (rootDir, filePath) => {
|
|
3941
|
+
try {
|
|
3942
|
+
return await fs15.readFile(resolveRepoPath(rootDir, filePath), "utf8");
|
|
3943
|
+
} catch (error) {
|
|
3944
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3945
|
+
return null;
|
|
2885
3946
|
}
|
|
2886
|
-
|
|
2887
|
-
if (config.realization.enabled) {
|
|
2888
|
-
files.push({
|
|
2889
|
-
path: `${basePath}/truthmark-realize/SKILL.md`,
|
|
2890
|
-
content: renderTruthmarkRealizeLocalSkill()
|
|
2891
|
-
});
|
|
2892
|
-
}
|
|
2893
|
-
return files;
|
|
2894
|
-
};
|
|
2895
|
-
var codexFiles2 = (config) => {
|
|
2896
|
-
const files = [
|
|
2897
|
-
{
|
|
2898
|
-
path: TRUTHMARK_STRUCTURE_SKILL_PATH,
|
|
2899
|
-
content: renderTruthmarkStructureSkill(config)
|
|
2900
|
-
},
|
|
2901
|
-
{
|
|
2902
|
-
path: TRUTHMARK_STRUCTURE_SKILL_METADATA_PATH,
|
|
2903
|
-
content: renderTruthmarkStructureSkillMetadata()
|
|
2904
|
-
},
|
|
2905
|
-
{
|
|
2906
|
-
path: TRUTHMARK_SYNC_SKILL_PATH,
|
|
2907
|
-
content: renderTruthmarkSyncSkill(config)
|
|
2908
|
-
},
|
|
2909
|
-
{
|
|
2910
|
-
path: TRUTHMARK_SYNC_SKILL_METADATA_PATH,
|
|
2911
|
-
content: renderTruthmarkSyncSkillMetadata()
|
|
2912
|
-
},
|
|
2913
|
-
{
|
|
2914
|
-
path: TRUTHMARK_CHECK_SKILL_PATH,
|
|
2915
|
-
content: renderTruthmarkCheckSkill(config)
|
|
2916
|
-
},
|
|
2917
|
-
{
|
|
2918
|
-
path: TRUTHMARK_CHECK_SKILL_METADATA_PATH,
|
|
2919
|
-
content: renderTruthmarkCheckSkillMetadata()
|
|
2920
|
-
}
|
|
2921
|
-
];
|
|
2922
|
-
if (config.realization.enabled) {
|
|
2923
|
-
files.push(
|
|
2924
|
-
{
|
|
2925
|
-
path: TRUTHMARK_REALIZE_SKILL_PATH,
|
|
2926
|
-
content: renderTruthmarkRealizeSkill()
|
|
2927
|
-
},
|
|
2928
|
-
{
|
|
2929
|
-
path: TRUTHMARK_REALIZE_SKILL_METADATA_PATH,
|
|
2930
|
-
content: renderTruthmarkRealizeSkillMetadata()
|
|
2931
|
-
}
|
|
2932
|
-
);
|
|
2933
|
-
}
|
|
2934
|
-
return files;
|
|
2935
|
-
};
|
|
2936
|
-
var copilotFiles2 = (config, block) => {
|
|
2937
|
-
const files = [
|
|
2938
|
-
...instructionBlockFiles2([".github/copilot-instructions.md"], block),
|
|
2939
|
-
{
|
|
2940
|
-
path: TRUTHMARK_COPILOT_STRUCTURE_PROMPT_PATH,
|
|
2941
|
-
content: renderTruthmarkCopilotStructurePrompt(config)
|
|
2942
|
-
},
|
|
2943
|
-
{
|
|
2944
|
-
path: TRUTHMARK_COPILOT_SYNC_PROMPT_PATH,
|
|
2945
|
-
content: renderTruthmarkCopilotSyncPrompt(config)
|
|
2946
|
-
},
|
|
2947
|
-
{
|
|
2948
|
-
path: TRUTHMARK_COPILOT_CHECK_PROMPT_PATH,
|
|
2949
|
-
content: renderTruthmarkCopilotCheckPrompt(config)
|
|
2950
|
-
}
|
|
2951
|
-
];
|
|
2952
|
-
if (config.realization.enabled) {
|
|
2953
|
-
files.push({
|
|
2954
|
-
path: TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH,
|
|
2955
|
-
content: renderTruthmarkCopilotRealizePrompt()
|
|
2956
|
-
});
|
|
2957
|
-
}
|
|
2958
|
-
return files;
|
|
2959
|
-
};
|
|
2960
|
-
var instructionBlockFiles2 = (paths, block) => {
|
|
2961
|
-
return paths.map((path4) => ({
|
|
2962
|
-
path: path4,
|
|
2963
|
-
content: block,
|
|
2964
|
-
managedBlock: true
|
|
2965
|
-
}));
|
|
2966
|
-
};
|
|
2967
|
-
var filesForPlatform2 = (platform, config, block) => {
|
|
2968
|
-
switch (platform) {
|
|
2969
|
-
case "codex":
|
|
2970
|
-
return codexFiles2(config);
|
|
2971
|
-
case "opencode":
|
|
2972
|
-
return workflowSkillFiles2(".opencode/skills", config);
|
|
2973
|
-
case "claude-code":
|
|
2974
|
-
return [
|
|
2975
|
-
...instructionBlockFiles2(["CLAUDE.md"], block),
|
|
2976
|
-
...workflowSkillFiles2(".claude/skills", config)
|
|
2977
|
-
];
|
|
2978
|
-
case "github-copilot":
|
|
2979
|
-
return copilotFiles2(config, block);
|
|
2980
|
-
case "gemini-cli":
|
|
2981
|
-
return [
|
|
2982
|
-
...instructionBlockFiles2(["GEMINI.md"], block),
|
|
2983
|
-
{
|
|
2984
|
-
path: TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH,
|
|
2985
|
-
content: renderTruthmarkGeminiStructureCommand(config)
|
|
2986
|
-
},
|
|
2987
|
-
{
|
|
2988
|
-
path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
|
|
2989
|
-
content: renderTruthmarkGeminiSyncCommand(config)
|
|
2990
|
-
},
|
|
2991
|
-
{
|
|
2992
|
-
path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
|
|
2993
|
-
content: renderTruthmarkGeminiCheckCommand(config)
|
|
2994
|
-
},
|
|
2995
|
-
...config.realization.enabled ? [
|
|
2996
|
-
{
|
|
2997
|
-
path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
|
|
2998
|
-
content: renderTruthmarkGeminiRealizeCommand()
|
|
2999
|
-
}
|
|
3000
|
-
] : []
|
|
3001
|
-
];
|
|
3002
|
-
}
|
|
3003
|
-
};
|
|
3004
|
-
var renderGeneratedSurfaces = (config, block = renderAgentsBlock(config)) => {
|
|
3005
|
-
const files = [
|
|
3006
|
-
...instructionBlockFiles2(config.instructionTargets, block),
|
|
3007
|
-
...config.platforms.flatMap((platform) => filesForPlatform2(platform, config, block))
|
|
3008
|
-
];
|
|
3009
|
-
return Array.from(new Map(files.map((file) => [file.path, file])).values()).sort(
|
|
3010
|
-
(left, right) => left.path.localeCompare(right.path)
|
|
3011
|
-
);
|
|
3012
|
-
};
|
|
3013
|
-
|
|
3014
|
-
// src/checks/generated-surfaces.ts
|
|
3015
|
-
var readOptionalFile = async (rootDir, filePath) => {
|
|
3016
|
-
try {
|
|
3017
|
-
return await fs14.readFile(resolveRepoPath(rootDir, filePath), "utf8");
|
|
3018
|
-
} catch (error) {
|
|
3019
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
3020
|
-
return null;
|
|
3021
|
-
}
|
|
3022
|
-
throw error;
|
|
3947
|
+
throw error;
|
|
3023
3948
|
}
|
|
3024
3949
|
};
|
|
3025
3950
|
var extractManagedBlock = (content) => {
|
|
@@ -3093,6 +4018,903 @@ var checkGeneratedSurfaces = async (rootDir, config) => {
|
|
|
3093
4018
|
return diagnostics;
|
|
3094
4019
|
};
|
|
3095
4020
|
|
|
4021
|
+
// src/impact/build.ts
|
|
4022
|
+
import path9 from "path";
|
|
4023
|
+
import micromatch6 from "micromatch";
|
|
4024
|
+
|
|
4025
|
+
// src/repo-index/build.ts
|
|
4026
|
+
import fs18 from "fs/promises";
|
|
4027
|
+
import path7 from "path";
|
|
4028
|
+
|
|
4029
|
+
// src/repo-index/file-tree.ts
|
|
4030
|
+
import fs16 from "fs/promises";
|
|
4031
|
+
import path5 from "path";
|
|
4032
|
+
import { execa as execa2 } from "execa";
|
|
4033
|
+
import fg6 from "fast-glob";
|
|
4034
|
+
import matter2 from "gray-matter";
|
|
4035
|
+
import micromatch5 from "micromatch";
|
|
4036
|
+
var languageByExtension = /* @__PURE__ */ new Map([
|
|
4037
|
+
[".ts", "typescript"],
|
|
4038
|
+
[".tsx", "typescript"],
|
|
4039
|
+
[".js", "javascript"],
|
|
4040
|
+
[".jsx", "javascript"],
|
|
4041
|
+
[".mjs", "javascript"],
|
|
4042
|
+
[".cjs", "javascript"],
|
|
4043
|
+
[".md", "markdown"],
|
|
4044
|
+
[".json", "json"],
|
|
4045
|
+
[".yml", "yaml"],
|
|
4046
|
+
[".yaml", "yaml"],
|
|
4047
|
+
[".toml", "toml"]
|
|
4048
|
+
]);
|
|
4049
|
+
var sourceExtensions = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
4050
|
+
var isJavaScriptLikePath = (filePath) => {
|
|
4051
|
+
return sourceExtensions.has(path5.posix.extname(filePath));
|
|
4052
|
+
};
|
|
4053
|
+
var isTestPath = (filePath) => {
|
|
4054
|
+
return filePath.startsWith("tests/") || filePath.includes("/__tests__/") || /(?:^|[./-])(test|spec)\.[cm]?[jt]sx?$/u.test(path5.posix.basename(filePath));
|
|
4055
|
+
};
|
|
4056
|
+
var fileKind = (filePath, ignore) => {
|
|
4057
|
+
const classification = classifyPath(filePath, ignore);
|
|
4058
|
+
if (classification === "derived") {
|
|
4059
|
+
return "generated";
|
|
4060
|
+
}
|
|
4061
|
+
if (isTestPath(filePath)) {
|
|
4062
|
+
return "test";
|
|
4063
|
+
}
|
|
4064
|
+
if (filePath.endsWith(".md")) {
|
|
4065
|
+
return "doc";
|
|
4066
|
+
}
|
|
4067
|
+
if (classification === "functional-code") {
|
|
4068
|
+
return "source";
|
|
4069
|
+
}
|
|
4070
|
+
if (classification === "markdown") {
|
|
4071
|
+
return "doc";
|
|
4072
|
+
}
|
|
4073
|
+
if (classification === "config") {
|
|
4074
|
+
return "config";
|
|
4075
|
+
}
|
|
4076
|
+
return "other";
|
|
4077
|
+
};
|
|
4078
|
+
var targetHintsForTest = (filePath) => {
|
|
4079
|
+
const hints = /* @__PURE__ */ new Set();
|
|
4080
|
+
const basename = path5.posix.basename(filePath).replace(/\.(test|spec)\.[cm]?[jt]sx?$/u, "");
|
|
4081
|
+
if (basename.length > 0) {
|
|
4082
|
+
hints.add(basename);
|
|
4083
|
+
}
|
|
4084
|
+
const segments = filePath.split("/");
|
|
4085
|
+
const testRootIndex = segments.findIndex((segment) => segment === "tests" || segment === "__tests__");
|
|
4086
|
+
if (testRootIndex >= 0) {
|
|
4087
|
+
for (const segment of segments.slice(testRootIndex + 1, -1)) {
|
|
4088
|
+
if (segment.length > 0) {
|
|
4089
|
+
hints.add(segment);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
return [...hints].sort();
|
|
4094
|
+
};
|
|
4095
|
+
var defaultIgnore = [".git/**", "node_modules/**", "dist/**", "build/**"];
|
|
4096
|
+
var normalizePath2 = (filePath) => filePath.replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
|
4097
|
+
var gitDiscoverableFiles = async (rootDir) => {
|
|
4098
|
+
const result = await execa2(
|
|
4099
|
+
"git",
|
|
4100
|
+
["ls-files", "--cached", "--others", "--exclude-standard", "--deduplicate"],
|
|
4101
|
+
{
|
|
4102
|
+
cwd: rootDir,
|
|
4103
|
+
reject: false
|
|
4104
|
+
}
|
|
4105
|
+
);
|
|
4106
|
+
if ((result.exitCode ?? 1) !== 0) {
|
|
4107
|
+
return null;
|
|
4108
|
+
}
|
|
4109
|
+
return result.stdout.split("\n").map((line) => normalizePath2(line.trim())).filter((line) => line.length > 0);
|
|
4110
|
+
};
|
|
4111
|
+
var isIgnoredPath = (filePath, ignore) => {
|
|
4112
|
+
return micromatch5.isMatch(filePath, [...defaultIgnore, ...ignore]);
|
|
4113
|
+
};
|
|
4114
|
+
var discoverRepoFiles = async (rootDir, ignore) => {
|
|
4115
|
+
const discoveredFiles = await gitDiscoverableFiles(rootDir) ?? await fg6(["**/*"], {
|
|
4116
|
+
cwd: rootDir,
|
|
4117
|
+
onlyFiles: true,
|
|
4118
|
+
dot: true,
|
|
4119
|
+
ignore: [...defaultIgnore, ...ignore],
|
|
4120
|
+
followSymbolicLinks: false
|
|
4121
|
+
});
|
|
4122
|
+
const files = [];
|
|
4123
|
+
const docs = [];
|
|
4124
|
+
const tests = [];
|
|
4125
|
+
for (const filePath of discoveredFiles.filter((entry) => !isIgnoredPath(entry, ignore)).sort()) {
|
|
4126
|
+
const extension = path5.posix.extname(filePath);
|
|
4127
|
+
const kind = fileKind(filePath, ignore);
|
|
4128
|
+
files.push({
|
|
4129
|
+
path: filePath,
|
|
4130
|
+
kind,
|
|
4131
|
+
language: languageByExtension.get(extension) ?? null
|
|
4132
|
+
});
|
|
4133
|
+
if (kind === "test") {
|
|
4134
|
+
tests.push({
|
|
4135
|
+
path: filePath,
|
|
4136
|
+
targetHints: targetHintsForTest(filePath)
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
if (kind === "doc") {
|
|
4140
|
+
const source = await fs16.readFile(path5.join(rootDir, filePath), "utf8");
|
|
4141
|
+
const parsed = matter2(source);
|
|
4142
|
+
const markdown = parseMarkdownDocument(parsed.content);
|
|
4143
|
+
const title = markdown.headings.find((heading) => heading.depth === 1)?.text ?? null;
|
|
4144
|
+
const sourceOfTruth = Array.isArray(parsed.data.source_of_truth) ? parsed.data.source_of_truth.filter((entry) => typeof entry === "string") : [];
|
|
4145
|
+
docs.push({
|
|
4146
|
+
path: filePath,
|
|
4147
|
+
title,
|
|
4148
|
+
docType: typeof parsed.data.doc_type === "string" ? parsed.data.doc_type : null,
|
|
4149
|
+
truthKind: typeof parsed.data.truth_kind === "string" ? parsed.data.truth_kind : null,
|
|
4150
|
+
sourceOfTruth: sourceOfTruth.sort()
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
return {
|
|
4155
|
+
files: files.sort((left, right) => left.path.localeCompare(right.path)),
|
|
4156
|
+
docs: docs.sort((left, right) => left.path.localeCompare(right.path)),
|
|
4157
|
+
tests: tests.sort((left, right) => left.path.localeCompare(right.path))
|
|
4158
|
+
};
|
|
4159
|
+
};
|
|
4160
|
+
|
|
4161
|
+
// src/repo-index/package-metadata.ts
|
|
4162
|
+
import fs17 from "fs/promises";
|
|
4163
|
+
import path6 from "path";
|
|
4164
|
+
import fg7 from "fast-glob";
|
|
4165
|
+
var packageManagerFor = async (rootDir, packageDir) => {
|
|
4166
|
+
const lockfiles = [
|
|
4167
|
+
["package-lock.json", "npm"],
|
|
4168
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
4169
|
+
["yarn.lock", "yarn"],
|
|
4170
|
+
["bun.lockb", "bun"],
|
|
4171
|
+
["bun.lock", "bun"]
|
|
4172
|
+
];
|
|
4173
|
+
for (const [lockfile, manager] of lockfiles) {
|
|
4174
|
+
try {
|
|
4175
|
+
await fs17.access(path6.join(rootDir, packageDir, lockfile));
|
|
4176
|
+
return manager;
|
|
4177
|
+
} catch {
|
|
4178
|
+
continue;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
return "npm";
|
|
4182
|
+
};
|
|
4183
|
+
var discoverPackageMetadata = async (rootDir) => {
|
|
4184
|
+
const packageFiles = await fg7(["package.json", "*/package.json", "packages/*/package.json"], {
|
|
4185
|
+
cwd: rootDir,
|
|
4186
|
+
onlyFiles: true,
|
|
4187
|
+
ignore: ["node_modules/**", "dist/**", "build/**"],
|
|
4188
|
+
followSymbolicLinks: false
|
|
4189
|
+
});
|
|
4190
|
+
const packages = [];
|
|
4191
|
+
for (const packageFile of packageFiles.sort()) {
|
|
4192
|
+
const packageDir = path6.posix.dirname(packageFile) === "." ? "" : path6.posix.dirname(packageFile);
|
|
4193
|
+
const raw = JSON.parse(await fs17.readFile(path6.join(rootDir, packageFile), "utf8"));
|
|
4194
|
+
const scripts = raw.scripts && typeof raw.scripts === "object" ? Object.keys(raw.scripts).sort() : [];
|
|
4195
|
+
packages.push({
|
|
4196
|
+
path: packageFile,
|
|
4197
|
+
manager: await packageManagerFor(rootDir, packageDir),
|
|
4198
|
+
name: typeof raw.name === "string" ? raw.name : null,
|
|
4199
|
+
version: typeof raw.version === "string" ? raw.version : null,
|
|
4200
|
+
private: typeof raw.private === "boolean" ? raw.private : null,
|
|
4201
|
+
scripts
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
return packages;
|
|
4205
|
+
};
|
|
4206
|
+
|
|
4207
|
+
// src/repo-index/route-map.ts
|
|
4208
|
+
var buildRouteMap = async (rootDir) => {
|
|
4209
|
+
const loadResult = await loadConfig(rootDir);
|
|
4210
|
+
if (!loadResult.config) {
|
|
4211
|
+
return {
|
|
4212
|
+
schemaVersion: "route-map/v0",
|
|
4213
|
+
routes: [],
|
|
4214
|
+
diagnostics: loadResult.diagnostics
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4217
|
+
const routing = await resolveAreaRouting(rootDir, {
|
|
4218
|
+
rootIndex: loadResult.config.docs.routing.rootIndex,
|
|
4219
|
+
areaFilesRoot: loadResult.config.docs.routing.areaFilesRoot,
|
|
4220
|
+
truthDocsRoot: resolveTruthDocsRoot(loadResult.config)
|
|
4221
|
+
});
|
|
4222
|
+
return {
|
|
4223
|
+
schemaVersion: "route-map/v0",
|
|
4224
|
+
routes: routing.areas.map((area) => ({
|
|
4225
|
+
id: area.id,
|
|
4226
|
+
name: area.name,
|
|
4227
|
+
key: area.key,
|
|
4228
|
+
sourcePath: area.sourcePath,
|
|
4229
|
+
parentName: area.parentName,
|
|
4230
|
+
codeSurface: [...area.codeSurface].sort(),
|
|
4231
|
+
truthDocs: [...area.truthDocuments].sort(),
|
|
4232
|
+
updateTruthWhen: [...area.updateTruthWhen]
|
|
4233
|
+
})).sort((left, right) => left.key.localeCompare(right.key)),
|
|
4234
|
+
diagnostics: routing.diagnostics
|
|
4235
|
+
};
|
|
4236
|
+
};
|
|
4237
|
+
|
|
4238
|
+
// src/repo-index/typescript-symbols.ts
|
|
4239
|
+
import ts from "typescript";
|
|
4240
|
+
var sortStrings = (values) => [...new Set(values)].sort();
|
|
4241
|
+
var hasExportModifier = (node) => {
|
|
4242
|
+
return Boolean(
|
|
4243
|
+
ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
4244
|
+
);
|
|
4245
|
+
};
|
|
4246
|
+
var declarationName = (node) => {
|
|
4247
|
+
if (!node.name || !ts.isIdentifier(node.name)) {
|
|
4248
|
+
return null;
|
|
4249
|
+
}
|
|
4250
|
+
return node.name.text;
|
|
4251
|
+
};
|
|
4252
|
+
var addExport = (exports, publicSymbols, path12, name, kind) => {
|
|
4253
|
+
if (!name) {
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
const entry = { path: path12, name, kind };
|
|
4257
|
+
exports.push(entry);
|
|
4258
|
+
publicSymbols.push(entry);
|
|
4259
|
+
};
|
|
4260
|
+
var analyzeTypeScriptSource = (path12, source) => {
|
|
4261
|
+
const sourceFile = ts.createSourceFile(path12, source, ts.ScriptTarget.Latest, true);
|
|
4262
|
+
const imports = [];
|
|
4263
|
+
const exports = [];
|
|
4264
|
+
const publicSymbols = [];
|
|
4265
|
+
for (const statement of sourceFile.statements) {
|
|
4266
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
4267
|
+
const imported = [];
|
|
4268
|
+
const clause = statement.importClause;
|
|
4269
|
+
if (clause?.name) {
|
|
4270
|
+
imported.push("default");
|
|
4271
|
+
}
|
|
4272
|
+
if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
4273
|
+
imported.push("*");
|
|
4274
|
+
}
|
|
4275
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
4276
|
+
for (const element of clause.namedBindings.elements) {
|
|
4277
|
+
imported.push(element.propertyName?.text ?? element.name.text);
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
imports.push({
|
|
4281
|
+
from: path12,
|
|
4282
|
+
specifier: statement.moduleSpecifier.text,
|
|
4283
|
+
imported: sortStrings(imported)
|
|
4284
|
+
});
|
|
4285
|
+
continue;
|
|
4286
|
+
}
|
|
4287
|
+
if (ts.isExportDeclaration(statement)) {
|
|
4288
|
+
if (statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
4289
|
+
for (const element of statement.exportClause.elements) {
|
|
4290
|
+
addExport(exports, publicSymbols, path12, element.name.text, "re-export");
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
continue;
|
|
4294
|
+
}
|
|
4295
|
+
if (ts.isFunctionDeclaration(statement) && hasExportModifier(statement)) {
|
|
4296
|
+
addExport(exports, publicSymbols, path12, declarationName(statement), "function");
|
|
4297
|
+
continue;
|
|
4298
|
+
}
|
|
4299
|
+
if (ts.isClassDeclaration(statement) && hasExportModifier(statement)) {
|
|
4300
|
+
addExport(exports, publicSymbols, path12, declarationName(statement), "class");
|
|
4301
|
+
continue;
|
|
4302
|
+
}
|
|
4303
|
+
if (ts.isInterfaceDeclaration(statement) && hasExportModifier(statement)) {
|
|
4304
|
+
addExport(exports, publicSymbols, path12, declarationName(statement), "interface");
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4307
|
+
if (ts.isTypeAliasDeclaration(statement) && hasExportModifier(statement)) {
|
|
4308
|
+
addExport(exports, publicSymbols, path12, declarationName(statement), "type");
|
|
4309
|
+
continue;
|
|
4310
|
+
}
|
|
4311
|
+
if (ts.isEnumDeclaration(statement) && hasExportModifier(statement)) {
|
|
4312
|
+
addExport(exports, publicSymbols, path12, declarationName(statement), "enum");
|
|
4313
|
+
continue;
|
|
4314
|
+
}
|
|
4315
|
+
if (ts.isVariableStatement(statement) && hasExportModifier(statement)) {
|
|
4316
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
4317
|
+
addExport(exports, publicSymbols, path12, declarationName(declaration), "const");
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
return {
|
|
4322
|
+
imports: imports.sort((left, right) => left.specifier.localeCompare(right.specifier)),
|
|
4323
|
+
exports: exports.sort((left, right) => left.name.localeCompare(right.name)),
|
|
4324
|
+
publicSymbols: publicSymbols.sort((left, right) => left.name.localeCompare(right.name))
|
|
4325
|
+
};
|
|
4326
|
+
};
|
|
4327
|
+
|
|
4328
|
+
// src/repo-index/build.ts
|
|
4329
|
+
var buildRepoIndex = async (cwd) => {
|
|
4330
|
+
const repository = await getGitRepository(cwd);
|
|
4331
|
+
const rootDir = repository.worktreePath;
|
|
4332
|
+
const loadResult = await loadConfig(rootDir);
|
|
4333
|
+
const ignore = loadResult.config?.ignore ?? [];
|
|
4334
|
+
const diagnostics = [...loadResult.diagnostics];
|
|
4335
|
+
const [packages, fileTree, routeMap] = await Promise.all([
|
|
4336
|
+
discoverPackageMetadata(rootDir),
|
|
4337
|
+
discoverRepoFiles(rootDir, ignore),
|
|
4338
|
+
buildRouteMap(rootDir)
|
|
4339
|
+
]);
|
|
4340
|
+
const imports = [];
|
|
4341
|
+
const exports = [];
|
|
4342
|
+
const publicSymbols = [];
|
|
4343
|
+
diagnostics.push(...routeMap.diagnostics);
|
|
4344
|
+
for (const file of fileTree.files) {
|
|
4345
|
+
if (!isJavaScriptLikePath(file.path)) {
|
|
4346
|
+
continue;
|
|
4347
|
+
}
|
|
4348
|
+
const source = await fs18.readFile(path7.join(rootDir, file.path), "utf8");
|
|
4349
|
+
const analysis = analyzeTypeScriptSource(file.path, source);
|
|
4350
|
+
imports.push(...analysis.imports);
|
|
4351
|
+
exports.push(...analysis.exports);
|
|
4352
|
+
publicSymbols.push(...analysis.publicSymbols);
|
|
4353
|
+
}
|
|
4354
|
+
return {
|
|
4355
|
+
schemaVersion: "repo-index/v0",
|
|
4356
|
+
repository: {
|
|
4357
|
+
root: rootDir,
|
|
4358
|
+
branchName: repository.branchName,
|
|
4359
|
+
headSha: repository.headSha
|
|
4360
|
+
},
|
|
4361
|
+
packages,
|
|
4362
|
+
files: fileTree.files,
|
|
4363
|
+
docs: fileTree.docs,
|
|
4364
|
+
tests: fileTree.tests,
|
|
4365
|
+
imports: imports.sort((left, right) => `${left.from}:${left.specifier}`.localeCompare(`${right.from}:${right.specifier}`)),
|
|
4366
|
+
exports: exports.sort((left, right) => `${left.path}:${left.name}`.localeCompare(`${right.path}:${right.name}`)),
|
|
4367
|
+
publicSymbols: publicSymbols.sort(
|
|
4368
|
+
(left, right) => `${left.path}:${left.name}`.localeCompare(`${right.path}:${right.name}`)
|
|
4369
|
+
),
|
|
4370
|
+
routeMap,
|
|
4371
|
+
diagnostics
|
|
4372
|
+
};
|
|
4373
|
+
};
|
|
4374
|
+
|
|
4375
|
+
// src/impact/git-diff.ts
|
|
4376
|
+
import { execa as execa4 } from "execa";
|
|
4377
|
+
|
|
4378
|
+
// src/git/changes.ts
|
|
4379
|
+
import fs19 from "fs/promises";
|
|
4380
|
+
import path8 from "path";
|
|
4381
|
+
import { execa as execa3 } from "execa";
|
|
4382
|
+
var normalizePath3 = (filePath) => {
|
|
4383
|
+
return filePath.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
4384
|
+
};
|
|
4385
|
+
var listChangedPaths = async (cwd, args) => {
|
|
4386
|
+
const result = await execa3("git", args, { cwd });
|
|
4387
|
+
return result.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => normalizePath3(line));
|
|
4388
|
+
};
|
|
4389
|
+
var pathExists4 = async (filePath) => {
|
|
4390
|
+
try {
|
|
4391
|
+
await fs19.access(filePath);
|
|
4392
|
+
return true;
|
|
4393
|
+
} catch {
|
|
4394
|
+
return false;
|
|
4395
|
+
}
|
|
4396
|
+
};
|
|
4397
|
+
var getOrCreateChange = (changesByPath, filePath) => {
|
|
4398
|
+
const existingChange = changesByPath.get(filePath);
|
|
4399
|
+
if (existingChange) {
|
|
4400
|
+
return existingChange;
|
|
4401
|
+
}
|
|
4402
|
+
const nextChange = {
|
|
4403
|
+
path: filePath,
|
|
4404
|
+
staged: false,
|
|
4405
|
+
unstaged: false,
|
|
4406
|
+
untracked: false,
|
|
4407
|
+
deleted: false
|
|
4408
|
+
};
|
|
4409
|
+
changesByPath.set(filePath, nextChange);
|
|
4410
|
+
return nextChange;
|
|
4411
|
+
};
|
|
4412
|
+
var getUncommittedChanges = async (cwd) => {
|
|
4413
|
+
const repository = await getGitRepository(cwd);
|
|
4414
|
+
const rootDir = repository.worktreePath;
|
|
4415
|
+
const [stagedPaths, unstagedPaths, untrackedPaths, stagedDeletedPaths, unstagedDeletedPaths] = await Promise.all([
|
|
4416
|
+
listChangedPaths(rootDir, ["diff", "--name-only", "--cached", "--diff-filter=ACDMRTUXB"]),
|
|
4417
|
+
listChangedPaths(rootDir, ["diff", "--name-only", "--diff-filter=ACDMRTUXB"]),
|
|
4418
|
+
listChangedPaths(rootDir, ["ls-files", "--others", "--exclude-standard"]),
|
|
4419
|
+
listChangedPaths(rootDir, ["diff", "--name-only", "--cached", "--diff-filter=D"]),
|
|
4420
|
+
listChangedPaths(rootDir, ["diff", "--name-only", "--diff-filter=D"])
|
|
4421
|
+
]);
|
|
4422
|
+
const changesByPath = /* @__PURE__ */ new Map();
|
|
4423
|
+
for (const stagedPath of stagedPaths) {
|
|
4424
|
+
getOrCreateChange(changesByPath, stagedPath).staged = true;
|
|
4425
|
+
}
|
|
4426
|
+
for (const unstagedPath of unstagedPaths) {
|
|
4427
|
+
getOrCreateChange(changesByPath, unstagedPath).unstaged = true;
|
|
4428
|
+
}
|
|
4429
|
+
for (const untrackedPath of untrackedPaths) {
|
|
4430
|
+
getOrCreateChange(changesByPath, untrackedPath).untracked = true;
|
|
4431
|
+
}
|
|
4432
|
+
const deletedPathCandidates = /* @__PURE__ */ new Set([...stagedDeletedPaths, ...unstagedDeletedPaths]);
|
|
4433
|
+
for (const deletedPath of deletedPathCandidates) {
|
|
4434
|
+
const change = getOrCreateChange(changesByPath, deletedPath);
|
|
4435
|
+
change.deleted = !await pathExists4(path8.join(rootDir, deletedPath));
|
|
4436
|
+
}
|
|
4437
|
+
return Array.from(changesByPath.values()).sort((left, right) => {
|
|
4438
|
+
return left.path.localeCompare(right.path);
|
|
4439
|
+
});
|
|
4440
|
+
};
|
|
4441
|
+
|
|
4442
|
+
// src/impact/git-diff.ts
|
|
4443
|
+
var normalizePath4 = (filePath) => filePath.replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
4444
|
+
var statusForCode = (code) => {
|
|
4445
|
+
if (code.startsWith("A")) return "added";
|
|
4446
|
+
if (code.startsWith("D")) return "deleted";
|
|
4447
|
+
if (code.startsWith("R")) return "renamed";
|
|
4448
|
+
if (code.startsWith("C")) return "copied";
|
|
4449
|
+
if (code.startsWith("T")) return "type-changed";
|
|
4450
|
+
return "modified";
|
|
4451
|
+
};
|
|
4452
|
+
var mergeFile = (files, next) => {
|
|
4453
|
+
const existing = files.get(next.path);
|
|
4454
|
+
if (!existing) {
|
|
4455
|
+
files.set(next.path, next);
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
4458
|
+
files.set(next.path, {
|
|
4459
|
+
...existing,
|
|
4460
|
+
status: existing.status === "deleted" || existing.status === "renamed" ? existing.status : next.status,
|
|
4461
|
+
previousPath: existing.previousPath ?? next.previousPath,
|
|
4462
|
+
staged: existing.staged || next.staged,
|
|
4463
|
+
unstaged: existing.unstaged || next.unstaged,
|
|
4464
|
+
untracked: existing.untracked || next.untracked,
|
|
4465
|
+
deleted: existing.deleted || next.deleted
|
|
4466
|
+
});
|
|
4467
|
+
};
|
|
4468
|
+
var getChangedFiles = async (cwd, base) => {
|
|
4469
|
+
const repository = await getGitRepository(cwd);
|
|
4470
|
+
const files = /* @__PURE__ */ new Map();
|
|
4471
|
+
const diagnostics = [];
|
|
4472
|
+
let diff = await execa4("git", ["diff", "--name-status", "--find-renames", `${base}...HEAD`], {
|
|
4473
|
+
cwd: repository.worktreePath,
|
|
4474
|
+
reject: false
|
|
4475
|
+
});
|
|
4476
|
+
if ((diff.exitCode ?? 1) !== 0) {
|
|
4477
|
+
diff = await execa4("git", ["diff", "--name-status", "--find-renames", base, "HEAD"], {
|
|
4478
|
+
cwd: repository.worktreePath,
|
|
4479
|
+
reject: false
|
|
4480
|
+
});
|
|
4481
|
+
}
|
|
4482
|
+
if ((diff.exitCode ?? 1) === 0) {
|
|
4483
|
+
for (const line of diff.stdout.split("\n").filter(Boolean)) {
|
|
4484
|
+
const [rawStatus, rawPath, rawNewPath] = line.split(" ");
|
|
4485
|
+
const filePath = normalizePath4(rawNewPath ?? rawPath);
|
|
4486
|
+
const previousPath = rawNewPath && rawStatus.startsWith("R") ? normalizePath4(rawPath) : void 0;
|
|
4487
|
+
mergeFile(files, {
|
|
4488
|
+
path: filePath,
|
|
4489
|
+
previousPath,
|
|
4490
|
+
status: statusForCode(rawStatus),
|
|
4491
|
+
staged: false,
|
|
4492
|
+
unstaged: false,
|
|
4493
|
+
untracked: false,
|
|
4494
|
+
deleted: rawStatus.startsWith("D")
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
4497
|
+
} else {
|
|
4498
|
+
diagnostics.push({
|
|
4499
|
+
category: "impact",
|
|
4500
|
+
severity: "error",
|
|
4501
|
+
message: `Unable to compare base ref ${base} to HEAD.`
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
for (const change of await getUncommittedChanges(repository.worktreePath)) {
|
|
4505
|
+
mergeFile(files, {
|
|
4506
|
+
path: change.path,
|
|
4507
|
+
status: change.untracked ? "added" : change.deleted ? "deleted" : "modified",
|
|
4508
|
+
staged: change.staged,
|
|
4509
|
+
unstaged: change.unstaged,
|
|
4510
|
+
untracked: change.untracked,
|
|
4511
|
+
deleted: change.deleted
|
|
4512
|
+
});
|
|
4513
|
+
}
|
|
4514
|
+
return {
|
|
4515
|
+
files: [...files.values()].sort((left, right) => left.path.localeCompare(right.path)),
|
|
4516
|
+
diagnostics
|
|
4517
|
+
};
|
|
4518
|
+
};
|
|
4519
|
+
var readBaseFile = async (cwd, base, filePath) => {
|
|
4520
|
+
const repository = await getGitRepository(cwd);
|
|
4521
|
+
const result = await execa4("git", ["show", `${base}:${filePath}`], {
|
|
4522
|
+
cwd: repository.worktreePath,
|
|
4523
|
+
reject: false
|
|
4524
|
+
});
|
|
4525
|
+
return (result.exitCode ?? 1) === 0 ? result.stdout : null;
|
|
4526
|
+
};
|
|
4527
|
+
|
|
4528
|
+
// src/impact/build.ts
|
|
4529
|
+
var uniqueSorted = (values) => [...new Set(values)].sort();
|
|
4530
|
+
var routeMatchesFile = (route, filePath) => {
|
|
4531
|
+
return route.codeSurface.some((pattern) => micromatch6.isMatch(filePath, pattern));
|
|
4532
|
+
};
|
|
4533
|
+
var routeOwnsTruthDoc = (route, filePath) => {
|
|
4534
|
+
return route.truthDocs.includes(filePath);
|
|
4535
|
+
};
|
|
4536
|
+
var toImpactRoute = (route) => ({
|
|
4537
|
+
id: route.id,
|
|
4538
|
+
name: route.name,
|
|
4539
|
+
key: route.key,
|
|
4540
|
+
sourcePath: route.sourcePath,
|
|
4541
|
+
truthDocs: [...route.truthDocs].sort(),
|
|
4542
|
+
codeSurface: [...route.codeSurface].sort()
|
|
4543
|
+
});
|
|
4544
|
+
var changedPathSet = (changedFiles) => {
|
|
4545
|
+
return new Set(
|
|
4546
|
+
changedFiles.flatMap((file) => [file.path, ...file.previousPath ? [file.previousPath] : []])
|
|
4547
|
+
);
|
|
4548
|
+
};
|
|
4549
|
+
var changedFilePaths = (changedFile) => {
|
|
4550
|
+
return [changedFile.path, ...changedFile.previousPath ? [changedFile.previousPath] : []];
|
|
4551
|
+
};
|
|
4552
|
+
var resolveImportPath = (importEdge) => {
|
|
4553
|
+
if (!importEdge.specifier.startsWith(".")) {
|
|
4554
|
+
return null;
|
|
4555
|
+
}
|
|
4556
|
+
const basePath = path9.posix.normalize(path9.posix.join(path9.posix.dirname(importEdge.from), importEdge.specifier));
|
|
4557
|
+
const withoutExtension = basePath.replace(/\.[cm]?[jt]sx?$/u, "");
|
|
4558
|
+
return withoutExtension;
|
|
4559
|
+
};
|
|
4560
|
+
var importTargetsChangedFile = (importEdge, changedPath) => {
|
|
4561
|
+
const resolved = resolveImportPath(importEdge);
|
|
4562
|
+
if (!resolved) {
|
|
4563
|
+
return false;
|
|
4564
|
+
}
|
|
4565
|
+
return changedPath.replace(/\.[cm]?[jt]sx?$/u, "") === resolved;
|
|
4566
|
+
};
|
|
4567
|
+
var pathSegments = (filePath) => filePath.split("/").filter(Boolean);
|
|
4568
|
+
var testHintMatchesChangedFile = (hints, changedPath) => {
|
|
4569
|
+
const changedBaseName = path9.posix.basename(changedPath);
|
|
4570
|
+
const changedSegments = pathSegments(changedPath);
|
|
4571
|
+
return hints.some((hint) => changedBaseName.startsWith(hint) || changedSegments.includes(hint));
|
|
4572
|
+
};
|
|
4573
|
+
var changedSymbolsFor = async (cwd, base, basePath, currentPath, currentExports) => {
|
|
4574
|
+
if (!isJavaScriptLikePath(basePath) && !isJavaScriptLikePath(currentPath)) {
|
|
4575
|
+
return [];
|
|
4576
|
+
}
|
|
4577
|
+
const baseSource = await readBaseFile(cwd, base, basePath);
|
|
4578
|
+
const baseExports = baseSource ? analyzeTypeScriptSource(basePath, baseSource).exports : [];
|
|
4579
|
+
const currentByName = new Map(currentExports.map((entry) => [entry.name, entry]));
|
|
4580
|
+
const baseByName = new Map(baseExports.map((entry) => [entry.name, entry]));
|
|
4581
|
+
const changes = [];
|
|
4582
|
+
if (basePath !== currentPath) {
|
|
4583
|
+
for (const entry of currentExports) {
|
|
4584
|
+
changes.push({ path: currentPath, name: entry.name, kind: entry.kind, change: "added" });
|
|
4585
|
+
}
|
|
4586
|
+
for (const entry of baseExports) {
|
|
4587
|
+
changes.push({ path: basePath, name: entry.name, kind: entry.kind, change: "removed" });
|
|
4588
|
+
}
|
|
4589
|
+
return changes;
|
|
4590
|
+
}
|
|
4591
|
+
for (const [name, entry] of currentByName) {
|
|
4592
|
+
if (!baseByName.has(name)) {
|
|
4593
|
+
changes.push({ path: currentPath, name, kind: entry.kind, change: "added" });
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
for (const [name, entry] of baseByName) {
|
|
4597
|
+
if (!currentByName.has(name)) {
|
|
4598
|
+
changes.push({ path: basePath, name, kind: entry.kind, change: "removed" });
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
return changes;
|
|
4602
|
+
};
|
|
4603
|
+
var buildImpactSet = async (cwd, options) => {
|
|
4604
|
+
const repository = await getGitRepository(cwd);
|
|
4605
|
+
const rootDir = repository.worktreePath;
|
|
4606
|
+
const [loadResult, repoIndex, changedFilesResult] = await Promise.all([
|
|
4607
|
+
loadConfig(rootDir),
|
|
4608
|
+
buildRepoIndex(rootDir),
|
|
4609
|
+
getChangedFiles(rootDir, options.base)
|
|
4610
|
+
]);
|
|
4611
|
+
const changedFiles = changedFilesResult.files;
|
|
4612
|
+
const ignore = loadResult.config?.ignore ?? [];
|
|
4613
|
+
const diagnostics = [...repoIndex.diagnostics, ...changedFilesResult.diagnostics];
|
|
4614
|
+
const affectedRoutes = /* @__PURE__ */ new Map();
|
|
4615
|
+
const affectedTruthDocs = [];
|
|
4616
|
+
const affectedTests = [];
|
|
4617
|
+
const changedPublicSymbols = [];
|
|
4618
|
+
const knownTestPaths = new Set(repoIndex.tests.map((test) => test.path));
|
|
4619
|
+
for (const changedFile of changedFiles) {
|
|
4620
|
+
const routeCandidatePaths = changedFilePaths(changedFile);
|
|
4621
|
+
if (routeCandidatePaths.some((filePath) => knownTestPaths.has(filePath))) {
|
|
4622
|
+
affectedTests.push(changedFile.path);
|
|
4623
|
+
}
|
|
4624
|
+
const matchingRoutes = repoIndex.routeMap.routes.filter(
|
|
4625
|
+
(route) => routeCandidatePaths.some(
|
|
4626
|
+
(filePath) => routeMatchesFile(route, filePath) || routeOwnsTruthDoc(route, filePath)
|
|
4627
|
+
)
|
|
4628
|
+
);
|
|
4629
|
+
for (const route of matchingRoutes) {
|
|
4630
|
+
affectedRoutes.set(route.key, toImpactRoute(route));
|
|
4631
|
+
affectedTruthDocs.push(...route.truthDocs);
|
|
4632
|
+
if (route.truthDocs.length === 0) {
|
|
4633
|
+
diagnostics.push({
|
|
4634
|
+
category: "impact",
|
|
4635
|
+
severity: "review",
|
|
4636
|
+
message: `Changed file ${changedFile.path} maps to route ${route.name} but the route has no truth document.`,
|
|
4637
|
+
file: changedFile.path,
|
|
4638
|
+
area: route.name
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
if (matchingRoutes.length === 0 && !routeCandidatePaths.some((filePath) => knownTestPaths.has(filePath)) && routeCandidatePaths.some((filePath) => classifyPath(filePath, ignore) === "functional-code")) {
|
|
4643
|
+
diagnostics.push({
|
|
4644
|
+
category: "impact",
|
|
4645
|
+
severity: "review",
|
|
4646
|
+
message: `Changed file ${changedFile.path} is not mapped to a Truthmark route.`,
|
|
4647
|
+
file: changedFile.path
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
const currentExports = repoIndex.exports.filter((entry) => entry.path === changedFile.path);
|
|
4651
|
+
changedPublicSymbols.push(
|
|
4652
|
+
...await changedSymbolsFor(
|
|
4653
|
+
rootDir,
|
|
4654
|
+
options.base,
|
|
4655
|
+
changedFile.previousPath ?? changedFile.path,
|
|
4656
|
+
changedFile.path,
|
|
4657
|
+
currentExports
|
|
4658
|
+
)
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
const uniqueAffectedTruthDocs = uniqueSorted(affectedTruthDocs);
|
|
4662
|
+
const changedPaths = changedPathSet(changedFiles);
|
|
4663
|
+
for (const symbol of changedPublicSymbols) {
|
|
4664
|
+
if (uniqueAffectedTruthDocs.length === 0) {
|
|
4665
|
+
diagnostics.push({
|
|
4666
|
+
category: "impact",
|
|
4667
|
+
severity: "review",
|
|
4668
|
+
message: `Changed public symbol ${symbol.name} in ${symbol.path} has no affected truth document.`,
|
|
4669
|
+
file: symbol.path,
|
|
4670
|
+
data: {
|
|
4671
|
+
symbol: symbol.name,
|
|
4672
|
+
change: symbol.change
|
|
4673
|
+
}
|
|
4674
|
+
});
|
|
4675
|
+
continue;
|
|
4676
|
+
}
|
|
4677
|
+
if (!uniqueAffectedTruthDocs.some((truthDoc) => changedPaths.has(truthDoc))) {
|
|
4678
|
+
diagnostics.push({
|
|
4679
|
+
category: "impact",
|
|
4680
|
+
severity: "review",
|
|
4681
|
+
message: `Changed public symbol ${symbol.name} in ${symbol.path} has affected truth docs but none were changed in this impact set.`,
|
|
4682
|
+
file: symbol.path,
|
|
4683
|
+
data: {
|
|
4684
|
+
symbol: symbol.name,
|
|
4685
|
+
change: symbol.change,
|
|
4686
|
+
affectedTruthDocs: uniqueAffectedTruthDocs
|
|
4687
|
+
}
|
|
4688
|
+
});
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
for (const test of repoIndex.tests) {
|
|
4692
|
+
const testImports = repoIndex.imports.filter((edge) => edge.from === test.path);
|
|
4693
|
+
const importsChangedFile = changedFiles.some(
|
|
4694
|
+
(changedFile) => changedFilePaths(changedFile).some(
|
|
4695
|
+
(filePath) => testImports.some((importEdge) => importTargetsChangedFile(importEdge, filePath))
|
|
4696
|
+
)
|
|
4697
|
+
);
|
|
4698
|
+
const hintMatchesChangedFile = changedFiles.some(
|
|
4699
|
+
(changedFile) => changedFilePaths(changedFile).some(
|
|
4700
|
+
(filePath) => testHintMatchesChangedFile(test.targetHints, filePath)
|
|
4701
|
+
)
|
|
4702
|
+
);
|
|
4703
|
+
if (importsChangedFile || hintMatchesChangedFile) {
|
|
4704
|
+
affectedTests.push(test.path);
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
return {
|
|
4708
|
+
schemaVersion: "impact-set/v0",
|
|
4709
|
+
base: options.base,
|
|
4710
|
+
headSha: repository.headSha,
|
|
4711
|
+
changedFiles,
|
|
4712
|
+
affectedRoutes: [...affectedRoutes.values()].sort(
|
|
4713
|
+
(left, right) => left.key.localeCompare(right.key)
|
|
4714
|
+
),
|
|
4715
|
+
affectedTruthDocs: uniqueAffectedTruthDocs,
|
|
4716
|
+
affectedTests: uniqueSorted(affectedTests),
|
|
4717
|
+
changedPublicSymbols: changedPublicSymbols.sort(
|
|
4718
|
+
(left, right) => `${left.path}:${left.name}:${left.change}`.localeCompare(`${right.path}:${right.name}:${right.change}`)
|
|
4719
|
+
),
|
|
4720
|
+
diagnostics
|
|
4721
|
+
};
|
|
4722
|
+
};
|
|
4723
|
+
|
|
4724
|
+
// src/evidence/validate.ts
|
|
4725
|
+
import fs21 from "fs/promises";
|
|
4726
|
+
import fg8 from "fast-glob";
|
|
4727
|
+
|
|
4728
|
+
// src/evidence/parse.ts
|
|
4729
|
+
import fs20 from "fs/promises";
|
|
4730
|
+
import path10 from "path";
|
|
4731
|
+
import matter3 from "gray-matter";
|
|
4732
|
+
import { parse as parse3 } from "yaml";
|
|
4733
|
+
var evidenceBlockPattern = /```ya?ml\s*\n([\s\S]*?)```/giu;
|
|
4734
|
+
var repoRootPrefixes = [".codex/", ".github/", ".truthmark/", "docs/", "src/", "tests/"];
|
|
4735
|
+
var normalizeReferencePath = (truthDocPath, referencePath) => {
|
|
4736
|
+
const strippedPath = referencePath.split("#")[0]?.trim() ?? "";
|
|
4737
|
+
const isRepoRelative = repoRootPrefixes.some((prefix) => strippedPath.startsWith(prefix));
|
|
4738
|
+
if (!isRepoRelative && (strippedPath.startsWith(".") || !strippedPath.includes("/"))) {
|
|
4739
|
+
return path10.posix.normalize(path10.posix.join(path10.posix.dirname(truthDocPath), strippedPath));
|
|
4740
|
+
}
|
|
4741
|
+
return path10.posix.normalize(strippedPath);
|
|
4742
|
+
};
|
|
4743
|
+
var toEvidenceReference = (truthDocPath, raw) => {
|
|
4744
|
+
if (!raw || typeof raw !== "object" || !("path" in raw) || typeof raw.path !== "string") {
|
|
4745
|
+
return null;
|
|
4746
|
+
}
|
|
4747
|
+
return {
|
|
4748
|
+
truthDocPath,
|
|
4749
|
+
path: normalizeReferencePath(truthDocPath, raw.path),
|
|
4750
|
+
symbol: "symbol" in raw && typeof raw.symbol === "string" ? raw.symbol : void 0,
|
|
4751
|
+
startLine: "start_line" in raw && typeof raw.start_line === "number" ? raw.start_line : void 0,
|
|
4752
|
+
endLine: "end_line" in raw && typeof raw.end_line === "number" ? raw.end_line : void 0,
|
|
4753
|
+
contentHash: "content_hash" in raw && typeof raw.content_hash === "string" ? raw.content_hash : void 0,
|
|
4754
|
+
source: "evidence-block"
|
|
4755
|
+
};
|
|
4756
|
+
};
|
|
4757
|
+
var parseEvidenceReferences = async (rootDir, truthDocPath) => {
|
|
4758
|
+
const source = await fs20.readFile(path10.join(rootDir, truthDocPath), "utf8");
|
|
4759
|
+
const parsed = matter3(source);
|
|
4760
|
+
const references = [];
|
|
4761
|
+
const sourceOfTruth = Array.isArray(parsed.data.source_of_truth) ? parsed.data.source_of_truth : [];
|
|
4762
|
+
for (const entry of sourceOfTruth) {
|
|
4763
|
+
if (typeof entry !== "string") {
|
|
4764
|
+
continue;
|
|
4765
|
+
}
|
|
4766
|
+
references.push({
|
|
4767
|
+
truthDocPath,
|
|
4768
|
+
path: normalizeReferencePath(truthDocPath, entry),
|
|
4769
|
+
source: "frontmatter"
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
for (const match of parsed.content.matchAll(evidenceBlockPattern)) {
|
|
4773
|
+
const block = parse3(match[1] ?? "");
|
|
4774
|
+
const rawEvidence = block && typeof block === "object" && "evidence" in block ? block.evidence : null;
|
|
4775
|
+
if (!Array.isArray(rawEvidence)) {
|
|
4776
|
+
continue;
|
|
4777
|
+
}
|
|
4778
|
+
for (const rawReference of rawEvidence) {
|
|
4779
|
+
const reference = toEvidenceReference(truthDocPath, rawReference);
|
|
4780
|
+
if (reference) {
|
|
4781
|
+
references.push(reference);
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
return references;
|
|
4786
|
+
};
|
|
4787
|
+
|
|
4788
|
+
// src/evidence/validate.ts
|
|
4789
|
+
var pathExists5 = async (filePath) => {
|
|
4790
|
+
try {
|
|
4791
|
+
await fs21.access(filePath);
|
|
4792
|
+
return true;
|
|
4793
|
+
} catch {
|
|
4794
|
+
return false;
|
|
4795
|
+
}
|
|
4796
|
+
};
|
|
4797
|
+
var diagnosticFor = (reference, message) => ({
|
|
4798
|
+
category: "freshness",
|
|
4799
|
+
severity: "error",
|
|
4800
|
+
message,
|
|
4801
|
+
file: reference.truthDocPath,
|
|
4802
|
+
data: {
|
|
4803
|
+
reference: reference.path,
|
|
4804
|
+
source: reference.source
|
|
4805
|
+
}
|
|
4806
|
+
});
|
|
4807
|
+
var isGlobReference = (referencePath) => /[*?[\]{}()]/u.test(referencePath);
|
|
4808
|
+
var validateGlob = async (rootDir, reference) => {
|
|
4809
|
+
if (reference.path.startsWith("../") || reference.path.startsWith("/")) {
|
|
4810
|
+
return diagnosticFor(reference, `Referenced file pattern ${reference.path} must stay inside the repository root.`);
|
|
4811
|
+
}
|
|
4812
|
+
const matches = await fg8(reference.path, {
|
|
4813
|
+
cwd: rootDir,
|
|
4814
|
+
dot: true,
|
|
4815
|
+
onlyFiles: true,
|
|
4816
|
+
followSymbolicLinks: false
|
|
4817
|
+
});
|
|
4818
|
+
return matches.length > 0 ? null : diagnosticFor(reference, `Referenced file pattern ${reference.path} does not match any file.`);
|
|
4819
|
+
};
|
|
4820
|
+
var validateSymbol = async (rootDir, reference) => {
|
|
4821
|
+
if (!reference.symbol || !isJavaScriptLikePath(reference.path)) {
|
|
4822
|
+
return null;
|
|
4823
|
+
}
|
|
4824
|
+
const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
|
|
4825
|
+
const analysis = analyzeTypeScriptSource(reference.path, source);
|
|
4826
|
+
const hasSymbol = analysis.publicSymbols.some((symbol) => symbol.name === reference.symbol);
|
|
4827
|
+
return hasSymbol ? null : diagnosticFor(reference, `Evidence symbol ${reference.symbol} was not found in ${reference.path}.`);
|
|
4828
|
+
};
|
|
4829
|
+
var validateHash = async (rootDir, reference) => {
|
|
4830
|
+
if (!reference.contentHash) {
|
|
4831
|
+
return null;
|
|
4832
|
+
}
|
|
4833
|
+
if (!reference.contentHash.startsWith("sha256:")) {
|
|
4834
|
+
return diagnosticFor(reference, `Evidence hash for ${reference.path} must use sha256:.`);
|
|
4835
|
+
}
|
|
4836
|
+
const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
|
|
4837
|
+
const lines = source.split("\n");
|
|
4838
|
+
const startLine = reference.startLine ?? 1;
|
|
4839
|
+
const endLine = reference.endLine ?? lines.length;
|
|
4840
|
+
if (startLine < 1 || endLine < startLine || endLine > lines.length) {
|
|
4841
|
+
return diagnosticFor(reference, `Evidence line span for ${reference.path} is outside the file.`);
|
|
4842
|
+
}
|
|
4843
|
+
const actualHash = `sha256:${hashText(lines.slice(startLine - 1, endLine).join("\n"))}`;
|
|
4844
|
+
return actualHash === reference.contentHash ? null : diagnosticFor(reference, `Evidence hash for ${reference.path} is stale.`);
|
|
4845
|
+
};
|
|
4846
|
+
var validateLineSpan = async (rootDir, reference) => {
|
|
4847
|
+
if (reference.startLine === void 0 && reference.endLine === void 0) {
|
|
4848
|
+
return null;
|
|
4849
|
+
}
|
|
4850
|
+
const source = await fs21.readFile(resolveRepoPath(rootDir, reference.path), "utf8");
|
|
4851
|
+
const lines = source.split("\n");
|
|
4852
|
+
const startLine = reference.startLine ?? 1;
|
|
4853
|
+
const endLine = reference.endLine ?? lines.length;
|
|
4854
|
+
return startLine < 1 || endLine < startLine || endLine > lines.length ? diagnosticFor(reference, `Evidence line span for ${reference.path} is outside the file.`) : null;
|
|
4855
|
+
};
|
|
4856
|
+
var validateReference = async (rootDir, reference) => {
|
|
4857
|
+
const diagnostics = [];
|
|
4858
|
+
try {
|
|
4859
|
+
if (isGlobReference(reference.path)) {
|
|
4860
|
+
const globDiagnostic = await validateGlob(rootDir, reference);
|
|
4861
|
+
return globDiagnostic ? [globDiagnostic] : [];
|
|
4862
|
+
}
|
|
4863
|
+
const absolutePath = resolveRepoPath(rootDir, reference.path);
|
|
4864
|
+
await assertRepoContainment(rootDir, absolutePath);
|
|
4865
|
+
if (!await pathExists5(absolutePath)) {
|
|
4866
|
+
diagnostics.push(diagnosticFor(reference, `Referenced file ${reference.path} does not exist.`));
|
|
4867
|
+
return diagnostics;
|
|
4868
|
+
}
|
|
4869
|
+
const symbolDiagnostic = await validateSymbol(rootDir, reference);
|
|
4870
|
+
if (symbolDiagnostic) {
|
|
4871
|
+
diagnostics.push(symbolDiagnostic);
|
|
4872
|
+
}
|
|
4873
|
+
const lineSpanDiagnostic = await validateLineSpan(rootDir, reference);
|
|
4874
|
+
if (lineSpanDiagnostic) {
|
|
4875
|
+
diagnostics.push(lineSpanDiagnostic);
|
|
4876
|
+
} else {
|
|
4877
|
+
const hashDiagnostic = await validateHash(rootDir, reference);
|
|
4878
|
+
if (hashDiagnostic) {
|
|
4879
|
+
diagnostics.push(hashDiagnostic);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
} catch {
|
|
4883
|
+
diagnostics.push(diagnosticFor(reference, `Referenced file ${reference.path} must stay inside the repository root.`));
|
|
4884
|
+
}
|
|
4885
|
+
return diagnostics;
|
|
4886
|
+
};
|
|
4887
|
+
var validateEvidenceReferences = async (rootDir, truthDocPaths) => {
|
|
4888
|
+
const diagnostics = [];
|
|
4889
|
+
for (const truthDocPath of [...truthDocPaths].sort()) {
|
|
4890
|
+
const references = await parseEvidenceReferences(rootDir, truthDocPath);
|
|
4891
|
+
for (const reference of references) {
|
|
4892
|
+
diagnostics.push(...await validateReference(rootDir, reference));
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
return diagnostics;
|
|
4896
|
+
};
|
|
4897
|
+
|
|
4898
|
+
// src/freshness/check.ts
|
|
4899
|
+
var checkFreshness = async (rootDir, _config, truthDocumentPaths, base) => {
|
|
4900
|
+
const impactSet = await buildImpactSet(rootDir, { base });
|
|
4901
|
+
const diagnostics = [...await validateEvidenceReferences(rootDir, truthDocumentPaths)];
|
|
4902
|
+
for (const diagnostic of impactSet.diagnostics) {
|
|
4903
|
+
if (diagnostic.category !== "impact") {
|
|
4904
|
+
continue;
|
|
4905
|
+
}
|
|
4906
|
+
diagnostics.push({
|
|
4907
|
+
...diagnostic,
|
|
4908
|
+
category: "freshness",
|
|
4909
|
+
message: diagnostic.message.replace("not mapped to a Truthmark route", "not routed to truth ownership")
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
return {
|
|
4913
|
+
diagnostics,
|
|
4914
|
+
impactSet
|
|
4915
|
+
};
|
|
4916
|
+
};
|
|
4917
|
+
|
|
3096
4918
|
// src/checks/check.ts
|
|
3097
4919
|
var summarizeDiagnostics = (diagnostics) => {
|
|
3098
4920
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
@@ -3102,7 +4924,7 @@ var summarizeDiagnostics = (diagnostics) => {
|
|
|
3102
4924
|
}
|
|
3103
4925
|
return `Truthmark check completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`;
|
|
3104
4926
|
};
|
|
3105
|
-
var runCheck = async (cwd) => {
|
|
4927
|
+
var runCheck = async (cwd, options = {}) => {
|
|
3106
4928
|
const repository = await getGitRepository(cwd);
|
|
3107
4929
|
const rootDir = repository.worktreePath;
|
|
3108
4930
|
const branchScope = await getBranchScopeData(rootDir);
|
|
@@ -3120,10 +4942,21 @@ var runCheck = async (cwd) => {
|
|
|
3120
4942
|
const authority = await checkAuthority(rootDir, loadResult.config);
|
|
3121
4943
|
const areas = await checkAreas(rootDir, loadResult.config);
|
|
3122
4944
|
const markdownPaths = [.../* @__PURE__ */ new Set([...authority.paths, ...areas.truthDocumentPaths])];
|
|
3123
|
-
const frontmatter = await checkFrontmatter(
|
|
4945
|
+
const frontmatter = await checkFrontmatter(
|
|
4946
|
+
rootDir,
|
|
4947
|
+
loadResult.config,
|
|
4948
|
+
markdownPaths,
|
|
4949
|
+
areas.truthDocumentEntries
|
|
4950
|
+
);
|
|
3124
4951
|
const links = await checkLinks(rootDir, markdownPaths);
|
|
3125
|
-
const decisionSections = await checkDecisionSections(
|
|
4952
|
+
const decisionSections = await checkDecisionSections(
|
|
4953
|
+
rootDir,
|
|
4954
|
+
loadResult.config,
|
|
4955
|
+
markdownPaths,
|
|
4956
|
+
areas.truthDocumentEntries
|
|
4957
|
+
);
|
|
3126
4958
|
const generatedSurfaces = await checkGeneratedSurfaces(rootDir, loadResult.config);
|
|
4959
|
+
const freshness = options.base ? await checkFreshness(rootDir, loadResult.config, areas.truthDocumentPaths, options.base) : null;
|
|
3127
4960
|
const diagnostics = [
|
|
3128
4961
|
...loadResult.diagnostics,
|
|
3129
4962
|
...authority.diagnostics,
|
|
@@ -3131,7 +4964,8 @@ var runCheck = async (cwd) => {
|
|
|
3131
4964
|
...links,
|
|
3132
4965
|
...areas.diagnostics,
|
|
3133
4966
|
...decisionSections,
|
|
3134
|
-
...generatedSurfaces
|
|
4967
|
+
...generatedSurfaces,
|
|
4968
|
+
...freshness?.diagnostics ?? []
|
|
3135
4969
|
];
|
|
3136
4970
|
const truthVisibility = {
|
|
3137
4971
|
routePrecision: areas.routePrecision,
|
|
@@ -3142,7 +4976,8 @@ var runCheck = async (cwd) => {
|
|
|
3142
4976
|
syncCompletenessIssueCount: diagnostics.filter(
|
|
3143
4977
|
(diagnostic) => diagnostic.category === "doc-structure" || diagnostic.category === "generated-surface"
|
|
3144
4978
|
).length,
|
|
3145
|
-
topologyPressureCount: areas.topologyPressureCount
|
|
4979
|
+
topologyPressureCount: areas.topologyPressureCount,
|
|
4980
|
+
freshnessDiagnosticCount: freshness?.diagnostics.length ?? 0
|
|
3146
4981
|
};
|
|
3147
4982
|
return {
|
|
3148
4983
|
command: "check",
|
|
@@ -3150,11 +4985,170 @@ var runCheck = async (cwd) => {
|
|
|
3150
4985
|
diagnostics,
|
|
3151
4986
|
data: {
|
|
3152
4987
|
branchScope,
|
|
3153
|
-
truthVisibility
|
|
4988
|
+
truthVisibility,
|
|
4989
|
+
...freshness ? { impactSet: freshness.impactSet } : {}
|
|
3154
4990
|
}
|
|
3155
4991
|
};
|
|
3156
4992
|
};
|
|
3157
4993
|
|
|
4994
|
+
// src/context-pack/build.ts
|
|
4995
|
+
import fs22 from "fs/promises";
|
|
4996
|
+
import path11 from "path";
|
|
4997
|
+
import fg9 from "fast-glob";
|
|
4998
|
+
var uniqueSorted2 = (values) => [...new Set(values)].sort();
|
|
4999
|
+
var repoRootPrefixes2 = [".codex/", ".github/", ".truthmark/", "docs/", "src/", "tests/"];
|
|
5000
|
+
var isGlobReference2 = (referencePath) => /[*?[\]{}()]/u.test(referencePath);
|
|
5001
|
+
var normalizeDocReferencePath = (docPath, referencePath) => {
|
|
5002
|
+
const strippedPath = referencePath.split("#")[0]?.trim() ?? "";
|
|
5003
|
+
if (strippedPath.length === 0 || strippedPath.startsWith("/")) {
|
|
5004
|
+
return null;
|
|
5005
|
+
}
|
|
5006
|
+
const isRepoRelative = repoRootPrefixes2.some((prefix) => strippedPath.startsWith(prefix));
|
|
5007
|
+
const normalized = isRepoRelative ? path11.posix.normalize(strippedPath) : path11.posix.normalize(path11.posix.join(path11.posix.dirname(docPath), strippedPath));
|
|
5008
|
+
return normalized === ".." || normalized.startsWith("../") ? null : normalized;
|
|
5009
|
+
};
|
|
5010
|
+
var readIfExists = async (rootDir, filePath) => {
|
|
5011
|
+
try {
|
|
5012
|
+
return await fs22.readFile(path11.join(rootDir, filePath), "utf8");
|
|
5013
|
+
} catch {
|
|
5014
|
+
return null;
|
|
5015
|
+
}
|
|
5016
|
+
};
|
|
5017
|
+
var boundedContent = (filePath, content, warnings) => {
|
|
5018
|
+
const lines = content.split("\n");
|
|
5019
|
+
if (lines.length <= 200) {
|
|
5020
|
+
return { path: filePath, content, truncated: false };
|
|
5021
|
+
}
|
|
5022
|
+
warnings.push({
|
|
5023
|
+
category: "context-pack",
|
|
5024
|
+
severity: "review",
|
|
5025
|
+
message: `Context source file ${filePath} was truncated to fit ContextPack v0 bounds.`,
|
|
5026
|
+
file: filePath
|
|
5027
|
+
});
|
|
5028
|
+
return {
|
|
5029
|
+
path: filePath,
|
|
5030
|
+
content: [...lines.slice(0, 80), "...", ...lines.slice(-40)].join("\n"),
|
|
5031
|
+
truncated: true
|
|
5032
|
+
};
|
|
5033
|
+
};
|
|
5034
|
+
var documentsFor = async (rootDir, paths) => {
|
|
5035
|
+
const documents = [];
|
|
5036
|
+
for (const filePath of uniqueSorted2(paths)) {
|
|
5037
|
+
const content = await readIfExists(rootDir, filePath);
|
|
5038
|
+
if (content !== null) {
|
|
5039
|
+
documents.push({ path: filePath, content });
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
return documents;
|
|
5043
|
+
};
|
|
5044
|
+
var sourceFilesFor = async (rootDir, paths, warnings) => {
|
|
5045
|
+
const sourceFiles = [];
|
|
5046
|
+
for (const filePath of uniqueSorted2(paths)) {
|
|
5047
|
+
const content = await readIfExists(rootDir, filePath);
|
|
5048
|
+
if (content !== null) {
|
|
5049
|
+
sourceFiles.push(boundedContent(filePath, content, warnings));
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
return sourceFiles;
|
|
5053
|
+
};
|
|
5054
|
+
var sourceOfTruthPathsFor = async (rootDir, docs, truthDocPaths) => {
|
|
5055
|
+
const selectedTruthDocs = new Set(truthDocPaths);
|
|
5056
|
+
const sourcePaths = [];
|
|
5057
|
+
for (const doc of docs) {
|
|
5058
|
+
if (!selectedTruthDocs.has(doc.path)) {
|
|
5059
|
+
continue;
|
|
5060
|
+
}
|
|
5061
|
+
for (const referencePath of doc.sourceOfTruth) {
|
|
5062
|
+
const normalizedPath = normalizeDocReferencePath(doc.path, referencePath);
|
|
5063
|
+
if (!normalizedPath) {
|
|
5064
|
+
continue;
|
|
5065
|
+
}
|
|
5066
|
+
if (isGlobReference2(normalizedPath)) {
|
|
5067
|
+
sourcePaths.push(
|
|
5068
|
+
...await fg9(normalizedPath, {
|
|
5069
|
+
cwd: rootDir,
|
|
5070
|
+
dot: true,
|
|
5071
|
+
onlyFiles: true,
|
|
5072
|
+
followSymbolicLinks: false
|
|
5073
|
+
})
|
|
5074
|
+
);
|
|
5075
|
+
} else {
|
|
5076
|
+
sourcePaths.push(normalizedPath);
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
}
|
|
5080
|
+
return uniqueSorted2(sourcePaths);
|
|
5081
|
+
};
|
|
5082
|
+
var writePathsFor = (workflow, truthDocs, routes) => {
|
|
5083
|
+
if (workflow === "truth-sync") {
|
|
5084
|
+
return uniqueSorted2(["docs/truthmark/areas.md", ...truthDocs]);
|
|
5085
|
+
}
|
|
5086
|
+
if (workflow === "truth-document") {
|
|
5087
|
+
return uniqueSorted2(["docs/truthmark/areas.md", ...truthDocs]);
|
|
5088
|
+
}
|
|
5089
|
+
return uniqueSorted2(routes.flatMap((route) => route.codeSurface));
|
|
5090
|
+
};
|
|
5091
|
+
var testCommandsFor = (affectedTests) => {
|
|
5092
|
+
return affectedTests.length === 0 ? ["npm test"] : [`npm test -- ${affectedTests.join(" ")}`];
|
|
5093
|
+
};
|
|
5094
|
+
var buildContextPack = async (cwd, options) => {
|
|
5095
|
+
const repoIndex = await buildRepoIndex(cwd);
|
|
5096
|
+
const rootDir = repoIndex.repository.root;
|
|
5097
|
+
const impactSet = options.base ? await buildImpactSet(rootDir, { base: options.base }) : null;
|
|
5098
|
+
const routeMap = impactSet ? repoIndex.routeMap : repoIndex.routeMap;
|
|
5099
|
+
const warnings = [];
|
|
5100
|
+
const truthDocPaths = impactSet?.affectedTruthDocs ?? (options.workflow === "truth-realize" ? [] : routeMap.routes.flatMap((route) => route.truthDocs));
|
|
5101
|
+
const contextRoutes = impactSet?.affectedRoutes ?? (options.workflow === "truth-realize" ? [] : routeMap.routes);
|
|
5102
|
+
if (options.workflow === "truth-realize" && !impactSet) {
|
|
5103
|
+
warnings.push({
|
|
5104
|
+
category: "context-pack",
|
|
5105
|
+
severity: "review",
|
|
5106
|
+
message: "truth-realize requires --base to derive bounded allowed write paths."
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5109
|
+
const sourceOfTruthPaths = await sourceOfTruthPathsFor(rootDir, repoIndex.docs, truthDocPaths);
|
|
5110
|
+
const sourceFilePaths = uniqueSorted2([
|
|
5111
|
+
...impactSet?.changedFiles.filter((file) => !file.deleted).map((file) => file.path) ?? [],
|
|
5112
|
+
...sourceOfTruthPaths
|
|
5113
|
+
]);
|
|
5114
|
+
return {
|
|
5115
|
+
schemaVersion: "context-pack/v0",
|
|
5116
|
+
workflow: options.workflow,
|
|
5117
|
+
base: options.base ?? null,
|
|
5118
|
+
impactSet,
|
|
5119
|
+
routeMap,
|
|
5120
|
+
allowedWritePaths: writePathsFor(options.workflow, truthDocPaths, contextRoutes),
|
|
5121
|
+
truthDocs: await documentsFor(rootDir, truthDocPaths),
|
|
5122
|
+
sourceFiles: await sourceFilesFor(rootDir, sourceFilePaths, warnings),
|
|
5123
|
+
testCommands: testCommandsFor(impactSet?.affectedTests ?? []),
|
|
5124
|
+
warnings
|
|
5125
|
+
};
|
|
5126
|
+
};
|
|
5127
|
+
|
|
5128
|
+
// src/context-pack/render.ts
|
|
5129
|
+
var renderContextPackMarkdown = (pack) => {
|
|
5130
|
+
const lines = [
|
|
5131
|
+
`# Truthmark ContextPack (${pack.workflow})`,
|
|
5132
|
+
"",
|
|
5133
|
+
`Schema: ${pack.schemaVersion}`,
|
|
5134
|
+
`Base: ${pack.base ?? "none"}`,
|
|
5135
|
+
"",
|
|
5136
|
+
"## Allowed Write Paths",
|
|
5137
|
+
...pack.allowedWritePaths.map((filePath) => `- ${filePath}`),
|
|
5138
|
+
"",
|
|
5139
|
+
"## Truth Docs",
|
|
5140
|
+
...pack.truthDocs.map((doc) => `- ${doc.path}`),
|
|
5141
|
+
"",
|
|
5142
|
+
"## Source Files",
|
|
5143
|
+
...pack.sourceFiles.map((file) => `- ${file.path}${file.truncated ? " (truncated)" : ""}`),
|
|
5144
|
+
"",
|
|
5145
|
+
"## Test Commands",
|
|
5146
|
+
...pack.testCommands.map((command) => `- ${command}`)
|
|
5147
|
+
];
|
|
5148
|
+
return `${lines.join("\n")}
|
|
5149
|
+
`;
|
|
5150
|
+
};
|
|
5151
|
+
|
|
3158
5152
|
// src/cli/handlers.ts
|
|
3159
5153
|
var runConfig2 = async (options) => {
|
|
3160
5154
|
return runConfig(process.cwd(), options);
|
|
@@ -3162,8 +5156,96 @@ var runConfig2 = async (options) => {
|
|
|
3162
5156
|
var runInit2 = async () => {
|
|
3163
5157
|
return runInit(process.cwd());
|
|
3164
5158
|
};
|
|
3165
|
-
var runCheck2 = async () => {
|
|
3166
|
-
return runCheck(process.cwd());
|
|
5159
|
+
var runCheck2 = async (options = {}) => {
|
|
5160
|
+
return runCheck(process.cwd(), options);
|
|
5161
|
+
};
|
|
5162
|
+
var runIndex = async () => {
|
|
5163
|
+
const repoIndex = await buildRepoIndex(process.cwd());
|
|
5164
|
+
const errorCount = repoIndex.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
5165
|
+
const reviewCount = repoIndex.diagnostics.filter((diagnostic) => diagnostic.severity === "review").length;
|
|
5166
|
+
return {
|
|
5167
|
+
command: "index",
|
|
5168
|
+
summary: `Truthmark index completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`,
|
|
5169
|
+
diagnostics: repoIndex.diagnostics,
|
|
5170
|
+
data: {
|
|
5171
|
+
repoIndex,
|
|
5172
|
+
routeMap: repoIndex.routeMap
|
|
5173
|
+
}
|
|
5174
|
+
};
|
|
5175
|
+
};
|
|
5176
|
+
var runImpact = async (options) => {
|
|
5177
|
+
if (!options.base) {
|
|
5178
|
+
return {
|
|
5179
|
+
command: "impact",
|
|
5180
|
+
summary: "Truthmark impact requires --base.",
|
|
5181
|
+
diagnostics: [
|
|
5182
|
+
{
|
|
5183
|
+
category: "impact",
|
|
5184
|
+
severity: "error",
|
|
5185
|
+
message: "truthmark impact requires --base <ref>."
|
|
5186
|
+
}
|
|
5187
|
+
]
|
|
5188
|
+
};
|
|
5189
|
+
}
|
|
5190
|
+
const impactSet = await buildImpactSet(process.cwd(), { base: options.base });
|
|
5191
|
+
const errorCount = impactSet.diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
5192
|
+
const reviewCount = impactSet.diagnostics.filter((diagnostic) => diagnostic.severity === "review").length;
|
|
5193
|
+
return {
|
|
5194
|
+
command: "impact",
|
|
5195
|
+
summary: `Truthmark impact completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`,
|
|
5196
|
+
diagnostics: impactSet.diagnostics,
|
|
5197
|
+
data: {
|
|
5198
|
+
impactSet
|
|
5199
|
+
}
|
|
5200
|
+
};
|
|
5201
|
+
};
|
|
5202
|
+
var isContextPackWorkflow = (value) => {
|
|
5203
|
+
return value === "truth-sync" || value === "truth-document" || value === "truth-realize";
|
|
5204
|
+
};
|
|
5205
|
+
var isContextPackFormat = (value) => {
|
|
5206
|
+
return value === void 0 || value === "json" || value === "markdown";
|
|
5207
|
+
};
|
|
5208
|
+
var runContext = async (options) => {
|
|
5209
|
+
if (!isContextPackWorkflow(options.workflow)) {
|
|
5210
|
+
return {
|
|
5211
|
+
command: "context",
|
|
5212
|
+
summary: "Truthmark context requires a supported --workflow value.",
|
|
5213
|
+
diagnostics: [
|
|
5214
|
+
{
|
|
5215
|
+
category: "context-pack",
|
|
5216
|
+
severity: "error",
|
|
5217
|
+
message: "truthmark context requires --workflow truth-sync, truth-document, or truth-realize."
|
|
5218
|
+
}
|
|
5219
|
+
]
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
if (!isContextPackFormat(options.format)) {
|
|
5223
|
+
return {
|
|
5224
|
+
command: "context",
|
|
5225
|
+
summary: "Truthmark context requires a supported --format value.",
|
|
5226
|
+
diagnostics: [
|
|
5227
|
+
{
|
|
5228
|
+
category: "context-pack",
|
|
5229
|
+
severity: "error",
|
|
5230
|
+
message: "truthmark context requires --format json or markdown."
|
|
5231
|
+
}
|
|
5232
|
+
]
|
|
5233
|
+
};
|
|
5234
|
+
}
|
|
5235
|
+
const contextPack = await buildContextPack(process.cwd(), {
|
|
5236
|
+
workflow: options.workflow,
|
|
5237
|
+
base: options.base
|
|
5238
|
+
});
|
|
5239
|
+
const diagnostics = contextPack.warnings;
|
|
5240
|
+
return {
|
|
5241
|
+
command: "context",
|
|
5242
|
+
summary: `Truthmark context generated ${contextPack.workflow} ContextPack with ${diagnostics.length} warnings.`,
|
|
5243
|
+
diagnostics,
|
|
5244
|
+
data: {
|
|
5245
|
+
contextPack,
|
|
5246
|
+
...options.format === "markdown" ? { markdown: renderContextPackMarkdown(contextPack) } : {}
|
|
5247
|
+
}
|
|
5248
|
+
};
|
|
3167
5249
|
};
|
|
3168
5250
|
|
|
3169
5251
|
// src/cli/program.ts
|
|
@@ -3172,6 +5254,13 @@ var writeResult = (result, options) => {
|
|
|
3172
5254
|
process.stdout.write(`${output}
|
|
3173
5255
|
`);
|
|
3174
5256
|
};
|
|
5257
|
+
var writeContextResult = (result, options) => {
|
|
5258
|
+
if (!options.json && options.format === "markdown" && typeof result.data?.markdown === "string") {
|
|
5259
|
+
process.stdout.write(result.data.markdown);
|
|
5260
|
+
return;
|
|
5261
|
+
}
|
|
5262
|
+
writeResult(result, options);
|
|
5263
|
+
};
|
|
3175
5264
|
var addJsonOption = (command) => {
|
|
3176
5265
|
return command.option("--json", "Render command output as JSON");
|
|
3177
5266
|
};
|
|
@@ -3189,9 +5278,31 @@ var buildProgram = () => {
|
|
|
3189
5278
|
writeResult(await runInit2(), options);
|
|
3190
5279
|
});
|
|
3191
5280
|
addJsonOption(
|
|
3192
|
-
program.command("check").description("Run local Truthmark diagnostics.")
|
|
5281
|
+
program.command("check").description("Run local Truthmark diagnostics.").option("--base <ref>", "Base Git ref for freshness diagnostics")
|
|
3193
5282
|
).action(async (options) => {
|
|
3194
|
-
writeResult(await runCheck2(), options);
|
|
5283
|
+
writeResult(await runCheck2({ base: options.base }), options);
|
|
5284
|
+
});
|
|
5285
|
+
addJsonOption(
|
|
5286
|
+
program.command("index").description("Build the deterministic Truthmark repository index.")
|
|
5287
|
+
).action(async (options) => {
|
|
5288
|
+
writeResult(await runIndex(), options);
|
|
5289
|
+
});
|
|
5290
|
+
addJsonOption(
|
|
5291
|
+
program.command("impact").description("Map changed files to truth routes, docs, owners, and tests.").requiredOption("--base <ref>", "Base Git ref to compare against")
|
|
5292
|
+
).action(async (options) => {
|
|
5293
|
+
writeResult(await runImpact({ base: options.base }), options);
|
|
5294
|
+
});
|
|
5295
|
+
addJsonOption(
|
|
5296
|
+
program.command("context").description("Generate a bounded workflow context pack.").requiredOption("--workflow <workflow>", "Workflow name: truth-sync, truth-document, or truth-realize").option("--base <ref>", "Base Git ref for impact-backed packs").option("--format <format>", "Output format: json or markdown", "json")
|
|
5297
|
+
).action(async (options) => {
|
|
5298
|
+
writeContextResult(
|
|
5299
|
+
await runContext({
|
|
5300
|
+
workflow: options.workflow,
|
|
5301
|
+
base: options.base,
|
|
5302
|
+
format: options.format
|
|
5303
|
+
}),
|
|
5304
|
+
options
|
|
5305
|
+
);
|
|
3195
5306
|
});
|
|
3196
5307
|
return program;
|
|
3197
5308
|
};
|