truthmark 1.2.3 → 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 +92 -36
- package/README.es.md +85 -29
- package/README.md +41 -26
- package/README.ru.md +84 -28
- package/README.zh.md +82 -26
- package/dist/main.js +2865 -1012
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -245,6 +245,7 @@ var resolveWorktreePath = (repository, relativePath) => {
|
|
|
245
245
|
};
|
|
246
246
|
|
|
247
247
|
// src/templates/init-files.ts
|
|
248
|
+
import path3 from "path";
|
|
248
249
|
import { stringify } from "yaml";
|
|
249
250
|
|
|
250
251
|
// src/config/schema.ts
|
|
@@ -265,7 +266,7 @@ var DEFAULT_PLATFORMS = [
|
|
|
265
266
|
var truthmarkConfigSchema = {
|
|
266
267
|
type: "object",
|
|
267
268
|
additionalProperties: false,
|
|
268
|
-
required: ["version", "authority"
|
|
269
|
+
required: ["version", "authority"],
|
|
269
270
|
properties: {
|
|
270
271
|
version: {
|
|
271
272
|
type: "integer",
|
|
@@ -361,16 +362,6 @@ var truthmarkConfigSchema = {
|
|
|
361
362
|
items: {
|
|
362
363
|
type: "string"
|
|
363
364
|
}
|
|
364
|
-
},
|
|
365
|
-
realization: {
|
|
366
|
-
type: "object",
|
|
367
|
-
additionalProperties: false,
|
|
368
|
-
required: ["enabled"],
|
|
369
|
-
properties: {
|
|
370
|
-
enabled: {
|
|
371
|
-
type: "boolean"
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
365
|
}
|
|
375
366
|
}
|
|
376
367
|
};
|
|
@@ -382,7 +373,7 @@ var DEFAULT_DOCS_HIERARCHY = {
|
|
|
382
373
|
ai: "docs/ai",
|
|
383
374
|
standards: "docs/standards",
|
|
384
375
|
architecture: "docs/architecture",
|
|
385
|
-
|
|
376
|
+
truth: "docs/truth"
|
|
386
377
|
},
|
|
387
378
|
routing: {
|
|
388
379
|
root_index: "docs/truthmark/areas.md",
|
|
@@ -397,7 +388,7 @@ var DEFAULT_AUTHORITY = [
|
|
|
397
388
|
`${DEFAULT_DOCS_HIERARCHY.roots.ai}/**/*.md`,
|
|
398
389
|
`${DEFAULT_DOCS_HIERARCHY.roots.standards}/**/*.md`,
|
|
399
390
|
`${DEFAULT_DOCS_HIERARCHY.roots.architecture}/**/*.md`,
|
|
400
|
-
`${DEFAULT_DOCS_HIERARCHY.roots.
|
|
391
|
+
`${DEFAULT_DOCS_HIERARCHY.roots.truth}/**/*.md`
|
|
401
392
|
];
|
|
402
393
|
var DEFAULT_INSTRUCTION_TARGETS = ["AGENTS.md"];
|
|
403
394
|
var createDefaultRawConfig = () => ({
|
|
@@ -414,10 +405,7 @@ var createDefaultRawConfig = () => ({
|
|
|
414
405
|
required: [],
|
|
415
406
|
recommended: ["status", "doc_type", "last_reviewed", "source_of_truth"]
|
|
416
407
|
},
|
|
417
|
-
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
418
|
-
realization: {
|
|
419
|
-
enabled: true
|
|
420
|
-
}
|
|
408
|
+
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
421
409
|
});
|
|
422
410
|
var createDefaultConfig = () => ({
|
|
423
411
|
version: 1,
|
|
@@ -438,13 +426,299 @@ var createDefaultConfig = () => ({
|
|
|
438
426
|
required: [],
|
|
439
427
|
recommended: ["status", "doc_type", "last_reviewed", "source_of_truth"]
|
|
440
428
|
},
|
|
441
|
-
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
442
|
-
realization: {
|
|
443
|
-
enabled: true
|
|
444
|
-
}
|
|
429
|
+
ignore: ["node_modules/**", "vendor/**", "dist/**", "build/**"]
|
|
445
430
|
});
|
|
446
431
|
|
|
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
|
+
};
|
|
712
|
+
|
|
447
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;
|
|
448
722
|
var renderConfigTemplate = () => {
|
|
449
723
|
return stringify(createDefaultRawConfig());
|
|
450
724
|
};
|
|
@@ -455,13 +729,17 @@ var renderHierarchicalAreasIndexTemplate = (config) => {
|
|
|
455
729
|
const defaultArea = config.docs.routing.defaultArea;
|
|
456
730
|
const childPath = `${config.docs.routing.areaFilesRoot}/${defaultArea}.md`;
|
|
457
731
|
const title = titleCase(defaultArea);
|
|
732
|
+
const sourceOfTruth = resolveRelativePath(
|
|
733
|
+
config.docs.routing.rootIndex,
|
|
734
|
+
".truthmark/config.yml"
|
|
735
|
+
);
|
|
458
736
|
return [
|
|
459
737
|
"---",
|
|
460
738
|
"status: active",
|
|
461
739
|
"doc_type: route-index",
|
|
462
|
-
|
|
740
|
+
`last_reviewed: ${currentDate()}`,
|
|
463
741
|
"source_of_truth:",
|
|
464
|
-
|
|
742
|
+
` - ${sourceOfTruth}`,
|
|
465
743
|
"---",
|
|
466
744
|
"",
|
|
467
745
|
"# Truthmark Areas",
|
|
@@ -483,15 +761,17 @@ var renderHierarchicalAreasIndexTemplate = (config) => {
|
|
|
483
761
|
var renderChildAreaTemplate = (config) => {
|
|
484
762
|
const defaultArea = config.docs.routing.defaultArea;
|
|
485
763
|
const title = titleCase(defaultArea);
|
|
486
|
-
const
|
|
487
|
-
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");
|
|
488
768
|
return [
|
|
489
769
|
"---",
|
|
490
770
|
"status: active",
|
|
491
771
|
"doc_type: area-route",
|
|
492
|
-
|
|
772
|
+
`last_reviewed: ${currentDate()}`,
|
|
493
773
|
"source_of_truth:",
|
|
494
|
-
|
|
774
|
+
` - ${sourceOfTruth}`,
|
|
495
775
|
"---",
|
|
496
776
|
"",
|
|
497
777
|
`# ${title} Areas`,
|
|
@@ -499,7 +779,11 @@ var renderChildAreaTemplate = (config) => {
|
|
|
499
779
|
`## ${title}`,
|
|
500
780
|
"",
|
|
501
781
|
"Truth documents:",
|
|
502
|
-
|
|
782
|
+
"```yaml",
|
|
783
|
+
"truth_documents:",
|
|
784
|
+
` - path: ${leafTruthDoc}`,
|
|
785
|
+
" kind: behavior",
|
|
786
|
+
"```",
|
|
503
787
|
"",
|
|
504
788
|
"Code surface:",
|
|
505
789
|
"- src/**",
|
|
@@ -509,41 +793,51 @@ var renderChildAreaTemplate = (config) => {
|
|
|
509
793
|
""
|
|
510
794
|
].join("\n");
|
|
511
795
|
};
|
|
512
|
-
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
|
+
);
|
|
513
802
|
return [
|
|
514
803
|
"---",
|
|
515
804
|
"status: active",
|
|
516
805
|
"doc_type: index",
|
|
517
|
-
|
|
806
|
+
`last_reviewed: ${currentDate()}`,
|
|
518
807
|
"source_of_truth:",
|
|
519
|
-
|
|
808
|
+
` - ${sourceOfTruth}`,
|
|
520
809
|
"---",
|
|
521
810
|
"",
|
|
522
|
-
"#
|
|
811
|
+
"# Truth Docs",
|
|
523
812
|
"",
|
|
524
|
-
"This directory is an index for current
|
|
813
|
+
"This directory is an index for current truth docs organized by the configured Truthmark hierarchy.",
|
|
525
814
|
"",
|
|
526
|
-
"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`.",
|
|
527
816
|
""
|
|
528
817
|
].join("\n");
|
|
529
818
|
};
|
|
530
|
-
var
|
|
819
|
+
var renderTruthDomainReadmeTemplate = (config) => {
|
|
531
820
|
const defaultArea = config.docs.routing.defaultArea;
|
|
532
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
|
+
);
|
|
533
827
|
return [
|
|
534
828
|
"---",
|
|
535
829
|
"status: active",
|
|
536
830
|
"doc_type: index",
|
|
537
|
-
|
|
831
|
+
`last_reviewed: ${currentDate()}`,
|
|
538
832
|
"source_of_truth:",
|
|
539
|
-
` -
|
|
833
|
+
` - ${sourceOfTruth}`,
|
|
540
834
|
"---",
|
|
541
835
|
"",
|
|
542
|
-
`# ${title}
|
|
836
|
+
`# ${title} Truth Docs`,
|
|
543
837
|
"",
|
|
544
|
-
`This directory indexes bounded ${title.toLowerCase()}
|
|
838
|
+
`This directory indexes bounded ${title.toLowerCase()} truth docs.`,
|
|
545
839
|
"",
|
|
546
|
-
"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.",
|
|
547
841
|
"",
|
|
548
842
|
"Current leaf docs:",
|
|
549
843
|
"",
|
|
@@ -551,13 +845,19 @@ var renderFeatureDomainReadmeTemplate = (config) => {
|
|
|
551
845
|
""
|
|
552
846
|
].join("\n");
|
|
553
847
|
};
|
|
554
|
-
var
|
|
555
|
-
var
|
|
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 = () => {
|
|
556
855
|
return [
|
|
557
856
|
"---",
|
|
558
857
|
"status: active",
|
|
559
|
-
"doc_type:
|
|
560
|
-
"
|
|
858
|
+
"doc_type: behavior",
|
|
859
|
+
"truth_kind: behavior",
|
|
860
|
+
`last_reviewed: ${currentDate()}`,
|
|
561
861
|
"source_of_truth:",
|
|
562
862
|
" - {{source_of_truth}}",
|
|
563
863
|
"---",
|
|
@@ -585,7 +885,7 @@ var renderFeatureDocTemplateFile = () => {
|
|
|
585
885
|
"Keep README.md files as indexes only.",
|
|
586
886
|
"-->",
|
|
587
887
|
"",
|
|
588
|
-
"This doc was created from the editable
|
|
888
|
+
"This doc was created from the editable behavior-doc template at {{template_path}}.",
|
|
589
889
|
"",
|
|
590
890
|
"## Current Behavior",
|
|
591
891
|
"",
|
|
@@ -637,29 +937,134 @@ var renderFeatureDocTemplateFile = () => {
|
|
|
637
937
|
""
|
|
638
938
|
].join("\n");
|
|
639
939
|
};
|
|
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
|
+
};
|
|
640
1038
|
var renderTemplate = (template, values) => {
|
|
641
1039
|
return Object.entries(values).reduce((rendered, [key, value]) => {
|
|
642
1040
|
return rendered.split(`{{${key}}}`).join(value);
|
|
643
1041
|
}, template);
|
|
644
1042
|
};
|
|
645
|
-
var
|
|
1043
|
+
var renderBehaviorLeafDocTemplate = (config, template = renderBehaviorDocTemplateFile()) => {
|
|
646
1044
|
const defaultArea = config.docs.routing.defaultArea;
|
|
647
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();
|
|
648
1052
|
return renderTemplate(template, {
|
|
649
1053
|
area: defaultArea,
|
|
650
1054
|
contracts: "- External contracts should link to the nearest canonical contract doc when one exists.",
|
|
651
|
-
core_rules: "-
|
|
1055
|
+
core_rules: "- Truth README files are indexes; behavior truth belongs in bounded leaf docs.",
|
|
652
1056
|
current_behavior: "- Document current behavior here when implementation changes make repository truth incomplete.",
|
|
653
|
-
decision:
|
|
1057
|
+
decision: `- Decision (${today}): Truth README files are indexes; behavior truth belongs in bounded leaf docs.`,
|
|
654
1058
|
flows_and_states: "- None beyond current behavior.",
|
|
655
1059
|
maintenance_notes: "- Update this doc when routed implementation changes alter current behavior, rules, contracts, or decisions.",
|
|
656
1060
|
non_goals: "- This doc is not a catch-all for unrelated repository behavior.",
|
|
657
1061
|
purpose: `Describe why the default ${title.toLowerCase()} behavior surface exists and what outcome it protects.`,
|
|
658
1062
|
rationale: "Bounded leaf docs keep agent context focused and prevent large products from accumulating unreviewable feature manuals.",
|
|
659
1063
|
scope: `This bounded leaf truth doc owns the default ${title.toLowerCase()} behavior surface created by Truthmark.`,
|
|
660
|
-
source_of_truth:
|
|
661
|
-
template_path:
|
|
662
|
-
title: `${title} Overview
|
|
1064
|
+
source_of_truth: sourceOfTruth,
|
|
1065
|
+
template_path: BEHAVIOR_DOC_TEMPLATE_PATH,
|
|
1066
|
+
title: `${title} Overview`,
|
|
1067
|
+
truth_kind: "behavior"
|
|
663
1068
|
});
|
|
664
1069
|
};
|
|
665
1070
|
|
|
@@ -745,7 +1150,7 @@ import fs7 from "fs/promises";
|
|
|
745
1150
|
// src/config/load.ts
|
|
746
1151
|
import fs4 from "fs/promises";
|
|
747
1152
|
import { Ajv } from "ajv";
|
|
748
|
-
import { parse } from "yaml";
|
|
1153
|
+
import { parse as parse2 } from "yaml";
|
|
749
1154
|
var ajv = new Ajv({ allErrors: true });
|
|
750
1155
|
var validateTruthmarkConfig = ajv.compile(truthmarkConfigSchema);
|
|
751
1156
|
var toConfigDiagnostic = (message, file) => {
|
|
@@ -762,12 +1167,13 @@ var normalizeConfig = (rawConfig) => {
|
|
|
762
1167
|
roots: { ...DEFAULT_DOCS_HIERARCHY.roots },
|
|
763
1168
|
routing: { ...DEFAULT_DOCS_HIERARCHY.routing }
|
|
764
1169
|
};
|
|
1170
|
+
const roots = { ...DEFAULT_DOCS_HIERARCHY.roots, ...rawDocs.roots };
|
|
765
1171
|
return {
|
|
766
1172
|
version: rawConfig.version,
|
|
767
1173
|
platforms: rawConfig.platforms ?? [...DEFAULT_PLATFORMS],
|
|
768
1174
|
docs: {
|
|
769
1175
|
layout: rawDocs.layout,
|
|
770
|
-
roots
|
|
1176
|
+
roots,
|
|
771
1177
|
routing: {
|
|
772
1178
|
rootIndex: rawDocs.routing.root_index,
|
|
773
1179
|
areaFilesRoot: rawDocs.routing.area_files_root,
|
|
@@ -781,10 +1187,7 @@ var normalizeConfig = (rawConfig) => {
|
|
|
781
1187
|
required: rawConfig.frontmatter?.required ?? [],
|
|
782
1188
|
recommended: rawConfig.frontmatter?.recommended ?? []
|
|
783
1189
|
},
|
|
784
|
-
ignore: rawConfig.ignore ?? []
|
|
785
|
-
realization: {
|
|
786
|
-
enabled: rawConfig.realization.enabled
|
|
787
|
-
}
|
|
1190
|
+
ignore: rawConfig.ignore ?? []
|
|
788
1191
|
};
|
|
789
1192
|
};
|
|
790
1193
|
var loadConfig = async (rootDir) => {
|
|
@@ -806,7 +1209,7 @@ var loadConfig = async (rootDir) => {
|
|
|
806
1209
|
}
|
|
807
1210
|
let parsedConfig;
|
|
808
1211
|
try {
|
|
809
|
-
parsedConfig =
|
|
1212
|
+
parsedConfig = parse2(source);
|
|
810
1213
|
} catch (error) {
|
|
811
1214
|
return {
|
|
812
1215
|
status: "invalid",
|
|
@@ -845,8 +1248,7 @@ var loadConfig = async (rootDir) => {
|
|
|
845
1248
|
import fs5 from "fs/promises";
|
|
846
1249
|
import fg from "fast-glob";
|
|
847
1250
|
var KNOWN_DEFAULT_ROOTS = [
|
|
848
|
-
DEFAULT_DOCS_HIERARCHY.roots.
|
|
849
|
-
"docs/features/current",
|
|
1251
|
+
DEFAULT_DOCS_HIERARCHY.roots.truth,
|
|
850
1252
|
"docs/api",
|
|
851
1253
|
DEFAULT_DOCS_HIERARCHY.roots.architecture,
|
|
852
1254
|
DEFAULT_DOCS_HIERARCHY.roots.standards,
|
|
@@ -860,20 +1262,28 @@ var hasMarkdownFiles = async (rootDir, root) => {
|
|
|
860
1262
|
});
|
|
861
1263
|
return matches.length > 0;
|
|
862
1264
|
};
|
|
863
|
-
var
|
|
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) => {
|
|
864
1274
|
try {
|
|
865
|
-
return await fs5.readFile(resolveRepoPath(rootDir,
|
|
1275
|
+
return await fs5.readFile(resolveRepoPath(rootDir, BEHAVIOR_DOC_TEMPLATE_PATH), "utf8");
|
|
866
1276
|
} catch (error) {
|
|
867
1277
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
868
|
-
return
|
|
1278
|
+
return renderBehaviorDocTemplateFile();
|
|
869
1279
|
}
|
|
870
1280
|
throw error;
|
|
871
1281
|
}
|
|
872
1282
|
};
|
|
873
1283
|
var scaffoldHierarchy = async (rootDir, config) => {
|
|
874
1284
|
const results = [];
|
|
875
|
-
const
|
|
876
|
-
const
|
|
1285
|
+
const truthDocsRoot = truthRoot2(config);
|
|
1286
|
+
const truthDomainRoot = `${truthDocsRoot}/${config.docs.routing.defaultArea}`;
|
|
877
1287
|
const childRoutePath = `${config.docs.routing.areaFilesRoot}/${config.docs.routing.defaultArea}.md`;
|
|
878
1288
|
results.push(
|
|
879
1289
|
await ensureRepoFile(
|
|
@@ -882,30 +1292,55 @@ var scaffoldHierarchy = async (rootDir, config) => {
|
|
|
882
1292
|
renderHierarchicalAreasIndexTemplate(config)
|
|
883
1293
|
)
|
|
884
1294
|
);
|
|
885
|
-
|
|
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
|
+
);
|
|
886
1305
|
results.push(
|
|
887
1306
|
await ensureRepoFile(
|
|
888
1307
|
rootDir,
|
|
889
|
-
`${
|
|
890
|
-
|
|
1308
|
+
`${truthDomainRoot}/README.md`,
|
|
1309
|
+
renderTruthDomainReadmeTemplate(config)
|
|
891
1310
|
)
|
|
892
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
|
+
);
|
|
893
1318
|
results.push(
|
|
894
1319
|
await ensureRepoFile(
|
|
895
1320
|
rootDir,
|
|
896
|
-
|
|
897
|
-
|
|
1321
|
+
ARCHITECTURE_DOC_TEMPLATE_PATH,
|
|
1322
|
+
renderArchitectureDocTemplateFile()
|
|
898
1323
|
)
|
|
899
1324
|
);
|
|
900
1325
|
results.push(
|
|
901
|
-
await ensureRepoFile(rootDir,
|
|
1326
|
+
await ensureRepoFile(rootDir, WORKFLOW_DOC_TEMPLATE_PATH, renderWorkflowDocTemplateFile())
|
|
1327
|
+
);
|
|
1328
|
+
results.push(
|
|
1329
|
+
await ensureRepoFile(rootDir, OPERATIONS_DOC_TEMPLATE_PATH, renderOperationsDocTemplateFile())
|
|
1330
|
+
);
|
|
1331
|
+
results.push(
|
|
1332
|
+
await ensureRepoFile(
|
|
1333
|
+
rootDir,
|
|
1334
|
+
TEST_BEHAVIOR_DOC_TEMPLATE_PATH,
|
|
1335
|
+
renderTestBehaviorDocTemplateFile()
|
|
1336
|
+
)
|
|
902
1337
|
);
|
|
903
|
-
const
|
|
1338
|
+
const behaviorDocTemplate = await readBehaviorDocTemplate(rootDir);
|
|
904
1339
|
results.push(
|
|
905
1340
|
await ensureRepoFile(
|
|
906
1341
|
rootDir,
|
|
907
|
-
`${
|
|
908
|
-
|
|
1342
|
+
`${truthDomainRoot}/overview.md`,
|
|
1343
|
+
renderBehaviorLeafDocTemplate(config, behaviorDocTemplate)
|
|
909
1344
|
)
|
|
910
1345
|
);
|
|
911
1346
|
return results;
|
|
@@ -929,70 +1364,128 @@ var detectHierarchyMigrationDiagnostics = async (rootDir, config) => {
|
|
|
929
1364
|
return diagnostics;
|
|
930
1365
|
};
|
|
931
1366
|
|
|
932
|
-
// src/
|
|
933
|
-
var
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
+
|
|
1394
|
+
// src/agents/shared.ts
|
|
1395
|
+
var DECISION_TRUTH_INSTRUCTIONS = [
|
|
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."
|
|
939
1399
|
].join("\n");
|
|
940
1400
|
var EVIDENCE_AUTHORITY_INSTRUCTIONS = [
|
|
941
1401
|
"Repository instruction docs such as docs/ai/repo-rules.md remain instruction authority.",
|
|
942
1402
|
"Implementation code and canonical truth docs are inspected evidence for current behavior; they do not silently override workflow write boundaries."
|
|
943
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");
|
|
944
1409
|
var FEATURE_DOC_TEMPLATE_INSTRUCTIONS = [
|
|
945
|
-
"When creating or updating a
|
|
946
|
-
"
|
|
947
|
-
"
|
|
948
|
-
"
|
|
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) => {
|
|
1417
|
+
return [
|
|
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"
|
|
1423
|
+
].join("\n");
|
|
1424
|
+
};
|
|
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"
|
|
949
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");
|
|
1443
|
+
};
|
|
950
1444
|
var ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS = [
|
|
951
|
-
"Maintain architecture docs
|
|
952
|
-
"
|
|
953
|
-
"Keep architecture docs focused on structure and ownership; keep current product behavior in feature or contract docs."
|
|
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."
|
|
954
1447
|
].join("\n");
|
|
1448
|
+
var renderRouteFirstEvidenceGateSection = (subject, noImpactedDocOutcome) => {
|
|
1449
|
+
return [
|
|
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");
|
|
1458
|
+
};
|
|
1459
|
+
var renderTopologyEvidenceGateSection = () => {
|
|
1460
|
+
return [
|
|
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");
|
|
1466
|
+
};
|
|
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");
|
|
1474
|
+
};
|
|
955
1475
|
var defaultAgentConfig = () => {
|
|
956
1476
|
return createDefaultConfig();
|
|
957
1477
|
};
|
|
958
1478
|
var renderHierarchySummary = (config) => {
|
|
959
|
-
const
|
|
1479
|
+
const truthRoot3 = resolveTruthDocsRoot(config);
|
|
960
1480
|
return [
|
|
961
1481
|
"Truthmark hierarchy:",
|
|
962
1482
|
"- Config: .truthmark/config.yml",
|
|
963
1483
|
`- Root route index: ${config.docs.routing.rootIndex}`,
|
|
964
1484
|
`- Area route files: ${config.docs.routing.areaFilesRoot}/**/*.md`,
|
|
965
|
-
`-
|
|
1485
|
+
`- Truth docs: ${truthRoot3}/**/*.md`
|
|
966
1486
|
].join("\n");
|
|
967
1487
|
};
|
|
968
1488
|
|
|
969
|
-
// src/sync/report.ts
|
|
970
|
-
var renderBulletSection = (title, items) => {
|
|
971
|
-
return `${title}:
|
|
972
|
-
${items.map((item) => `- ${item}`).join("\n")}`;
|
|
973
|
-
};
|
|
974
|
-
var renderTruthSyncCompletedReport = (input) => {
|
|
975
|
-
return [
|
|
976
|
-
"Truth Sync: completed",
|
|
977
|
-
renderBulletSection("Changed code reviewed", input.changedCode),
|
|
978
|
-
renderBulletSection("Truth docs updated", input.truthDocsUpdated),
|
|
979
|
-
renderBulletSection("Notes", input.notes)
|
|
980
|
-
].join("\n\n");
|
|
981
|
-
};
|
|
982
|
-
var renderTruthSyncBlockedReport = (input) => {
|
|
983
|
-
const sections = [
|
|
984
|
-
"Truth Sync: blocked",
|
|
985
|
-
renderBulletSection("Reason", [input.reason])
|
|
986
|
-
];
|
|
987
|
-
if ((input.manualReviewFiles?.length ?? 0) > 0) {
|
|
988
|
-
sections.push(renderBulletSection("Files requiring manual review", input.manualReviewFiles));
|
|
989
|
-
}
|
|
990
|
-
sections.push(renderBulletSection("Next action", [input.nextAction]));
|
|
991
|
-
return [
|
|
992
|
-
...sections
|
|
993
|
-
].join("\n\n");
|
|
994
|
-
};
|
|
995
|
-
|
|
996
1489
|
// src/version.ts
|
|
997
1490
|
import fs6 from "fs";
|
|
998
1491
|
var packageJson = JSON.parse(
|
|
@@ -1000,134 +1493,287 @@ var packageJson = JSON.parse(
|
|
|
1000
1493
|
);
|
|
1001
1494
|
var TRUTHMARK_VERSION = packageJson.version;
|
|
1002
1495
|
|
|
1003
|
-
// src/agents/truth-sync.ts
|
|
1004
|
-
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.";
|
|
1005
|
-
var renderMarkdownExample = (content) => {
|
|
1006
|
-
return ["```md", content, "```"].join("\n");
|
|
1007
|
-
};
|
|
1008
|
-
var renderTruthSyncWorkerPrompt = () => {
|
|
1009
|
-
return `### Truth Sync Worker
|
|
1010
|
-
The parent provides the task focus and any repository context already gathered.
|
|
1011
|
-
Worker rules:
|
|
1012
|
-
- inspect relevant staged, unstaged, and untracked functional code directly
|
|
1013
|
-
- read .truthmark/config.yml, docs/truthmark/areas.md, and canonical truth docs directly
|
|
1014
|
-
- Code verification is parent-owned; report what was run or why it was not run
|
|
1015
|
-
- may write truth docs and docs/truthmark/areas.md only for Truth Sync alignment
|
|
1016
|
-
- must not rewrite functional code
|
|
1017
|
-
Return result in this shape:
|
|
1018
|
-
- status: completed | blocked
|
|
1019
|
-
- changedCodeReviewed: string[]
|
|
1020
|
-
- truthDocsUpdated: string[]
|
|
1021
|
-
- routingDocsUpdated: string[]
|
|
1022
|
-
- notes: string[]
|
|
1023
|
-
- blockedReason?: string
|
|
1024
|
-
- manualReviewFiles?: string[]`;
|
|
1025
|
-
};
|
|
1026
|
-
var renderTruthSyncSkillBody = (config = defaultAgentConfig()) => {
|
|
1027
|
-
return `---
|
|
1028
|
-
name: truthmark-sync
|
|
1029
|
-
description: Use automatically before finishing when functional code changed since the last successful Truth Sync, and when the user explicitly invokes /truthmark-sync, $truthmark-sync, or /truthmark:sync. Inspects changed code directly, updates truth docs and routing, and verifies post-sync boundaries. Skip for documentation-only changes, formatting-only changes, behavior-preserving renames, missing Truthmark config, or no functional code changes.
|
|
1030
|
-
argument-hint: Optional changed-code area, truth-doc area, or sync focus
|
|
1031
|
-
user-invocable: true
|
|
1032
|
-
truthmark-version: ${TRUTHMARK_VERSION}
|
|
1033
|
-
---
|
|
1034
|
-
|
|
1035
|
-
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.
|
|
1036
|
-
Invocations: ${TRUTH_SYNC_EXPLICIT_INVOCATIONS}
|
|
1037
|
-
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.
|
|
1038
|
-
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.
|
|
1039
|
-
Parent workflow:
|
|
1040
|
-
1. Inspect git status, staged changes, unstaged changes, and untracked files directly.
|
|
1041
|
-
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.
|
|
1042
|
-
3. Identify functional-code changes and the nearest truth docs or routing repairs.
|
|
1043
|
-
4. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1044
|
-
5. Code verification is parent-owned: follow repository instructions and task context, and report what ran or why it did not run.
|
|
1045
|
-
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.
|
|
1046
|
-
Topology quality gate:
|
|
1047
|
-
- before updating truth docs, verify the changed code resolves to a specific behavior-owned area and bounded truth owner
|
|
1048
|
-
- 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 feature doc
|
|
1049
|
-
- run Truth Structure before syncing when topology repair is safe and in scope
|
|
1050
|
-
- block and recommend Truth Structure when topology repair is unsafe, ambiguous, or outside the current task boundary
|
|
1051
|
-
- report the route files and changed code paths that require structure repair
|
|
1052
|
-
- README.md files are indexes, not Truth Sync targets
|
|
1053
|
-
- must not append behavior details to a feature README
|
|
1054
|
-
- create or update a bounded leaf truth doc when behavior changes do not fit an existing leaf doc
|
|
1055
|
-
${FEATURE_DOC_TEMPLATE_INSTRUCTIONS}
|
|
1056
|
-
${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
|
|
1057
|
-
Optional validation tooling:
|
|
1058
|
-
- you may run truthmark check when local tooling is available
|
|
1059
|
-
- do not require the truthmark binary; direct checkout inspection is the canonical path
|
|
1060
|
-
- optional validation must not replace agent judgment about docs and routing
|
|
1061
|
-
- update Product Decisions and Rationale when a behavior change comes from a decision change
|
|
1062
|
-
${renderHierarchySummary(config)}
|
|
1063
|
-
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1064
|
-
${renderTruthSyncWorkerPrompt()}
|
|
1065
|
-
Parent post-sync verification:
|
|
1066
|
-
- verify only truth docs and docs/truthmark/areas.md changed during sync
|
|
1067
|
-
- block on any unrelated diff caused by the sync step
|
|
1068
|
-
- block if functional code changed during sync
|
|
1069
|
-
- verify the worker report matches the required headings and sections
|
|
1070
|
-
- verify the updated docs correspond to the reviewed changed-code surface
|
|
1071
|
-
- blocked outcomes must preserve the working tree as-is: no rollback, no post-block cleanup edits, and manual-review reporting of any remaining files
|
|
1072
|
-
Report completion in this shape:
|
|
1073
|
-
${renderMarkdownExample(
|
|
1074
|
-
renderTruthSyncCompletedReport({
|
|
1075
|
-
changedCode: ["src/auth/session.ts"],
|
|
1076
|
-
truthDocsUpdated: ["docs/features/repository/overview.md"],
|
|
1077
|
-
notes: ["Updated session timeout behavior."]
|
|
1078
|
-
})
|
|
1079
|
-
)}
|
|
1080
|
-
Blocked report example:
|
|
1081
|
-
${renderMarkdownExample(
|
|
1082
|
-
renderTruthSyncBlockedReport({
|
|
1083
|
-
reason: "routing repair is not allowed",
|
|
1084
|
-
manualReviewFiles: ["docs/truthmark/areas.md"],
|
|
1085
|
-
nextAction: "update routing metadata and rerun Truth Sync"
|
|
1086
|
-
})
|
|
1087
|
-
)}`;
|
|
1088
|
-
};
|
|
1089
|
-
|
|
1090
|
-
// src/sync/policy.ts
|
|
1091
|
-
var TRUTH_SYNC_SKIP_REASONS = [
|
|
1092
|
-
"documentation-only change",
|
|
1093
|
-
"formatting-only change",
|
|
1094
|
-
"clearly behavior-preserving rename with no truth impact",
|
|
1095
|
-
"no Truthmark config exists yet",
|
|
1096
|
-
"no functional code changes"
|
|
1097
|
-
];
|
|
1098
|
-
|
|
1099
1496
|
// src/templates/agents-block.ts
|
|
1100
1497
|
var TRUTHMARK_BLOCK_START = "<!-- truthmark:start -->";
|
|
1101
1498
|
var TRUTHMARK_BLOCK_END = "<!-- truthmark:end -->";
|
|
1102
|
-
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
|
+
};
|
|
1103
1503
|
var renderAgentsBlock = (config = defaultAgentConfig()) => {
|
|
1104
|
-
const syncInvocations = trimPeriod(TRUTH_SYNC_EXPLICIT_INVOCATIONS);
|
|
1105
1504
|
return [
|
|
1106
1505
|
TRUTHMARK_BLOCK_START,
|
|
1107
1506
|
"## Truthmark Workflow",
|
|
1108
1507
|
"",
|
|
1109
|
-
`Generated by Truthmark ${TRUTHMARK_VERSION}. Rerun \`truthmark init\` after upgrades
|
|
1110
|
-
|
|
1111
|
-
"
|
|
1112
|
-
"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.",
|
|
1113
1512
|
"### Truth Sync",
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
"
|
|
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.",
|
|
1117
1518
|
TRUTHMARK_BLOCK_END
|
|
1118
1519
|
].join("\n");
|
|
1119
1520
|
};
|
|
1120
1521
|
|
|
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
|
+
---
|
|
1533
|
+
|
|
1534
|
+
# Default Principles
|
|
1535
|
+
|
|
1536
|
+
## Scope
|
|
1537
|
+
|
|
1538
|
+
This is a bootstrap standards baseline for repositories that adopt Truthmark.
|
|
1539
|
+
|
|
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
|
+
|
|
1121
1766
|
// src/agents/truth-check.ts
|
|
1122
|
-
var
|
|
1767
|
+
var renderMarkdownExample = (content) => {
|
|
1123
1768
|
return ["```md", content, "```"].join("\n");
|
|
1124
1769
|
};
|
|
1125
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.";
|
|
1126
|
-
var renderTruthCheckReportExample = () => {
|
|
1771
|
+
var renderTruthCheckReportExample = (config = defaultAgentConfig()) => {
|
|
1772
|
+
const rootRouteIndex = config.docs.routing.rootIndex;
|
|
1127
1773
|
return `Truth Check: completed
|
|
1128
1774
|
|
|
1129
1775
|
Files reviewed:
|
|
1130
|
-
-
|
|
1776
|
+
- ${rootRouteIndex}
|
|
1131
1777
|
|
|
1132
1778
|
Issues found:
|
|
1133
1779
|
- none
|
|
@@ -1135,13 +1781,23 @@ Issues found:
|
|
|
1135
1781
|
Fixes suggested:
|
|
1136
1782
|
- none
|
|
1137
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
|
+
|
|
1138
1793
|
Validation:
|
|
1139
1794
|
- truthmark check`;
|
|
1140
1795
|
};
|
|
1141
1796
|
var renderTruthCheckSkillBody = (config = defaultAgentConfig()) => {
|
|
1797
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1142
1798
|
return `---
|
|
1143
1799
|
name: truthmark-check
|
|
1144
|
-
description:
|
|
1800
|
+
description: ${workflow.description}
|
|
1145
1801
|
argument-hint: Optional area, doc path, or audit focus
|
|
1146
1802
|
user-invocable: true
|
|
1147
1803
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1155,51 +1811,70 @@ Invocations: ${TRUTH_CHECK_EXPLICIT_INVOCATIONS}
|
|
|
1155
1811
|
|
|
1156
1812
|
Truth Check is agent-led:
|
|
1157
1813
|
|
|
1158
|
-
- inspect .truthmark/config.yml, docs
|
|
1814
|
+
- inspect .truthmark/config.yml, ${config.docs.routing.rootIndex}, canonical docs, and relevant implementation directly
|
|
1159
1815
|
- ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1160
1816
|
- inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/
|
|
1161
1817
|
- check that current docs describe current code rather than historical plans
|
|
1162
|
-
- 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
|
|
1163
1820
|
- check that canonical behavior docs keep active Product Decisions and Rationale sections
|
|
1164
1821
|
- optionally run truthmark check when local tooling is available
|
|
1165
1822
|
- must not require the truthmark binary; direct inspection is always valid
|
|
1166
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()}
|
|
1167
1826
|
|
|
1168
1827
|
${renderHierarchySummary(config)}
|
|
1169
1828
|
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1170
1829
|
|
|
1171
1830
|
Report completion in this shape:
|
|
1172
1831
|
|
|
1173
|
-
${
|
|
1832
|
+
${renderMarkdownExample(renderTruthCheckReportExample(config))}`;
|
|
1174
1833
|
};
|
|
1175
1834
|
|
|
1176
1835
|
// src/agents/truth-document.ts
|
|
1177
|
-
var
|
|
1836
|
+
var renderMarkdownExample2 = (content) => {
|
|
1178
1837
|
return ["```md", content, "```"].join("\n");
|
|
1179
1838
|
};
|
|
1180
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.";
|
|
1181
|
-
var renderTruthDocumentReportExample = () => {
|
|
1840
|
+
var renderTruthDocumentReportExample = (config = defaultAgentConfig()) => {
|
|
1841
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
1182
1842
|
return `Truth Document: completed
|
|
1183
1843
|
|
|
1184
1844
|
Implementation reviewed:
|
|
1185
|
-
- src/
|
|
1845
|
+
- src/routing/area-resolver.ts
|
|
1186
1846
|
|
|
1187
1847
|
Truth docs created:
|
|
1188
|
-
-
|
|
1848
|
+
- ${truthDocsRoot}/contracts.md
|
|
1189
1849
|
|
|
1190
1850
|
Truth docs updated:
|
|
1191
|
-
-
|
|
1851
|
+
- ${truthDocsRoot}/check-diagnostics.md
|
|
1852
|
+
|
|
1853
|
+
Truth docs restructured:
|
|
1854
|
+
- ${truthDocsRoot}/check-diagnostics.md
|
|
1192
1855
|
|
|
1193
1856
|
Routing updated:
|
|
1194
|
-
- docs
|
|
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
|
+
])}
|
|
1195
1869
|
|
|
1196
1870
|
Notes:
|
|
1197
|
-
- Documented
|
|
1871
|
+
- Documented routing and behavior from route handlers and tests.`;
|
|
1198
1872
|
};
|
|
1199
1873
|
var renderTruthDocumentSkillBody = (config = defaultAgentConfig()) => {
|
|
1874
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
1200
1875
|
return `---
|
|
1201
1876
|
name: truthmark-document
|
|
1202
|
-
description:
|
|
1877
|
+
description: ${workflow.description}
|
|
1203
1878
|
argument-hint: Optional implemented behavior, API endpoint, route, controller, package, or truth-doc area to document
|
|
1204
1879
|
user-invocable: true
|
|
1205
1880
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1222,44 +1897,71 @@ Truth Document is manual and implementation-first:
|
|
|
1222
1897
|
- block and recommend Truth Structure when routing repair is unsafe, ambiguous, or outside the task boundary
|
|
1223
1898
|
- keep feature README.md files as indexes rather than truth-document targets
|
|
1224
1899
|
- create or update bounded leaf truth docs when behavior does not fit an existing leaf doc
|
|
1225
|
-
- keep
|
|
1900
|
+
- keep behavior truth docs behavior-oriented, not endpoint-oriented, unless the endpoint itself is the behavior boundary
|
|
1226
1901
|
- keep API endpoint details in the nearest contract truth doc when such a doc owns the API contract
|
|
1227
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}
|
|
1228
1913
|
${FEATURE_DOC_TEMPLATE_INSTRUCTIONS}
|
|
1914
|
+
${renderTruthDocRestructureGateSection(
|
|
1915
|
+
"Truth Document may restructure only truth docs for the implemented behavior being documented."
|
|
1916
|
+
)}
|
|
1229
1917
|
${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
|
|
1230
1918
|
${renderHierarchySummary(config)}
|
|
1231
1919
|
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1232
1920
|
|
|
1233
1921
|
Report completion in this shape:
|
|
1234
|
-
${
|
|
1922
|
+
${renderMarkdownExample2(renderTruthDocumentReportExample(config))}`;
|
|
1235
1923
|
};
|
|
1236
1924
|
|
|
1237
1925
|
// src/agents/truth-structure.ts
|
|
1238
|
-
var
|
|
1926
|
+
var renderMarkdownExample3 = (content) => {
|
|
1239
1927
|
return ["```md", content, "```"].join("\n");
|
|
1240
1928
|
};
|
|
1241
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.";
|
|
1242
|
-
var renderTruthStructureReportExample = () => {
|
|
1930
|
+
var renderTruthStructureReportExample = (config = defaultAgentConfig()) => {
|
|
1931
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
1243
1932
|
return `Truth Structure: completed
|
|
1244
1933
|
Topology reviewed:
|
|
1245
1934
|
- controllers: src/auth/**
|
|
1246
|
-
- docs root:
|
|
1247
|
-
- route files: docs
|
|
1935
|
+
- docs root: ${truthDocsRoot}
|
|
1936
|
+
- route files: ${config.docs.routing.rootIndex}
|
|
1248
1937
|
Areas reviewed:
|
|
1249
1938
|
- src/auth/**
|
|
1250
1939
|
Routing updated:
|
|
1251
|
-
- docs
|
|
1940
|
+
- ${config.docs.routing.rootIndex}
|
|
1252
1941
|
Truth docs created:
|
|
1253
|
-
-
|
|
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
|
+
])}
|
|
1254
1954
|
Topology decisions:
|
|
1255
1955
|
- Added an Authentication area because session behavior has a distinct code surface and truth owner.
|
|
1256
1956
|
Notes:
|
|
1257
1957
|
- Added an Authentication area for session behavior.`;
|
|
1258
1958
|
};
|
|
1259
1959
|
var renderTruthStructureSkillBody = (config = defaultAgentConfig()) => {
|
|
1960
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
1961
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1260
1962
|
return `---
|
|
1261
1963
|
name: truthmark-structure
|
|
1262
|
-
description:
|
|
1964
|
+
description: ${workflow.description}
|
|
1263
1965
|
argument-hint: Optional area, directory, or routing concern
|
|
1264
1966
|
user-invocable: true
|
|
1265
1967
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1268,46 +1970,57 @@ truthmark-version: ${TRUTHMARK_VERSION}
|
|
|
1268
1970
|
Use this skill to design or repair Truthmark area structure.
|
|
1269
1971
|
Invocations: ${TRUTH_STRUCTURE_EXPLICIT_INVOCATIONS}
|
|
1270
1972
|
Truth Structure is agent-native:
|
|
1271
|
-
- inspect repository layout, current docs, .truthmark/config.yml, docs
|
|
1973
|
+
- inspect repository layout, current docs, .truthmark/config.yml, ${config.docs.routing.rootIndex}, and relevant code directly
|
|
1272
1974
|
- ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1273
1975
|
- inspect the configured root route index at ${config.docs.routing.rootIndex} and relevant child route files under ${config.docs.routing.areaFilesRoot}/
|
|
1274
1976
|
- define areas by product or behavior ownership, not by mechanical directory mirroring
|
|
1275
|
-
- create or repair docs
|
|
1977
|
+
- create or repair ${config.docs.routing.rootIndex}
|
|
1276
1978
|
- create starter truth docs when useful and when they belong in the canonical current-truth surface
|
|
1277
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.
|
|
1278
1980
|
- Starter truth docs must include ## Product Decisions and ## Rationale sections.
|
|
1279
1981
|
${FEATURE_DOC_TEMPLATE_INSTRUCTIONS}
|
|
1280
|
-
- use
|
|
1982
|
+
- use ${truthDocsRoot}/**, docs/architecture/**, or docs/standards/** for current truth destinations
|
|
1281
1983
|
- use only canonical current-truth destinations for starter truth docs
|
|
1282
1984
|
- keep active Product Decisions and Rationale in the canonical doc that owns the behavior
|
|
1283
1985
|
- preserve unrelated authored content
|
|
1284
1986
|
## Topology Governance
|
|
1285
|
-
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.
|
|
1286
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.
|
|
1287
|
-
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}
|
|
1288
1995
|
Topology pressure signals:
|
|
1289
1996
|
- one area maps broad code such as src/**, app/**, server/**, services/**, or packages/**
|
|
1290
1997
|
- one area maps multiple unrelated controllers, route groups, services, or bounded contexts
|
|
1291
1998
|
- one truth doc owns unrelated behaviors or unrelated endpoint families
|
|
1292
|
-
- the configured
|
|
1999
|
+
- the configured truth root has many direct non-index docs
|
|
1293
2000
|
- a changed controller, route, or service cannot map to a specific behavior doc
|
|
1294
|
-
- 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
|
|
1295
2002
|
- endpoint or controller names reveal domains missing from ${config.docs.routing.areaFilesRoot}/**
|
|
1296
2003
|
Use these review thresholds as guidance:
|
|
1297
|
-
- more than 10 direct
|
|
2004
|
+
- more than 10 direct truth docs in one folder
|
|
1298
2005
|
- more than 15 leaf areas in one child route file
|
|
1299
2006
|
- more than 8 truth docs mapped to one area
|
|
1300
2007
|
- more than 5 controllers mapped through one catch-all area
|
|
1301
2008
|
Repair rules:
|
|
1302
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
|
|
1303
2011
|
- create route files under ${config.docs.routing.areaFilesRoot}/ when a product/domain boundary is clear
|
|
1304
|
-
- create
|
|
2012
|
+
- create behavior truth docs under the configured truth root only when behavior lacks a current doc
|
|
1305
2013
|
- README.md files are indexes, not Truth Sync targets
|
|
1306
|
-
- prefer bounded leaf truth docs at <
|
|
1307
|
-
- keep
|
|
2014
|
+
- prefer bounded leaf truth docs at <truth-root>/<domain>/<behavior>.md
|
|
2015
|
+
- keep behavior truth docs behavior-oriented, not endpoint-oriented
|
|
1308
2016
|
- keep API endpoint details in the nearest contract truth doc when such a doc exists
|
|
1309
2017
|
- update routing so future Truth Sync can target small docs
|
|
1310
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()}
|
|
1311
2024
|
${ARCHITECTURE_DOC_BOUNDARY_INSTRUCTIONS}
|
|
1312
2025
|
- Do not finish topology repair with routed canonical current-truth docs missing Product Decisions or Rationale sections.
|
|
1313
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.
|
|
@@ -1319,21 +2032,164 @@ Portable fallback:
|
|
|
1319
2032
|
${renderHierarchySummary(config)}
|
|
1320
2033
|
${DECISION_TRUTH_INSTRUCTIONS}
|
|
1321
2034
|
Report completion in this shape:
|
|
1322
|
-
${
|
|
2035
|
+
${renderMarkdownExample3(renderTruthStructureReportExample(config))}`;
|
|
1323
2036
|
};
|
|
1324
2037
|
|
|
1325
|
-
// src/
|
|
1326
|
-
var
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
var
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
+
)}`;
|
|
2179
|
+
};
|
|
2180
|
+
|
|
2181
|
+
// src/templates/codex-skills.ts
|
|
2182
|
+
var TRUTHMARK_STRUCTURE_SKILL_PATH = ".codex/skills/truthmark-structure/SKILL.md";
|
|
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";
|
|
2186
|
+
var TRUTHMARK_SYNC_SKILL_PATH = ".codex/skills/truthmark-sync/SKILL.md";
|
|
2187
|
+
var TRUTHMARK_SYNC_SKILL_METADATA_PATH = ".codex/skills/truthmark-sync/agents/openai.yaml";
|
|
2188
|
+
var TRUTHMARK_REALIZE_SKILL_PATH = ".codex/skills/truthmark-realize/SKILL.md";
|
|
2189
|
+
var TRUTHMARK_REALIZE_SKILL_METADATA_PATH = ".codex/skills/truthmark-realize/agents/openai.yaml";
|
|
2190
|
+
var TRUTHMARK_CHECK_SKILL_PATH = ".codex/skills/truthmark-check/SKILL.md";
|
|
2191
|
+
var TRUTHMARK_CHECK_SKILL_METADATA_PATH = ".codex/skills/truthmark-check/agents/openai.yaml";
|
|
2192
|
+
var TRUTHMARK_GEMINI_STRUCTURE_COMMAND_PATH = ".gemini/commands/truthmark/structure.toml";
|
|
1337
2193
|
var TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH = ".gemini/commands/truthmark/document.toml";
|
|
1338
2194
|
var TRUTHMARK_GEMINI_SYNC_COMMAND_PATH = ".gemini/commands/truthmark/sync.toml";
|
|
1339
2195
|
var TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH = ".gemini/commands/truthmark/realize.toml";
|
|
@@ -1366,13 +2222,14 @@ var renderTruthmarkStructureLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1366
2222
|
return renderTruthStructureSkillBody(config);
|
|
1367
2223
|
};
|
|
1368
2224
|
var renderTruthmarkStructureSkillMetadata = () => {
|
|
2225
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1369
2226
|
return `interface:
|
|
1370
|
-
display_name: "
|
|
1371
|
-
short_description: "
|
|
1372
|
-
default_prompt: "
|
|
2227
|
+
display_name: "${workflow.displayName}"
|
|
2228
|
+
short_description: "${workflow.shortDescription}"
|
|
2229
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1373
2230
|
|
|
1374
2231
|
policy:
|
|
1375
|
-
allow_implicit_invocation:
|
|
2232
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1376
2233
|
|
|
1377
2234
|
truthmark:
|
|
1378
2235
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1386,13 +2243,14 @@ var renderTruthmarkDocumentLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1386
2243
|
return renderTruthDocumentSkillBody(config);
|
|
1387
2244
|
};
|
|
1388
2245
|
var renderTruthmarkDocumentSkillMetadata = () => {
|
|
2246
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
1389
2247
|
return `interface:
|
|
1390
|
-
display_name: "
|
|
1391
|
-
short_description: "
|
|
1392
|
-
default_prompt: "
|
|
2248
|
+
display_name: "${workflow.displayName}"
|
|
2249
|
+
short_description: "${workflow.shortDescription}"
|
|
2250
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1393
2251
|
|
|
1394
2252
|
policy:
|
|
1395
|
-
allow_implicit_invocation:
|
|
2253
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1396
2254
|
|
|
1397
2255
|
truthmark:
|
|
1398
2256
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1406,23 +2264,26 @@ var renderTruthmarkSyncLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1406
2264
|
return renderTruthSyncSkillBody(config);
|
|
1407
2265
|
};
|
|
1408
2266
|
var renderTruthmarkSyncSkillMetadata = () => {
|
|
2267
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
1409
2268
|
return `interface:
|
|
1410
|
-
display_name: "
|
|
1411
|
-
short_description: "
|
|
1412
|
-
default_prompt: "
|
|
2269
|
+
display_name: "${workflow.displayName}"
|
|
2270
|
+
short_description: "${workflow.shortDescription}"
|
|
2271
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1413
2272
|
|
|
1414
2273
|
policy:
|
|
1415
|
-
allow_implicit_invocation:
|
|
2274
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1416
2275
|
|
|
1417
2276
|
truthmark:
|
|
1418
2277
|
version: "${TRUTHMARK_VERSION}"
|
|
1419
2278
|
refresh_command: "truthmark init"
|
|
1420
2279
|
`;
|
|
1421
2280
|
};
|
|
1422
|
-
var renderTruthmarkRealizeSkillBody = () => {
|
|
2281
|
+
var renderTruthmarkRealizeSkillBody = (config = defaultAgentConfig()) => {
|
|
2282
|
+
const truthDocsRoot = resolveTruthDocsRoot(config);
|
|
2283
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1423
2284
|
return `---
|
|
1424
2285
|
name: truthmark-realize
|
|
1425
|
-
description:
|
|
2286
|
+
description: ${workflow.description}
|
|
1426
2287
|
argument-hint: Optional truth doc path, area, or desired code behavior to realize
|
|
1427
2288
|
user-invocable: true
|
|
1428
2289
|
truthmark-version: ${TRUTHMARK_VERSION}
|
|
@@ -1442,13 +2303,18 @@ Truth Realize is doc-first:
|
|
|
1442
2303
|
|
|
1443
2304
|
Workflow:
|
|
1444
2305
|
|
|
1445
|
-
1. Read the updated truth docs named by the user, or infer the relevant docs from docs
|
|
1446
|
-
2. Read .truthmark/config.yml, docs
|
|
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.
|
|
1447
2308
|
3. ${EVIDENCE_AUTHORITY_INSTRUCTIONS}
|
|
1448
|
-
|
|
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.
|
|
1449
2314
|
5. Do not edit truth docs or truth routing while realizing those docs.
|
|
1450
2315
|
6. Run relevant tests for the changed code.
|
|
1451
2316
|
7. Report changed code files and verification steps.
|
|
2317
|
+
${renderHierarchySummary(config)}
|
|
1452
2318
|
|
|
1453
2319
|
Read and write boundaries:
|
|
1454
2320
|
|
|
@@ -1462,7 +2328,7 @@ Report completion in this shape:
|
|
|
1462
2328
|
Truth Realize: completed
|
|
1463
2329
|
|
|
1464
2330
|
Truth docs used:
|
|
1465
|
-
-
|
|
2331
|
+
- ${truthDocsRoot}/authentication/session-timeout.md
|
|
1466
2332
|
|
|
1467
2333
|
Code updated:
|
|
1468
2334
|
- src/auth/session.ts
|
|
@@ -1472,20 +2338,21 @@ Verification:
|
|
|
1472
2338
|
\`\`\`
|
|
1473
2339
|
`;
|
|
1474
2340
|
};
|
|
1475
|
-
var renderTruthmarkRealizeSkill = () => {
|
|
1476
|
-
return renderTruthmarkRealizeSkillBody();
|
|
2341
|
+
var renderTruthmarkRealizeSkill = (config = defaultAgentConfig()) => {
|
|
2342
|
+
return renderTruthmarkRealizeSkillBody(config);
|
|
1477
2343
|
};
|
|
1478
|
-
var renderTruthmarkRealizeLocalSkill = () => {
|
|
1479
|
-
return renderTruthmarkRealizeSkillBody();
|
|
2344
|
+
var renderTruthmarkRealizeLocalSkill = (config = defaultAgentConfig()) => {
|
|
2345
|
+
return renderTruthmarkRealizeSkillBody(config);
|
|
1480
2346
|
};
|
|
1481
2347
|
var renderTruthmarkRealizeSkillMetadata = () => {
|
|
2348
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1482
2349
|
return `interface:
|
|
1483
|
-
display_name: "
|
|
1484
|
-
short_description: "
|
|
1485
|
-
default_prompt: "
|
|
2350
|
+
display_name: "${workflow.displayName}"
|
|
2351
|
+
short_description: "${workflow.shortDescription}"
|
|
2352
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1486
2353
|
|
|
1487
2354
|
policy:
|
|
1488
|
-
allow_implicit_invocation:
|
|
2355
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1489
2356
|
|
|
1490
2357
|
truthmark:
|
|
1491
2358
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1499,13 +2366,14 @@ var renderTruthmarkCheckLocalSkill = (config = defaultAgentConfig()) => {
|
|
|
1499
2366
|
return renderTruthCheckSkillBody(config);
|
|
1500
2367
|
};
|
|
1501
2368
|
var renderTruthmarkCheckSkillMetadata = () => {
|
|
2369
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1502
2370
|
return `interface:
|
|
1503
|
-
display_name: "
|
|
1504
|
-
short_description: "
|
|
1505
|
-
default_prompt: "
|
|
2371
|
+
display_name: "${workflow.displayName}"
|
|
2372
|
+
short_description: "${workflow.shortDescription}"
|
|
2373
|
+
default_prompt: "${workflow.defaultPrompt}"
|
|
1506
2374
|
|
|
1507
2375
|
policy:
|
|
1508
|
-
allow_implicit_invocation:
|
|
2376
|
+
allow_implicit_invocation: ${workflow.allowImplicitInvocation}
|
|
1509
2377
|
|
|
1510
2378
|
truthmark:
|
|
1511
2379
|
version: "${TRUTHMARK_VERSION}"
|
|
@@ -1513,308 +2381,101 @@ truthmark:
|
|
|
1513
2381
|
`;
|
|
1514
2382
|
};
|
|
1515
2383
|
var renderTruthmarkGeminiStructureCommand = (config = defaultAgentConfig()) => {
|
|
2384
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1516
2385
|
return renderGeminiCommand(
|
|
1517
|
-
|
|
2386
|
+
workflow.description,
|
|
1518
2387
|
renderTruthStructureSkillBody(config)
|
|
1519
2388
|
);
|
|
1520
2389
|
};
|
|
1521
2390
|
var renderTruthmarkGeminiDocumentCommand = (config = defaultAgentConfig()) => {
|
|
2391
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
1522
2392
|
return renderGeminiCommand(
|
|
1523
|
-
|
|
2393
|
+
workflow.description,
|
|
1524
2394
|
renderTruthDocumentSkillBody(config)
|
|
1525
2395
|
);
|
|
1526
2396
|
};
|
|
1527
2397
|
var renderTruthmarkGeminiSyncCommand = (config = defaultAgentConfig()) => {
|
|
2398
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
1528
2399
|
return renderGeminiCommand(
|
|
1529
|
-
|
|
2400
|
+
workflow.description,
|
|
1530
2401
|
renderTruthSyncSkillBody(config)
|
|
1531
2402
|
);
|
|
1532
2403
|
};
|
|
1533
|
-
var renderTruthmarkGeminiRealizeCommand = () => {
|
|
2404
|
+
var renderTruthmarkGeminiRealizeCommand = (config = defaultAgentConfig()) => {
|
|
2405
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1534
2406
|
return renderGeminiCommand(
|
|
1535
|
-
|
|
1536
|
-
renderTruthmarkRealizeSkillBody()
|
|
2407
|
+
workflow.description,
|
|
2408
|
+
renderTruthmarkRealizeSkillBody(config)
|
|
1537
2409
|
);
|
|
1538
2410
|
};
|
|
1539
2411
|
var renderTruthmarkGeminiCheckCommand = (config = defaultAgentConfig()) => {
|
|
2412
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1540
2413
|
return renderGeminiCommand(
|
|
1541
|
-
|
|
2414
|
+
workflow.description,
|
|
1542
2415
|
renderTruthCheckSkillBody(config)
|
|
1543
2416
|
);
|
|
1544
2417
|
};
|
|
1545
2418
|
var renderTruthmarkCopilotStructurePrompt = (config = defaultAgentConfig()) => {
|
|
2419
|
+
const workflow = getTruthmarkWorkflow("truthmark-structure");
|
|
1546
2420
|
return renderCopilotPromptFile(
|
|
1547
|
-
|
|
2421
|
+
workflow.description,
|
|
1548
2422
|
renderTruthStructureSkillBody(config)
|
|
1549
2423
|
);
|
|
1550
2424
|
};
|
|
1551
2425
|
var renderTruthmarkCopilotDocumentPrompt = (config = defaultAgentConfig()) => {
|
|
2426
|
+
const workflow = getTruthmarkWorkflow("truthmark-document");
|
|
1552
2427
|
return renderCopilotPromptFile(
|
|
1553
|
-
|
|
2428
|
+
workflow.description,
|
|
1554
2429
|
renderTruthDocumentSkillBody(config)
|
|
1555
2430
|
);
|
|
1556
2431
|
};
|
|
1557
2432
|
var renderTruthmarkCopilotSyncPrompt = (config = defaultAgentConfig()) => {
|
|
2433
|
+
const workflow = getTruthmarkWorkflow("truthmark-sync");
|
|
1558
2434
|
return renderCopilotPromptFile(
|
|
1559
|
-
|
|
2435
|
+
workflow.description,
|
|
1560
2436
|
renderTruthSyncSkillBody(config)
|
|
1561
2437
|
);
|
|
1562
2438
|
};
|
|
1563
|
-
var renderTruthmarkCopilotRealizePrompt = () => {
|
|
2439
|
+
var renderTruthmarkCopilotRealizePrompt = (config = defaultAgentConfig()) => {
|
|
2440
|
+
const workflow = getTruthmarkWorkflow("truthmark-realize");
|
|
1564
2441
|
return renderCopilotPromptFile(
|
|
1565
|
-
|
|
1566
|
-
renderTruthmarkRealizeSkillBody()
|
|
2442
|
+
workflow.description,
|
|
2443
|
+
renderTruthmarkRealizeSkillBody(config)
|
|
1567
2444
|
);
|
|
1568
2445
|
};
|
|
1569
2446
|
var renderTruthmarkCopilotCheckPrompt = (config = defaultAgentConfig()) => {
|
|
2447
|
+
const workflow = getTruthmarkWorkflow("truthmark-check");
|
|
1570
2448
|
return renderCopilotPromptFile(
|
|
1571
|
-
|
|
2449
|
+
workflow.description,
|
|
1572
2450
|
renderTruthCheckSkillBody(config)
|
|
1573
2451
|
);
|
|
1574
2452
|
};
|
|
1575
2453
|
|
|
1576
|
-
// src/templates/
|
|
1577
|
-
var
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
-
|
|
1597
|
-
|
|
1598
|
-
- Each document should have one primary responsibility.
|
|
1599
|
-
- Each class of fact should have one canonical source.
|
|
1600
|
-
- Architecture docs describe system structure, module boundaries, runtime topology, persistence boundaries, cross-cutting contracts, and generated-surface ownership.
|
|
1601
|
-
- Do not put ordinary feature behavior in architecture docs.
|
|
1602
|
-
- Verification should be explicit, and skipped checks should state why.
|
|
1603
|
-
- Missing, stale, broad, overloaded, or unrouteable documentation topology should be repaired through AI-native structure workflow before agents create more generic truth docs.
|
|
1604
|
-
- Installed repository workflows should remain usable from committed files even when the Truthmark CLI is unavailable.
|
|
1605
|
-
`
|
|
1606
|
-
},
|
|
1607
|
-
{
|
|
1608
|
-
path: "docs/standards/documentation-governance.md",
|
|
1609
|
-
content: `---
|
|
1610
|
-
status: active
|
|
1611
|
-
doc_type: standard
|
|
1612
|
-
last_reviewed: 2026-05-03
|
|
1613
|
-
source_of_truth:
|
|
1614
|
-
- README.md
|
|
1615
|
-
---
|
|
1616
|
-
|
|
1617
|
-
# Documentation Governance
|
|
1618
|
-
|
|
1619
|
-
## Core Rules
|
|
1620
|
-
|
|
1621
|
-
- Each document should have one primary responsibility.
|
|
1622
|
-
- Each class of fact should have one canonical source.
|
|
1623
|
-
- Current implementation, reusable standards, and future proposals should be stored separately.
|
|
1624
|
-
- Generated helper output is never canonical truth.
|
|
1625
|
-
- Architecture docs describe structure and ownership; feature docs describe current product behavior.
|
|
1626
|
-
|
|
1627
|
-
## Truthmark Implications
|
|
1628
|
-
|
|
1629
|
-
- Truth Sync should extend mapped docs first, create an area-local doc second, and create a new area only as a last resort.
|
|
1630
|
-
- Weak routing produces weak truth maintenance.
|
|
1631
|
-
- Missing, stale, broad, overloaded, or unrouteable routing should trigger Truth Structure before more generic feature docs are created.
|
|
1632
|
-
`
|
|
1633
|
-
}
|
|
1634
|
-
];
|
|
1635
|
-
var renderDefaultStandards = (documents) => {
|
|
1636
|
-
const existingPaths = new Set(documents.map((document) => document.path));
|
|
1637
|
-
return DEFAULT_STANDARDS.filter((template) => !existingPaths.has(template.path));
|
|
1638
|
-
};
|
|
1639
|
-
|
|
1640
|
-
// src/init/init.ts
|
|
1641
|
-
var escapeRegExp = (value) => {
|
|
1642
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1643
|
-
};
|
|
1644
|
-
var MANAGED_WORKFLOW_HEADING = "## Truthmark Workflow";
|
|
1645
|
-
var LEGACY_MANAGED_LINES = [
|
|
1646
|
-
"### Truth Sync",
|
|
1647
|
-
"- may read changed functional code files",
|
|
1648
|
-
"- may write truth docs only",
|
|
1649
|
-
"- must not rewrite functional code"
|
|
1650
|
-
];
|
|
1651
|
-
var CANONICAL_MANAGED_LINES = /* @__PURE__ */ new Set(
|
|
1652
|
-
[
|
|
1653
|
-
...renderAgentsBlock().split("\n").map((line) => line.trim()).filter(
|
|
1654
|
-
(line) => line.length > 0 && line !== TRUTHMARK_BLOCK_START && line !== TRUTHMARK_BLOCK_END
|
|
1655
|
-
),
|
|
1656
|
-
...LEGACY_MANAGED_LINES
|
|
1657
|
-
]
|
|
1658
|
-
);
|
|
1659
|
-
var countCanonicalManagedLineMatches = (lines) => {
|
|
1660
|
-
return lines.reduce((matchCount, line) => {
|
|
1661
|
-
return CANONICAL_MANAGED_LINES.has(line.trim()) ? matchCount + 1 : matchCount;
|
|
1662
|
-
}, 0);
|
|
1663
|
-
};
|
|
1664
|
-
var isManagedChunk = (lines, minimumMatches) => {
|
|
1665
|
-
return countCanonicalManagedLineMatches(lines) >= minimumMatches;
|
|
1666
|
-
};
|
|
1667
|
-
var removeTrailingManagedChunk = (preservedLines) => {
|
|
1668
|
-
let startIndex = -1;
|
|
1669
|
-
for (let index = preservedLines.length - 1; index >= 0; index -= 1) {
|
|
1670
|
-
if (preservedLines[index].trim() === MANAGED_WORKFLOW_HEADING) {
|
|
1671
|
-
startIndex = index;
|
|
1672
|
-
break;
|
|
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)
|
|
1673
2476
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
return;
|
|
1677
|
-
}
|
|
1678
|
-
const candidateChunk = preservedLines.slice(startIndex);
|
|
1679
|
-
const looksManaged = isManagedChunk(candidateChunk, 4);
|
|
1680
|
-
if (looksManaged) {
|
|
1681
|
-
preservedLines.splice(startIndex);
|
|
1682
|
-
}
|
|
1683
|
-
};
|
|
1684
|
-
var normalizeLegacyInstructionPreamble = (content) => {
|
|
1685
|
-
return content.replaceAll(
|
|
1686
|
-
"Use that file as the primary repository instruction source for Codex.",
|
|
1687
|
-
"Use that file as the primary repository instruction source for this agent."
|
|
1688
|
-
).replaceAll("Codex-specific:", "Agent-specific:");
|
|
1689
|
-
};
|
|
1690
|
-
var upsertManagedBlock = (existingContent, block) => {
|
|
1691
|
-
if (!existingContent || existingContent.trim().length === 0) {
|
|
1692
|
-
return block;
|
|
1693
|
-
}
|
|
1694
|
-
const normalizedExistingContent = normalizeLegacyInstructionPreamble(existingContent);
|
|
1695
|
-
const startMarkerPattern = new RegExp(escapeRegExp(TRUTHMARK_BLOCK_START), "g");
|
|
1696
|
-
const endMarkerPattern = new RegExp(escapeRegExp(TRUTHMARK_BLOCK_END), "g");
|
|
1697
|
-
const managedBlockPattern = new RegExp(
|
|
1698
|
-
`${escapeRegExp(TRUTHMARK_BLOCK_START)}[\\s\\S]*?${escapeRegExp(TRUTHMARK_BLOCK_END)}`,
|
|
1699
|
-
"g"
|
|
1700
|
-
);
|
|
1701
|
-
const completeBlocks = normalizedExistingContent.match(managedBlockPattern) ?? [];
|
|
1702
|
-
const startCount = normalizedExistingContent.match(startMarkerPattern)?.length ?? 0;
|
|
1703
|
-
const endCount = normalizedExistingContent.match(endMarkerPattern)?.length ?? 0;
|
|
1704
|
-
if (startCount === 1 && endCount === 1 && completeBlocks.length === 1) {
|
|
1705
|
-
return normalizedExistingContent.replace(managedBlockPattern, block);
|
|
1706
|
-
}
|
|
1707
|
-
const preservedLines = [];
|
|
1708
|
-
let insideManagedBlock = false;
|
|
1709
|
-
let managedLines = [];
|
|
1710
|
-
for (const line of normalizedExistingContent.split("\n")) {
|
|
1711
|
-
const trimmedLine = line.trim();
|
|
1712
|
-
if (trimmedLine === TRUTHMARK_BLOCK_START) {
|
|
1713
|
-
if (insideManagedBlock && !isManagedChunk(managedLines, 2)) {
|
|
1714
|
-
preservedLines.push(...managedLines);
|
|
1715
|
-
}
|
|
1716
|
-
insideManagedBlock = true;
|
|
1717
|
-
managedLines = [];
|
|
1718
|
-
continue;
|
|
1719
|
-
}
|
|
1720
|
-
if (trimmedLine === TRUTHMARK_BLOCK_END) {
|
|
1721
|
-
if (insideManagedBlock) {
|
|
1722
|
-
insideManagedBlock = false;
|
|
1723
|
-
managedLines = [];
|
|
1724
|
-
continue;
|
|
1725
|
-
}
|
|
1726
|
-
if (!insideManagedBlock) {
|
|
1727
|
-
removeTrailingManagedChunk(preservedLines);
|
|
1728
|
-
}
|
|
1729
|
-
continue;
|
|
1730
|
-
}
|
|
1731
|
-
if (insideManagedBlock) {
|
|
1732
|
-
managedLines.push(line);
|
|
1733
|
-
continue;
|
|
1734
|
-
}
|
|
1735
|
-
preservedLines.push(line);
|
|
1736
|
-
}
|
|
1737
|
-
if (insideManagedBlock && !isManagedChunk(managedLines, 2)) {
|
|
1738
|
-
preservedLines.push(...managedLines);
|
|
1739
|
-
}
|
|
1740
|
-
const preservedContent = preservedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1741
|
-
if (preservedContent.length === 0) {
|
|
1742
|
-
return block;
|
|
1743
|
-
}
|
|
1744
|
-
return `${preservedContent}
|
|
1745
|
-
|
|
1746
|
-
${block}`;
|
|
1747
|
-
};
|
|
1748
|
-
var writeManagedAgentsFile = async (rootDir, path4 = "AGENTS.md", block) => {
|
|
1749
|
-
let existingContent = null;
|
|
1750
|
-
try {
|
|
1751
|
-
existingContent = await fs7.readFile(resolveRepoPath(rootDir, path4), "utf8");
|
|
1752
|
-
} catch (error) {
|
|
1753
|
-
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
1754
|
-
throw error;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
return writeRepoFile(rootDir, path4, upsertManagedBlock(existingContent, block));
|
|
1758
|
-
};
|
|
1759
|
-
var diagnosticCategoryForPath = (filePath) => {
|
|
1760
|
-
if (filePath === "AGENTS.md") {
|
|
1761
|
-
return "truth-sync";
|
|
1762
|
-
}
|
|
1763
|
-
if (filePath === "CLAUDE.md" || filePath === "GEMINI.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-")) {
|
|
1764
|
-
return "truth-sync";
|
|
1765
|
-
}
|
|
1766
|
-
if (filePath.startsWith(".codex/skills/truthmark-structure/")) {
|
|
1767
|
-
return "truth-sync";
|
|
1768
|
-
}
|
|
1769
|
-
if (filePath.startsWith(".codex/skills/truthmark-document/")) {
|
|
1770
|
-
return "truth-sync";
|
|
1771
|
-
}
|
|
1772
|
-
if (filePath.startsWith(".codex/skills/truthmark-sync/")) {
|
|
1773
|
-
return "truth-sync";
|
|
1774
|
-
}
|
|
1775
|
-
if (filePath.startsWith(".codex/skills/truthmark-realize/")) {
|
|
1776
|
-
return "realization";
|
|
1777
|
-
}
|
|
1778
|
-
if (filePath.startsWith(".gemini/commands/truthmark/realize")) {
|
|
1779
|
-
return "realization";
|
|
1780
|
-
}
|
|
1781
|
-
if (filePath.startsWith(".gemini/commands/truthmark/")) {
|
|
1782
|
-
return "truth-sync";
|
|
1783
|
-
}
|
|
1784
|
-
if (filePath.startsWith(".codex/skills/truthmark-check/")) {
|
|
1785
|
-
return "truth-sync";
|
|
1786
|
-
}
|
|
1787
|
-
if (filePath === "docs/truthmark/areas.md") {
|
|
1788
|
-
return "authority";
|
|
1789
|
-
}
|
|
1790
|
-
return "config";
|
|
1791
|
-
};
|
|
1792
|
-
var workflowSkillFiles = (basePath, config) => {
|
|
1793
|
-
const files = [
|
|
1794
|
-
{
|
|
1795
|
-
path: `${basePath}/truthmark-structure/SKILL.md`,
|
|
1796
|
-
content: renderTruthmarkStructureLocalSkill(config)
|
|
1797
|
-
},
|
|
1798
|
-
{
|
|
1799
|
-
path: `${basePath}/truthmark-document/SKILL.md`,
|
|
1800
|
-
content: renderTruthmarkDocumentLocalSkill(config)
|
|
1801
|
-
},
|
|
1802
|
-
{
|
|
1803
|
-
path: `${basePath}/truthmark-sync/SKILL.md`,
|
|
1804
|
-
content: renderTruthmarkSyncLocalSkill(config)
|
|
1805
|
-
},
|
|
1806
|
-
{
|
|
1807
|
-
path: `${basePath}/truthmark-check/SKILL.md`,
|
|
1808
|
-
content: renderTruthmarkCheckLocalSkill(config)
|
|
1809
|
-
}
|
|
1810
|
-
];
|
|
1811
|
-
if (config.realization.enabled) {
|
|
1812
|
-
files.push({
|
|
1813
|
-
path: `${basePath}/truthmark-realize/SKILL.md`,
|
|
1814
|
-
content: renderTruthmarkRealizeLocalSkill()
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
return files;
|
|
2477
|
+
];
|
|
2478
|
+
return files;
|
|
1818
2479
|
};
|
|
1819
2480
|
var codexFiles = (config) => {
|
|
1820
2481
|
const files = [
|
|
@@ -1849,20 +2510,16 @@ var codexFiles = (config) => {
|
|
|
1849
2510
|
{
|
|
1850
2511
|
path: TRUTHMARK_CHECK_SKILL_METADATA_PATH,
|
|
1851
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()
|
|
1852
2521
|
}
|
|
1853
2522
|
];
|
|
1854
|
-
if (config.realization.enabled) {
|
|
1855
|
-
files.push(
|
|
1856
|
-
{
|
|
1857
|
-
path: TRUTHMARK_REALIZE_SKILL_PATH,
|
|
1858
|
-
content: renderTruthmarkRealizeSkill()
|
|
1859
|
-
},
|
|
1860
|
-
{
|
|
1861
|
-
path: TRUTHMARK_REALIZE_SKILL_METADATA_PATH,
|
|
1862
|
-
content: renderTruthmarkRealizeSkillMetadata()
|
|
1863
|
-
}
|
|
1864
|
-
);
|
|
1865
|
-
}
|
|
1866
2523
|
return files;
|
|
1867
2524
|
};
|
|
1868
2525
|
var copilotFiles = (config, block) => {
|
|
@@ -1883,19 +2540,17 @@ var copilotFiles = (config, block) => {
|
|
|
1883
2540
|
{
|
|
1884
2541
|
path: TRUTHMARK_COPILOT_CHECK_PROMPT_PATH,
|
|
1885
2542
|
content: renderTruthmarkCopilotCheckPrompt(config)
|
|
2543
|
+
},
|
|
2544
|
+
{
|
|
2545
|
+
path: TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH,
|
|
2546
|
+
content: renderTruthmarkCopilotRealizePrompt(config)
|
|
1886
2547
|
}
|
|
1887
2548
|
];
|
|
1888
|
-
if (config.realization.enabled) {
|
|
1889
|
-
files.push({
|
|
1890
|
-
path: TRUTHMARK_COPILOT_REALIZE_PROMPT_PATH,
|
|
1891
|
-
content: renderTruthmarkCopilotRealizePrompt()
|
|
1892
|
-
});
|
|
1893
|
-
}
|
|
1894
2549
|
return files;
|
|
1895
2550
|
};
|
|
1896
2551
|
var instructionBlockFiles = (paths, block) => {
|
|
1897
|
-
return paths.map((
|
|
1898
|
-
path:
|
|
2552
|
+
return paths.map((path12) => ({
|
|
2553
|
+
path: path12,
|
|
1899
2554
|
content: block,
|
|
1900
2555
|
managedBlock: true
|
|
1901
2556
|
}));
|
|
@@ -1932,119 +2587,279 @@ var filesForPlatform = (platform, config, block) => {
|
|
|
1932
2587
|
path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
|
|
1933
2588
|
content: renderTruthmarkGeminiCheckCommand(config)
|
|
1934
2589
|
},
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
}
|
|
1940
|
-
] : []
|
|
2590
|
+
{
|
|
2591
|
+
path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
|
|
2592
|
+
content: renderTruthmarkGeminiRealizeCommand(config)
|
|
2593
|
+
}
|
|
1941
2594
|
];
|
|
1942
2595
|
}
|
|
1943
2596
|
};
|
|
1944
|
-
var
|
|
1945
|
-
|
|
1946
|
-
return writeManagedAgentsFile(rootDir, file.path, file.content);
|
|
1947
|
-
}
|
|
1948
|
-
return writeRepoFile(rootDir, file.path, file.content);
|
|
1949
|
-
};
|
|
1950
|
-
var messageForWriteResult = (result) => {
|
|
1951
|
-
switch (result.status) {
|
|
1952
|
-
case "created":
|
|
1953
|
-
return `Created ${result.path}.`;
|
|
1954
|
-
case "updated":
|
|
1955
|
-
return `Updated ${result.path}.`;
|
|
1956
|
-
case "unchanged":
|
|
1957
|
-
return `Unchanged ${result.path}.`;
|
|
1958
|
-
}
|
|
1959
|
-
};
|
|
1960
|
-
var writeDiagnostics = (results) => {
|
|
1961
|
-
return results.map((result) => ({
|
|
1962
|
-
category: diagnosticCategoryForPath(result.path),
|
|
1963
|
-
severity: "action",
|
|
1964
|
-
message: messageForWriteResult(result),
|
|
1965
|
-
file: result.path
|
|
1966
|
-
}));
|
|
1967
|
-
};
|
|
1968
|
-
var runInit = async (cwd) => {
|
|
1969
|
-
const repository = await getGitRepository(cwd);
|
|
1970
|
-
const rootDir = repository.worktreePath;
|
|
1971
|
-
const loadedConfig = await loadConfig(rootDir);
|
|
1972
|
-
if (!loadedConfig.config) {
|
|
1973
|
-
return {
|
|
1974
|
-
command: "init",
|
|
1975
|
-
summary: "Truthmark init requires .truthmark/config.yml. Run truthmark config first, review the hierarchy, then run truthmark init.",
|
|
1976
|
-
diagnostics: loadedConfig.diagnostics,
|
|
1977
|
-
data: {
|
|
1978
|
-
repositoryRoot: repository.repositoryRoot,
|
|
1979
|
-
worktreePath: repository.worktreePath,
|
|
1980
|
-
branchName: repository.branchName,
|
|
1981
|
-
isDetached: repository.isDetached,
|
|
1982
|
-
isUnborn: repository.isUnborn
|
|
1983
|
-
}
|
|
1984
|
-
};
|
|
1985
|
-
}
|
|
1986
|
-
const defaultStandards = renderDefaultStandards([]);
|
|
1987
|
-
const results = [];
|
|
1988
|
-
for (const template of defaultStandards) {
|
|
1989
|
-
results.push(await ensureRepoFile(rootDir, template.path, template.content));
|
|
1990
|
-
}
|
|
1991
|
-
const config = loadedConfig.config;
|
|
1992
|
-
results.push(...await scaffoldHierarchy(rootDir, config));
|
|
1993
|
-
const migrationDiagnostics = await detectHierarchyMigrationDiagnostics(rootDir, config);
|
|
1994
|
-
const block = renderAgentsBlock(config);
|
|
1995
|
-
const platformFiles = [
|
|
2597
|
+
var renderGeneratedSurfaces = (config, block = renderAgentsBlock(config)) => {
|
|
2598
|
+
const files = [
|
|
1996
2599
|
...instructionBlockFiles(config.instructionTargets, block),
|
|
1997
2600
|
...config.platforms.flatMap((platform) => filesForPlatform(platform, config, block))
|
|
1998
2601
|
];
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
)
|
|
2002
|
-
for (const file of uniquePlatformFiles) {
|
|
2003
|
-
results.push(await writePlatformFile(rootDir, file));
|
|
2004
|
-
}
|
|
2005
|
-
const changedResults = results.filter((result) => result.status !== "unchanged");
|
|
2006
|
-
return {
|
|
2007
|
-
command: "init",
|
|
2008
|
-
summary: changedResults.length > 0 ? "Initialized or updated the Truthmark repository scaffold." : "Truthmark repository scaffold is already up to date.",
|
|
2009
|
-
diagnostics: [...writeDiagnostics(results), ...migrationDiagnostics],
|
|
2010
|
-
data: {
|
|
2011
|
-
repositoryRoot: repository.repositoryRoot,
|
|
2012
|
-
worktreePath: repository.worktreePath,
|
|
2013
|
-
branchName: repository.branchName,
|
|
2014
|
-
isDetached: repository.isDetached,
|
|
2015
|
-
isUnborn: repository.isUnborn
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2602
|
+
return Array.from(new Map(files.map((file) => [file.path, file])).values()).sort(
|
|
2603
|
+
(left, right) => left.path.localeCompare(right.path)
|
|
2604
|
+
);
|
|
2018
2605
|
};
|
|
2019
2606
|
|
|
2020
|
-
// src/
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
// src/markdown/hash.ts
|
|
2025
|
-
import { createHash } from "crypto";
|
|
2026
|
-
var hashText = (value) => {
|
|
2027
|
-
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
2607
|
+
// src/init/init.ts
|
|
2608
|
+
var escapeRegExp = (value) => {
|
|
2609
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2028
2610
|
};
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2611
|
+
var MANAGED_WORKFLOW_HEADING = "## Truthmark Workflow";
|
|
2612
|
+
var LEGACY_MANAGED_LINES = [
|
|
2613
|
+
"### Truth Sync",
|
|
2614
|
+
"- may read changed functional code files",
|
|
2615
|
+
"- may write truth docs only",
|
|
2616
|
+
"- must not rewrite functional code"
|
|
2617
|
+
];
|
|
2618
|
+
var CANONICAL_MANAGED_LINES = /* @__PURE__ */ new Set(
|
|
2619
|
+
[
|
|
2620
|
+
...renderAgentsBlock().split("\n").map((line) => line.trim()).filter(
|
|
2621
|
+
(line) => line.length > 0 && line !== TRUTHMARK_BLOCK_START && line !== TRUTHMARK_BLOCK_END
|
|
2622
|
+
),
|
|
2623
|
+
...LEGACY_MANAGED_LINES
|
|
2624
|
+
]
|
|
2625
|
+
);
|
|
2626
|
+
var countCanonicalManagedLineMatches = (lines) => {
|
|
2627
|
+
return lines.reduce((matchCount, line) => {
|
|
2628
|
+
return CANONICAL_MANAGED_LINES.has(line.trim()) ? matchCount + 1 : matchCount;
|
|
2629
|
+
}, 0);
|
|
2038
2630
|
};
|
|
2039
|
-
var
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2631
|
+
var isManagedChunk = (lines, minimumMatches) => {
|
|
2632
|
+
return countCanonicalManagedLineMatches(lines) >= minimumMatches;
|
|
2633
|
+
};
|
|
2634
|
+
var removeTrailingManagedChunk = (preservedLines) => {
|
|
2635
|
+
let startIndex = -1;
|
|
2636
|
+
for (let index = preservedLines.length - 1; index >= 0; index -= 1) {
|
|
2637
|
+
if (preservedLines[index].trim() === MANAGED_WORKFLOW_HEADING) {
|
|
2638
|
+
startIndex = index;
|
|
2639
|
+
break;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (startIndex === -1) {
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
const candidateChunk = preservedLines.slice(startIndex);
|
|
2646
|
+
const looksManaged = isManagedChunk(candidateChunk, 4);
|
|
2647
|
+
if (looksManaged) {
|
|
2648
|
+
preservedLines.splice(startIndex);
|
|
2649
|
+
}
|
|
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
|
+
};
|
|
2663
|
+
var upsertManagedBlock = (existingContent, block) => {
|
|
2664
|
+
if (!existingContent || existingContent.trim().length === 0) {
|
|
2665
|
+
return block;
|
|
2666
|
+
}
|
|
2667
|
+
const normalizedExistingContent = normalizeLegacyInstructionPreamble(existingContent);
|
|
2668
|
+
const startMarkerPattern = new RegExp(escapeRegExp(TRUTHMARK_BLOCK_START), "g");
|
|
2669
|
+
const endMarkerPattern = new RegExp(escapeRegExp(TRUTHMARK_BLOCK_END), "g");
|
|
2670
|
+
const managedBlockPattern = new RegExp(
|
|
2671
|
+
`${escapeRegExp(TRUTHMARK_BLOCK_START)}[\\s\\S]*?${escapeRegExp(TRUTHMARK_BLOCK_END)}`,
|
|
2672
|
+
"g"
|
|
2673
|
+
);
|
|
2674
|
+
const completeBlocks = normalizedExistingContent.match(managedBlockPattern) ?? [];
|
|
2675
|
+
const startCount = normalizedExistingContent.match(startMarkerPattern)?.length ?? 0;
|
|
2676
|
+
const endCount = normalizedExistingContent.match(endMarkerPattern)?.length ?? 0;
|
|
2677
|
+
if (startCount === 1 && endCount === 1 && completeBlocks.length === 1) {
|
|
2678
|
+
return normalizedExistingContent.replace(managedBlockPattern, block);
|
|
2679
|
+
}
|
|
2680
|
+
const preservedLines = [];
|
|
2681
|
+
let insideManagedBlock = false;
|
|
2682
|
+
let managedLines = [];
|
|
2683
|
+
for (const line of normalizedExistingContent.split("\n")) {
|
|
2684
|
+
const trimmedLine = line.trim();
|
|
2685
|
+
if (trimmedLine === TRUTHMARK_BLOCK_START) {
|
|
2686
|
+
if (insideManagedBlock && !isManagedChunk(managedLines, 2)) {
|
|
2687
|
+
preservedLines.push(...managedLines);
|
|
2688
|
+
}
|
|
2689
|
+
insideManagedBlock = true;
|
|
2690
|
+
managedLines = [];
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
if (trimmedLine === TRUTHMARK_BLOCK_END) {
|
|
2694
|
+
if (insideManagedBlock) {
|
|
2695
|
+
insideManagedBlock = false;
|
|
2696
|
+
managedLines = [];
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
if (!insideManagedBlock) {
|
|
2700
|
+
removeTrailingManagedChunk(preservedLines);
|
|
2701
|
+
}
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
if (insideManagedBlock) {
|
|
2705
|
+
managedLines.push(line);
|
|
2706
|
+
continue;
|
|
2707
|
+
}
|
|
2708
|
+
preservedLines.push(line);
|
|
2709
|
+
}
|
|
2710
|
+
if (insideManagedBlock && !isManagedChunk(managedLines, 2)) {
|
|
2711
|
+
preservedLines.push(...managedLines);
|
|
2712
|
+
}
|
|
2713
|
+
const preservedContent = preservedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
2714
|
+
if (preservedContent.length === 0) {
|
|
2715
|
+
return block;
|
|
2716
|
+
}
|
|
2717
|
+
return `${preservedContent}
|
|
2718
|
+
|
|
2719
|
+
${block}`;
|
|
2720
|
+
};
|
|
2721
|
+
var writeManagedAgentsFile = async (rootDir, path12 = "AGENTS.md", block) => {
|
|
2722
|
+
let existingContent = null;
|
|
2723
|
+
try {
|
|
2724
|
+
existingContent = await fs7.readFile(resolveRepoPath(rootDir, path12), "utf8");
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") {
|
|
2727
|
+
throw error;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return writeRepoFile(rootDir, path12, upsertManagedBlock(existingContent, block));
|
|
2731
|
+
};
|
|
2732
|
+
var diagnosticCategoryForPath = (filePath, config) => {
|
|
2733
|
+
if (filePath === "AGENTS.md") {
|
|
2734
|
+
return "truth-sync";
|
|
2735
|
+
}
|
|
2736
|
+
if (filePath === "CLAUDE.md" || filePath === "GEMINI.md" || filePath === ".github/copilot-instructions.md" || filePath.startsWith(".github/prompts/truthmark-") || filePath.startsWith(".claude/skills/truthmark-") || filePath.startsWith(".opencode/skills/truthmark-")) {
|
|
2737
|
+
return "truth-sync";
|
|
2738
|
+
}
|
|
2739
|
+
if (filePath.startsWith(".codex/skills/truthmark-structure/")) {
|
|
2740
|
+
return "truth-sync";
|
|
2741
|
+
}
|
|
2742
|
+
if (filePath.startsWith(".codex/skills/truthmark-document/")) {
|
|
2743
|
+
return "truth-sync";
|
|
2744
|
+
}
|
|
2745
|
+
if (filePath.startsWith(".codex/skills/truthmark-sync/")) {
|
|
2746
|
+
return "truth-sync";
|
|
2747
|
+
}
|
|
2748
|
+
if (filePath.startsWith(".codex/skills/truthmark-realize/")) {
|
|
2749
|
+
return "realization";
|
|
2750
|
+
}
|
|
2751
|
+
if (filePath.startsWith(".gemini/commands/truthmark/realize")) {
|
|
2752
|
+
return "realization";
|
|
2753
|
+
}
|
|
2754
|
+
if (filePath.startsWith(".gemini/commands/truthmark/")) {
|
|
2755
|
+
return "truth-sync";
|
|
2756
|
+
}
|
|
2757
|
+
if (filePath.startsWith(".codex/skills/truthmark-check/")) {
|
|
2758
|
+
return "truth-sync";
|
|
2759
|
+
}
|
|
2760
|
+
if (filePath === config.docs.routing.rootIndex) {
|
|
2761
|
+
return "authority";
|
|
2762
|
+
}
|
|
2763
|
+
return "config";
|
|
2764
|
+
};
|
|
2765
|
+
var writePlatformFile = async (rootDir, file) => {
|
|
2766
|
+
if (file.managedBlock) {
|
|
2767
|
+
return writeManagedAgentsFile(rootDir, file.path, file.content);
|
|
2768
|
+
}
|
|
2769
|
+
return writeRepoFile(rootDir, file.path, file.content);
|
|
2770
|
+
};
|
|
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}.`;
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
var writeDiagnostics = (results, config) => {
|
|
2782
|
+
return results.map((result) => ({
|
|
2783
|
+
category: diagnosticCategoryForPath(result.path, config),
|
|
2784
|
+
severity: "action",
|
|
2785
|
+
message: messageForWriteResult(result),
|
|
2786
|
+
file: result.path
|
|
2787
|
+
}));
|
|
2788
|
+
};
|
|
2789
|
+
var runInit = async (cwd) => {
|
|
2790
|
+
const repository = await getGitRepository(cwd);
|
|
2791
|
+
const rootDir = repository.worktreePath;
|
|
2792
|
+
const loadedConfig = await loadConfig(rootDir);
|
|
2793
|
+
if (!loadedConfig.config) {
|
|
2794
|
+
return {
|
|
2795
|
+
command: "init",
|
|
2796
|
+
summary: "Truthmark init requires .truthmark/config.yml. Run truthmark config first, review the hierarchy, then run truthmark init.",
|
|
2797
|
+
diagnostics: loadedConfig.diagnostics,
|
|
2798
|
+
data: {
|
|
2799
|
+
repositoryRoot: repository.repositoryRoot,
|
|
2800
|
+
worktreePath: repository.worktreePath,
|
|
2801
|
+
branchName: repository.branchName,
|
|
2802
|
+
isDetached: repository.isDetached,
|
|
2803
|
+
isUnborn: repository.isUnborn
|
|
2804
|
+
}
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
const defaultStandards = renderDefaultStandards([]);
|
|
2808
|
+
const results = [];
|
|
2809
|
+
for (const template of defaultStandards) {
|
|
2810
|
+
results.push(await ensureRepoFile(rootDir, template.path, template.content));
|
|
2811
|
+
}
|
|
2812
|
+
const config = loadedConfig.config;
|
|
2813
|
+
results.push(...await scaffoldHierarchy(rootDir, config));
|
|
2814
|
+
const migrationDiagnostics = await detectHierarchyMigrationDiagnostics(rootDir, config);
|
|
2815
|
+
const block = renderAgentsBlock(config);
|
|
2816
|
+
const platformFiles = renderGeneratedSurfaces(config, block);
|
|
2817
|
+
for (const file of platformFiles) {
|
|
2818
|
+
results.push(await writePlatformFile(rootDir, file));
|
|
2819
|
+
}
|
|
2820
|
+
const changedResults = results.filter((result) => result.status !== "unchanged");
|
|
2821
|
+
return {
|
|
2822
|
+
command: "init",
|
|
2823
|
+
summary: changedResults.length > 0 ? "Initialized or updated the Truthmark repository scaffold." : "Truthmark repository scaffold is already up to date.",
|
|
2824
|
+
diagnostics: [...writeDiagnostics(results, config), ...migrationDiagnostics],
|
|
2825
|
+
data: {
|
|
2826
|
+
repositoryRoot: repository.repositoryRoot,
|
|
2827
|
+
worktreePath: repository.worktreePath,
|
|
2828
|
+
branchName: repository.branchName,
|
|
2829
|
+
isDetached: repository.isDetached,
|
|
2830
|
+
isUnborn: repository.isUnborn
|
|
2831
|
+
}
|
|
2832
|
+
};
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
// src/checks/branch-scope.ts
|
|
2836
|
+
import fs8 from "fs/promises";
|
|
2837
|
+
import fg2 from "fast-glob";
|
|
2838
|
+
|
|
2839
|
+
// src/markdown/hash.ts
|
|
2840
|
+
import { createHash } from "crypto";
|
|
2841
|
+
var hashText = (value) => {
|
|
2842
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
2843
|
+
};
|
|
2844
|
+
|
|
2845
|
+
// src/checks/branch-scope.ts
|
|
2846
|
+
var BranchScopeFileError = class extends Error {
|
|
2847
|
+
file;
|
|
2848
|
+
constructor(file, message) {
|
|
2849
|
+
super(message);
|
|
2850
|
+
this.name = "BranchScopeFileError";
|
|
2851
|
+
this.file = file;
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
var RELEVANT_BRANCH_SCOPE_FILES = [".truthmark/config.yml"];
|
|
2855
|
+
var toBranchIdentity = (branchName, headSha) => {
|
|
2856
|
+
if (branchName && headSha) {
|
|
2857
|
+
return `${branchName}@${headSha}`;
|
|
2858
|
+
}
|
|
2859
|
+
if (branchName) {
|
|
2860
|
+
return `unborn:${branchName}`;
|
|
2861
|
+
}
|
|
2862
|
+
return headSha ? `detached:${headSha}` : "detached:unknown";
|
|
2048
2863
|
};
|
|
2049
2864
|
var createBranchScopeData = (repository, relevantFileHashes = {}) => {
|
|
2050
2865
|
return {
|
|
@@ -2226,8 +3041,12 @@ var parseMarkdownDocument = (source) => {
|
|
|
2226
3041
|
};
|
|
2227
3042
|
|
|
2228
3043
|
// src/checks/frontmatter.ts
|
|
2229
|
-
var
|
|
3044
|
+
var isTruthDocumentKind2 = (value) => TRUTH_DOCUMENT_KINDS.includes(value);
|
|
3045
|
+
var checkFrontmatter = async (rootDir, config, markdownPaths, truthDocumentEntries = []) => {
|
|
2230
3046
|
const diagnostics = [];
|
|
3047
|
+
const truthDocumentMap = new Map(
|
|
3048
|
+
truthDocumentEntries.map((entry) => [entry.path, entry])
|
|
3049
|
+
);
|
|
2231
3050
|
for (const markdownPath of markdownPaths) {
|
|
2232
3051
|
if (!markdownPath.endsWith(".md")) {
|
|
2233
3052
|
continue;
|
|
@@ -2267,13 +3086,34 @@ var checkFrontmatter = async (rootDir, config, markdownPaths) => {
|
|
|
2267
3086
|
});
|
|
2268
3087
|
}
|
|
2269
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
|
+
}
|
|
2270
3110
|
}
|
|
2271
3111
|
return diagnostics;
|
|
2272
3112
|
};
|
|
2273
3113
|
|
|
2274
3114
|
// src/checks/links.ts
|
|
2275
3115
|
import fs11 from "fs/promises";
|
|
2276
|
-
import
|
|
3116
|
+
import path4 from "path";
|
|
2277
3117
|
var pathExists2 = async (absolutePath) => {
|
|
2278
3118
|
try {
|
|
2279
3119
|
await fs11.stat(absolutePath);
|
|
@@ -2307,7 +3147,7 @@ var checkLinks = async (rootDir, markdownPaths) => {
|
|
|
2307
3147
|
if (targetPath.length === 0) {
|
|
2308
3148
|
continue;
|
|
2309
3149
|
}
|
|
2310
|
-
const absoluteTarget =
|
|
3150
|
+
const absoluteTarget = path4.resolve(path4.dirname(absolutePath), targetPath);
|
|
2311
3151
|
const relativeTarget = toRepoRelativePath(rootDir, absoluteTarget);
|
|
2312
3152
|
try {
|
|
2313
3153
|
await assertRepoContainment(rootDir, absoluteTarget);
|
|
@@ -2342,123 +3182,17 @@ import micromatch3 from "micromatch";
|
|
|
2342
3182
|
import fs12 from "fs/promises";
|
|
2343
3183
|
import fg4 from "fast-glob";
|
|
2344
3184
|
import micromatch from "micromatch";
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
var slugify = (value) => {
|
|
2348
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3185
|
+
var unique = (values) => {
|
|
3186
|
+
return [...new Set(values)];
|
|
2349
3187
|
};
|
|
2350
|
-
var
|
|
2351
|
-
return
|
|
2352
|
-
category: "area-index",
|
|
2353
|
-
severity: "error",
|
|
2354
|
-
message,
|
|
2355
|
-
area
|
|
2356
|
-
};
|
|
3188
|
+
var normalizeGlobPath = (value) => {
|
|
3189
|
+
return value.replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
|
2357
3190
|
};
|
|
2358
|
-
var
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
const diagnostics = [];
|
|
2364
|
-
const areas = [];
|
|
2365
|
-
const truthDocumentReferences = [];
|
|
2366
|
-
const areaFileReferences = [];
|
|
2367
|
-
let areaIndex = 0;
|
|
2368
|
-
let currentAreaName = null;
|
|
2369
|
-
let currentSections = /* @__PURE__ */ new Map();
|
|
2370
|
-
let currentSectionName = null;
|
|
2371
|
-
const flushArea = () => {
|
|
2372
|
-
if (!currentAreaName) {
|
|
2373
|
-
return;
|
|
2374
|
-
}
|
|
2375
|
-
const truthDocuments = parseListSection(currentSections.get("Truth documents") ?? []);
|
|
2376
|
-
const areaFiles = parseListSection(currentSections.get("Area files") ?? []);
|
|
2377
|
-
const codeSurface = parseListSection(currentSections.get("Code surface") ?? []);
|
|
2378
|
-
const updateTruthWhen = parseListSection(currentSections.get("Update truth when") ?? []);
|
|
2379
|
-
const areaKey = slugify(currentAreaName);
|
|
2380
|
-
const areaId = areaKey.length > 0 ? areaKey : `area-${areaIndex}`;
|
|
2381
|
-
const hasTruthDocuments = truthDocuments.length > 0;
|
|
2382
|
-
const hasAreaFiles = areaFiles.length > 0;
|
|
2383
|
-
areaIndex += 1;
|
|
2384
|
-
if (hasTruthDocuments) {
|
|
2385
|
-
truthDocumentReferences.push({
|
|
2386
|
-
id: areaId,
|
|
2387
|
-
name: currentAreaName,
|
|
2388
|
-
key: areaKey,
|
|
2389
|
-
truthDocuments
|
|
2390
|
-
});
|
|
2391
|
-
}
|
|
2392
|
-
if (hasTruthDocuments === hasAreaFiles || codeSurface.length === 0 || updateTruthWhen.length === 0) {
|
|
2393
|
-
diagnostics.push(
|
|
2394
|
-
createAreaDiagnostic(
|
|
2395
|
-
`Area ${currentAreaName} must define exactly one of Truth documents or Area files, plus Code surface and Update truth when sections.`,
|
|
2396
|
-
currentAreaName
|
|
2397
|
-
)
|
|
2398
|
-
);
|
|
2399
|
-
} else if (hasAreaFiles) {
|
|
2400
|
-
areaFileReferences.push({
|
|
2401
|
-
id: areaId,
|
|
2402
|
-
name: currentAreaName,
|
|
2403
|
-
key: areaKey,
|
|
2404
|
-
areaFiles,
|
|
2405
|
-
codeSurface,
|
|
2406
|
-
updateTruthWhen
|
|
2407
|
-
});
|
|
2408
|
-
} else {
|
|
2409
|
-
areas.push({
|
|
2410
|
-
id: areaId,
|
|
2411
|
-
name: currentAreaName,
|
|
2412
|
-
key: areaKey,
|
|
2413
|
-
truthDocuments,
|
|
2414
|
-
codeSurface,
|
|
2415
|
-
updateTruthWhen
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
currentAreaName = null;
|
|
2419
|
-
currentSections = /* @__PURE__ */ new Map();
|
|
2420
|
-
currentSectionName = null;
|
|
2421
|
-
};
|
|
2422
|
-
for (const line of lines) {
|
|
2423
|
-
const areaHeadingMatch = line.match(/^\s{0,3}##\s+(.*)$/u);
|
|
2424
|
-
if (areaHeadingMatch) {
|
|
2425
|
-
flushArea();
|
|
2426
|
-
currentAreaName = areaHeadingMatch[1]?.trim() ?? null;
|
|
2427
|
-
continue;
|
|
2428
|
-
}
|
|
2429
|
-
if (!currentAreaName) {
|
|
2430
|
-
continue;
|
|
2431
|
-
}
|
|
2432
|
-
if (/^(Truth documents|Area files|Code surface|Update truth when):$/u.test(line.trim())) {
|
|
2433
|
-
currentSectionName = line.trim().slice(0, -1);
|
|
2434
|
-
currentSections.set(currentSectionName, []);
|
|
2435
|
-
continue;
|
|
2436
|
-
}
|
|
2437
|
-
if (currentSectionName) {
|
|
2438
|
-
currentSections.get(currentSectionName)?.push(line);
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
flushArea();
|
|
2442
|
-
return {
|
|
2443
|
-
areas,
|
|
2444
|
-
truthDocumentReferences,
|
|
2445
|
-
areaFileReferences,
|
|
2446
|
-
diagnostics
|
|
2447
|
-
};
|
|
2448
|
-
};
|
|
2449
|
-
|
|
2450
|
-
// src/routing/area-resolver.ts
|
|
2451
|
-
var unique = (values) => {
|
|
2452
|
-
return [...new Set(values)];
|
|
2453
|
-
};
|
|
2454
|
-
var normalizeGlobPath = (value) => {
|
|
2455
|
-
return value.replaceAll("\\", "/").replace(/^\.\/+/u, "");
|
|
2456
|
-
};
|
|
2457
|
-
var concretePrefix = (pattern) => {
|
|
2458
|
-
const normalizedPattern = normalizeGlobPath(pattern);
|
|
2459
|
-
const wildcardIndex = normalizedPattern.search(/[*?[{(!+@]/u);
|
|
2460
|
-
const prefix = wildcardIndex === -1 ? normalizedPattern : normalizedPattern.slice(0, wildcardIndex);
|
|
2461
|
-
return prefix.replace(/[^/]*$/u, "");
|
|
3191
|
+
var concretePrefix = (pattern) => {
|
|
3192
|
+
const normalizedPattern = normalizeGlobPath(pattern);
|
|
3193
|
+
const wildcardIndex = normalizedPattern.search(/[*?[{(!+@]/u);
|
|
3194
|
+
const prefix = wildcardIndex === -1 ? normalizedPattern : normalizedPattern.slice(0, wildcardIndex);
|
|
3195
|
+
return prefix.replace(/[^/]*$/u, "");
|
|
2462
3196
|
};
|
|
2463
3197
|
var isCodeSurfaceWithinParent = (childPattern, parentPatterns) => {
|
|
2464
3198
|
const childPrefix = concretePrefix(childPattern);
|
|
@@ -2530,7 +3264,9 @@ var resolveAreaRouting = async (rootDir, config) => {
|
|
|
2530
3264
|
diagnostics: [rootRead.diagnostic]
|
|
2531
3265
|
};
|
|
2532
3266
|
}
|
|
2533
|
-
const rootParsed = parseAreasMarkdown(rootRead.source ?? ""
|
|
3267
|
+
const rootParsed = parseAreasMarkdown(rootRead.source ?? "", {
|
|
3268
|
+
truthDocsRoot: config.truthDocsRoot
|
|
3269
|
+
});
|
|
2534
3270
|
diagnostics.push(
|
|
2535
3271
|
...rootParsed.diagnostics.map((diagnostic) => ({
|
|
2536
3272
|
...diagnostic,
|
|
@@ -2567,7 +3303,9 @@ var resolveAreaRouting = async (rootDir, config) => {
|
|
|
2567
3303
|
continue;
|
|
2568
3304
|
}
|
|
2569
3305
|
routeFiles.push(areaFile);
|
|
2570
|
-
const childParsed = parseAreasMarkdown(childRead.source ?? ""
|
|
3306
|
+
const childParsed = parseAreasMarkdown(childRead.source ?? "", {
|
|
3307
|
+
truthDocsRoot: config.truthDocsRoot
|
|
3308
|
+
});
|
|
2571
3309
|
diagnostics.push(
|
|
2572
3310
|
...childParsed.diagnostics.map((diagnostic) => ({
|
|
2573
3311
|
...diagnostic,
|
|
@@ -2846,7 +3584,8 @@ var isBroadCodeSurface = (pattern) => {
|
|
|
2846
3584
|
var checkAreas = async (rootDir, config) => {
|
|
2847
3585
|
const routing = await resolveAreaRouting(rootDir, {
|
|
2848
3586
|
rootIndex: config.docs.routing.rootIndex,
|
|
2849
|
-
areaFilesRoot: config.docs.routing.areaFilesRoot
|
|
3587
|
+
areaFilesRoot: config.docs.routing.areaFilesRoot,
|
|
3588
|
+
truthDocsRoot: resolveTruthDocsRoot(config)
|
|
2850
3589
|
});
|
|
2851
3590
|
const discoveredCodeFiles = await fg5([...COVERAGE_SCAN_PATTERNS], {
|
|
2852
3591
|
cwd: rootDir,
|
|
@@ -2861,6 +3600,7 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2861
3600
|
const diagnostics = [...routing.diagnostics];
|
|
2862
3601
|
const truthDocumentPaths = [];
|
|
2863
3602
|
const seenTruthDocumentPaths = /* @__PURE__ */ new Set();
|
|
3603
|
+
const truthDocumentEntryMap = /* @__PURE__ */ new Map();
|
|
2864
3604
|
const areaCoverage = routing.areas.map((area) => ({
|
|
2865
3605
|
area,
|
|
2866
3606
|
valid: true,
|
|
@@ -2878,8 +3618,28 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2878
3618
|
const truthReferences = routing.truthDocumentReferences;
|
|
2879
3619
|
for (const area of truthReferences) {
|
|
2880
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
|
+
};
|
|
2881
3638
|
for (const truthDocument of area.truthDocuments) {
|
|
2882
3639
|
if (looksLikeGlob2(truthDocument)) {
|
|
3640
|
+
const routedGlobEntry = area.truthDocumentEntries.find(
|
|
3641
|
+
(entry) => entry.path === truthDocument
|
|
3642
|
+
);
|
|
2883
3643
|
const matches = (await fg5([truthDocument], { cwd: rootDir, onlyFiles: true })).sort();
|
|
2884
3644
|
if (matches.length === 0) {
|
|
2885
3645
|
diagnostics.push({
|
|
@@ -2911,6 +3671,12 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2911
3671
|
seenTruthDocumentPaths.add(match);
|
|
2912
3672
|
truthDocumentPaths.push(match);
|
|
2913
3673
|
}
|
|
3674
|
+
if (routedGlobEntry && !registerTruthDocumentEntry({
|
|
3675
|
+
...routedGlobEntry,
|
|
3676
|
+
path: match
|
|
3677
|
+
})) {
|
|
3678
|
+
areaHasTruthDocumentErrors = true;
|
|
3679
|
+
}
|
|
2914
3680
|
}
|
|
2915
3681
|
continue;
|
|
2916
3682
|
}
|
|
@@ -2944,6 +3710,10 @@ var checkAreas = async (rootDir, config) => {
|
|
|
2944
3710
|
seenTruthDocumentPaths.add(truthDocument);
|
|
2945
3711
|
truthDocumentPaths.push(truthDocument);
|
|
2946
3712
|
}
|
|
3713
|
+
const routedEntry = area.truthDocumentEntries.find((entry) => entry.path === truthDocument);
|
|
3714
|
+
if (routedEntry && !registerTruthDocumentEntry(routedEntry)) {
|
|
3715
|
+
areaHasTruthDocumentErrors = true;
|
|
3716
|
+
}
|
|
2947
3717
|
}
|
|
2948
3718
|
if (areaHasTruthDocumentErrors) {
|
|
2949
3719
|
const matchingArea = areaCoverage.find(
|
|
@@ -3056,6 +3826,7 @@ var checkAreas = async (rootDir, config) => {
|
|
|
3056
3826
|
return {
|
|
3057
3827
|
diagnostics,
|
|
3058
3828
|
truthDocumentPaths,
|
|
3829
|
+
truthDocumentEntries: [...truthDocumentEntryMap.values()],
|
|
3059
3830
|
routePrecision: {
|
|
3060
3831
|
leafAreaCount: routing.areas.length,
|
|
3061
3832
|
broadAreaCount
|
|
@@ -3067,36 +3838,97 @@ var checkAreas = async (rootDir, config) => {
|
|
|
3067
3838
|
// src/checks/decisions.ts
|
|
3068
3839
|
import fs14 from "fs/promises";
|
|
3069
3840
|
import micromatch4 from "micromatch";
|
|
3070
|
-
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
|
+
};
|
|
3071
3845
|
var escapeRegExp2 = (value) => {
|
|
3072
3846
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3073
3847
|
};
|
|
3074
3848
|
var hasHeading = (source, heading) => {
|
|
3075
3849
|
return new RegExp(`^#{2,3}\\s+${escapeRegExp2(heading)}\\s*$`, "mu").test(source);
|
|
3076
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
|
+
};
|
|
3077
3896
|
var decisionTruthGlobs = (config) => {
|
|
3078
3897
|
return [
|
|
3079
3898
|
config.docs.roots.architecture,
|
|
3080
|
-
config
|
|
3899
|
+
resolveTruthDocsRoot(config),
|
|
3081
3900
|
config.docs.roots.api
|
|
3082
3901
|
].filter((root) => Boolean(root)).map((root) => `${root}/**/*.md`);
|
|
3083
3902
|
};
|
|
3084
3903
|
var isDecisionTruthCandidate = (config, filePath) => {
|
|
3085
3904
|
return !filePath.endsWith("/README.md") && micromatch4.isMatch(filePath, decisionTruthGlobs(config));
|
|
3086
3905
|
};
|
|
3087
|
-
var checkDecisionSections = async (rootDir, config, markdownPaths) => {
|
|
3906
|
+
var checkDecisionSections = async (rootDir, config, markdownPaths, truthDocumentEntries = []) => {
|
|
3088
3907
|
const diagnostics = [];
|
|
3089
|
-
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();
|
|
3090
3914
|
for (const filePath of candidatePaths) {
|
|
3091
3915
|
const source = await fs14.readFile(resolveRepoPath(rootDir, filePath), "utf8");
|
|
3092
|
-
const
|
|
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));
|
|
3093
3925
|
if (missingHeadings.length === 0) {
|
|
3094
3926
|
continue;
|
|
3095
3927
|
}
|
|
3096
3928
|
diagnostics.push({
|
|
3097
3929
|
category: "doc-structure",
|
|
3098
3930
|
severity: "review",
|
|
3099
|
-
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.`,
|
|
3100
3932
|
file: filePath
|
|
3101
3933
|
});
|
|
3102
3934
|
}
|
|
@@ -3105,252 +3937,984 @@ var checkDecisionSections = async (rootDir, config, markdownPaths) => {
|
|
|
3105
3937
|
|
|
3106
3938
|
// src/checks/generated-surfaces.ts
|
|
3107
3939
|
import fs15 from "fs/promises";
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
{
|
|
3113
|
-
|
|
3114
|
-
content: renderTruthmarkStructureLocalSkill(config)
|
|
3115
|
-
},
|
|
3116
|
-
{
|
|
3117
|
-
path: `${basePath}/truthmark-document/SKILL.md`,
|
|
3118
|
-
content: renderTruthmarkDocumentLocalSkill(config)
|
|
3119
|
-
},
|
|
3120
|
-
{
|
|
3121
|
-
path: `${basePath}/truthmark-sync/SKILL.md`,
|
|
3122
|
-
content: renderTruthmarkSyncLocalSkill(config)
|
|
3123
|
-
},
|
|
3124
|
-
{
|
|
3125
|
-
path: `${basePath}/truthmark-check/SKILL.md`,
|
|
3126
|
-
content: renderTruthmarkCheckLocalSkill(config)
|
|
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;
|
|
3127
3946
|
}
|
|
3128
|
-
|
|
3129
|
-
if (config.realization.enabled) {
|
|
3130
|
-
files.push({
|
|
3131
|
-
path: `${basePath}/truthmark-realize/SKILL.md`,
|
|
3132
|
-
content: renderTruthmarkRealizeLocalSkill()
|
|
3133
|
-
});
|
|
3947
|
+
throw error;
|
|
3134
3948
|
}
|
|
3135
|
-
return files;
|
|
3136
3949
|
};
|
|
3137
|
-
var
|
|
3138
|
-
const
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
{
|
|
3156
|
-
|
|
3157
|
-
content: renderTruthmarkSyncSkill(config)
|
|
3158
|
-
},
|
|
3159
|
-
{
|
|
3160
|
-
path: TRUTHMARK_SYNC_SKILL_METADATA_PATH,
|
|
3161
|
-
content: renderTruthmarkSyncSkillMetadata()
|
|
3162
|
-
},
|
|
3163
|
-
{
|
|
3164
|
-
path: TRUTHMARK_CHECK_SKILL_PATH,
|
|
3165
|
-
content: renderTruthmarkCheckSkill(config)
|
|
3166
|
-
},
|
|
3167
|
-
{
|
|
3168
|
-
path: TRUTHMARK_CHECK_SKILL_METADATA_PATH,
|
|
3169
|
-
content: renderTruthmarkCheckSkillMetadata()
|
|
3170
|
-
}
|
|
3950
|
+
var extractManagedBlock = (content) => {
|
|
3951
|
+
const startIndex = content.indexOf(TRUTHMARK_BLOCK_START);
|
|
3952
|
+
const endIndex = content.indexOf(TRUTHMARK_BLOCK_END);
|
|
3953
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
3954
|
+
return null;
|
|
3955
|
+
}
|
|
3956
|
+
return content.slice(startIndex, endIndex + TRUTHMARK_BLOCK_END.length);
|
|
3957
|
+
};
|
|
3958
|
+
var normalizeGeneratedSurfaceContent = (content) => {
|
|
3959
|
+
if (content === null) {
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3962
|
+
return content.replace(/\r\n/g, "\n").replace(/\n$/u, "");
|
|
3963
|
+
};
|
|
3964
|
+
var versionMarkers = (content) => {
|
|
3965
|
+
const markers = [];
|
|
3966
|
+
const patterns = [
|
|
3967
|
+
/truthmark-version:\s*([^\s]+)/gu,
|
|
3968
|
+
/Generated by Truthmark\s+([^\s.]+(?:\.[^\s.]+){1,2})/gu,
|
|
3969
|
+
/^version:\s*"(\d+\.\d+\.\d+)"\s*$/gmu
|
|
3171
3970
|
];
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
{
|
|
3175
|
-
|
|
3176
|
-
content: renderTruthmarkRealizeSkill()
|
|
3177
|
-
},
|
|
3178
|
-
{
|
|
3179
|
-
path: TRUTHMARK_REALIZE_SKILL_METADATA_PATH,
|
|
3180
|
-
content: renderTruthmarkRealizeSkillMetadata()
|
|
3971
|
+
for (const pattern of patterns) {
|
|
3972
|
+
for (const match of content.matchAll(pattern)) {
|
|
3973
|
+
if (match[1]) {
|
|
3974
|
+
markers.push(match[1]);
|
|
3181
3975
|
}
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
return markers;
|
|
3979
|
+
};
|
|
3980
|
+
var checkGeneratedSurfaces = async (rootDir, config) => {
|
|
3981
|
+
const diagnostics = [];
|
|
3982
|
+
for (const surface of renderGeneratedSurfaces(config)) {
|
|
3983
|
+
const content = await readOptionalFile(rootDir, surface.path);
|
|
3984
|
+
if (content === null) {
|
|
3985
|
+
diagnostics.push({
|
|
3986
|
+
category: "generated-surface",
|
|
3987
|
+
severity: "review",
|
|
3988
|
+
message: `Generated surface ${surface.path} is missing; rerun truthmark init.`,
|
|
3989
|
+
file: surface.path
|
|
3990
|
+
});
|
|
3991
|
+
continue;
|
|
3992
|
+
}
|
|
3993
|
+
const comparableContent = normalizeGeneratedSurfaceContent(
|
|
3994
|
+
surface.managedBlock ? extractManagedBlock(content) : content
|
|
3182
3995
|
);
|
|
3996
|
+
const expectedContent = normalizeGeneratedSurfaceContent(surface.content);
|
|
3997
|
+
if (comparableContent !== expectedContent) {
|
|
3998
|
+
diagnostics.push({
|
|
3999
|
+
category: "generated-surface",
|
|
4000
|
+
severity: "review",
|
|
4001
|
+
message: `Generated surface ${surface.path} is stale; rerun truthmark init.`,
|
|
4002
|
+
file: surface.path
|
|
4003
|
+
});
|
|
4004
|
+
}
|
|
4005
|
+
const versionContent = surface.managedBlock ? comparableContent ?? "" : content;
|
|
4006
|
+
const mismatchedVersions = versionMarkers(versionContent).filter(
|
|
4007
|
+
(version) => version !== TRUTHMARK_VERSION
|
|
4008
|
+
);
|
|
4009
|
+
if (mismatchedVersions.length > 0) {
|
|
4010
|
+
diagnostics.push({
|
|
4011
|
+
category: "generated-surface",
|
|
4012
|
+
severity: "review",
|
|
4013
|
+
message: `Generated surface ${surface.path} has Truthmark version ${mismatchedVersions[0]} but current version is ${TRUTHMARK_VERSION}; rerun truthmark init.`,
|
|
4014
|
+
file: surface.path
|
|
4015
|
+
});
|
|
4016
|
+
}
|
|
3183
4017
|
}
|
|
3184
|
-
return
|
|
4018
|
+
return diagnostics;
|
|
3185
4019
|
};
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
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"],
|
|
3201
4101
|
{
|
|
3202
|
-
|
|
3203
|
-
|
|
4102
|
+
cwd: rootDir,
|
|
4103
|
+
reject: false
|
|
3204
4104
|
}
|
|
3205
|
-
|
|
3206
|
-
if (
|
|
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);
|
|
3207
4128
|
files.push({
|
|
3208
|
-
path:
|
|
3209
|
-
|
|
4129
|
+
path: filePath,
|
|
4130
|
+
kind,
|
|
4131
|
+
language: languageByExtension.get(extension) ?? null
|
|
3210
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
|
+
}
|
|
3211
4153
|
}
|
|
3212
|
-
return
|
|
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
|
+
};
|
|
3213
4159
|
};
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
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";
|
|
3220
4182
|
};
|
|
3221
|
-
var
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
{
|
|
3242
|
-
path: TRUTHMARK_GEMINI_DOCUMENT_COMMAND_PATH,
|
|
3243
|
-
content: renderTruthmarkGeminiDocumentCommand(config)
|
|
3244
|
-
},
|
|
3245
|
-
{
|
|
3246
|
-
path: TRUTHMARK_GEMINI_SYNC_COMMAND_PATH,
|
|
3247
|
-
content: renderTruthmarkGeminiSyncCommand(config)
|
|
3248
|
-
},
|
|
3249
|
-
{
|
|
3250
|
-
path: TRUTHMARK_GEMINI_CHECK_COMMAND_PATH,
|
|
3251
|
-
content: renderTruthmarkGeminiCheckCommand(config)
|
|
3252
|
-
},
|
|
3253
|
-
...config.realization.enabled ? [
|
|
3254
|
-
{
|
|
3255
|
-
path: TRUTHMARK_GEMINI_REALIZE_COMMAND_PATH,
|
|
3256
|
-
content: renderTruthmarkGeminiRealizeCommand()
|
|
3257
|
-
}
|
|
3258
|
-
] : []
|
|
3259
|
-
];
|
|
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
|
+
});
|
|
3260
4203
|
}
|
|
4204
|
+
return packages;
|
|
3261
4205
|
};
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
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)
|
|
3269
4244
|
);
|
|
3270
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
|
+
};
|
|
3271
4327
|
|
|
3272
|
-
// src/
|
|
3273
|
-
var
|
|
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) => {
|
|
3274
4390
|
try {
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
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
|
+
});
|
|
3279
4496
|
}
|
|
3280
|
-
|
|
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
|
+
});
|
|
3281
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
|
+
);
|
|
3282
4548
|
};
|
|
3283
|
-
var
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
4549
|
+
var changedFilePaths = (changedFile) => {
|
|
4550
|
+
return [changedFile.path, ...changedFile.previousPath ? [changedFile.previousPath] : []];
|
|
4551
|
+
};
|
|
4552
|
+
var resolveImportPath = (importEdge) => {
|
|
4553
|
+
if (!importEdge.specifier.startsWith(".")) {
|
|
3287
4554
|
return null;
|
|
3288
4555
|
}
|
|
3289
|
-
|
|
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;
|
|
3290
4559
|
};
|
|
3291
|
-
var
|
|
3292
|
-
|
|
3293
|
-
|
|
4560
|
+
var importTargetsChangedFile = (importEdge, changedPath) => {
|
|
4561
|
+
const resolved = resolveImportPath(importEdge);
|
|
4562
|
+
if (!resolved) {
|
|
4563
|
+
return false;
|
|
3294
4564
|
}
|
|
3295
|
-
return
|
|
4565
|
+
return changedPath.replace(/\.[cm]?[jt]sx?$/u, "") === resolved;
|
|
3296
4566
|
};
|
|
3297
|
-
var
|
|
3298
|
-
|
|
3299
|
-
const
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
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" });
|
|
3309
4585
|
}
|
|
4586
|
+
for (const entry of baseExports) {
|
|
4587
|
+
changes.push({ path: basePath, name: entry.name, kind: entry.kind, change: "removed" });
|
|
4588
|
+
}
|
|
4589
|
+
return changes;
|
|
3310
4590
|
}
|
|
3311
|
-
|
|
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;
|
|
3312
4602
|
};
|
|
3313
|
-
var
|
|
3314
|
-
const
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
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")) {
|
|
3318
4643
|
diagnostics.push({
|
|
3319
|
-
category: "
|
|
4644
|
+
category: "impact",
|
|
3320
4645
|
severity: "review",
|
|
3321
|
-
message: `
|
|
3322
|
-
file:
|
|
4646
|
+
message: `Changed file ${changedFile.path} is not mapped to a Truthmark route.`,
|
|
4647
|
+
file: changedFile.path
|
|
3323
4648
|
});
|
|
3324
|
-
continue;
|
|
3325
4649
|
}
|
|
3326
|
-
const
|
|
3327
|
-
|
|
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
|
+
)
|
|
3328
4659
|
);
|
|
3329
|
-
|
|
3330
|
-
|
|
4660
|
+
}
|
|
4661
|
+
const uniqueAffectedTruthDocs = uniqueSorted(affectedTruthDocs);
|
|
4662
|
+
const changedPaths = changedPathSet(changedFiles);
|
|
4663
|
+
for (const symbol of changedPublicSymbols) {
|
|
4664
|
+
if (uniqueAffectedTruthDocs.length === 0) {
|
|
3331
4665
|
diagnostics.push({
|
|
3332
|
-
category: "
|
|
4666
|
+
category: "impact",
|
|
3333
4667
|
severity: "review",
|
|
3334
|
-
message: `
|
|
3335
|
-
file:
|
|
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
|
+
}
|
|
3336
4674
|
});
|
|
4675
|
+
continue;
|
|
3337
4676
|
}
|
|
3338
|
-
|
|
3339
|
-
const mismatchedVersions = versionMarkers(versionContent).filter(
|
|
3340
|
-
(version) => version !== TRUTHMARK_VERSION
|
|
3341
|
-
);
|
|
3342
|
-
if (mismatchedVersions.length > 0) {
|
|
4677
|
+
if (!uniqueAffectedTruthDocs.some((truthDoc) => changedPaths.has(truthDoc))) {
|
|
3343
4678
|
diagnostics.push({
|
|
3344
|
-
category: "
|
|
4679
|
+
category: "impact",
|
|
3345
4680
|
severity: "review",
|
|
3346
|
-
message: `
|
|
3347
|
-
file:
|
|
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
|
+
}
|
|
3348
4688
|
});
|
|
3349
4689
|
}
|
|
3350
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
|
+
}
|
|
3351
4895
|
return diagnostics;
|
|
3352
4896
|
};
|
|
3353
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
|
+
|
|
3354
4918
|
// src/checks/check.ts
|
|
3355
4919
|
var summarizeDiagnostics = (diagnostics) => {
|
|
3356
4920
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
@@ -3360,7 +4924,7 @@ var summarizeDiagnostics = (diagnostics) => {
|
|
|
3360
4924
|
}
|
|
3361
4925
|
return `Truthmark check completed with ${errorCount} error diagnostics and ${reviewCount} review diagnostics.`;
|
|
3362
4926
|
};
|
|
3363
|
-
var runCheck = async (cwd) => {
|
|
4927
|
+
var runCheck = async (cwd, options = {}) => {
|
|
3364
4928
|
const repository = await getGitRepository(cwd);
|
|
3365
4929
|
const rootDir = repository.worktreePath;
|
|
3366
4930
|
const branchScope = await getBranchScopeData(rootDir);
|
|
@@ -3378,10 +4942,21 @@ var runCheck = async (cwd) => {
|
|
|
3378
4942
|
const authority = await checkAuthority(rootDir, loadResult.config);
|
|
3379
4943
|
const areas = await checkAreas(rootDir, loadResult.config);
|
|
3380
4944
|
const markdownPaths = [.../* @__PURE__ */ new Set([...authority.paths, ...areas.truthDocumentPaths])];
|
|
3381
|
-
const frontmatter = await checkFrontmatter(
|
|
4945
|
+
const frontmatter = await checkFrontmatter(
|
|
4946
|
+
rootDir,
|
|
4947
|
+
loadResult.config,
|
|
4948
|
+
markdownPaths,
|
|
4949
|
+
areas.truthDocumentEntries
|
|
4950
|
+
);
|
|
3382
4951
|
const links = await checkLinks(rootDir, markdownPaths);
|
|
3383
|
-
const decisionSections = await checkDecisionSections(
|
|
4952
|
+
const decisionSections = await checkDecisionSections(
|
|
4953
|
+
rootDir,
|
|
4954
|
+
loadResult.config,
|
|
4955
|
+
markdownPaths,
|
|
4956
|
+
areas.truthDocumentEntries
|
|
4957
|
+
);
|
|
3384
4958
|
const generatedSurfaces = await checkGeneratedSurfaces(rootDir, loadResult.config);
|
|
4959
|
+
const freshness = options.base ? await checkFreshness(rootDir, loadResult.config, areas.truthDocumentPaths, options.base) : null;
|
|
3385
4960
|
const diagnostics = [
|
|
3386
4961
|
...loadResult.diagnostics,
|
|
3387
4962
|
...authority.diagnostics,
|
|
@@ -3389,7 +4964,8 @@ var runCheck = async (cwd) => {
|
|
|
3389
4964
|
...links,
|
|
3390
4965
|
...areas.diagnostics,
|
|
3391
4966
|
...decisionSections,
|
|
3392
|
-
...generatedSurfaces
|
|
4967
|
+
...generatedSurfaces,
|
|
4968
|
+
...freshness?.diagnostics ?? []
|
|
3393
4969
|
];
|
|
3394
4970
|
const truthVisibility = {
|
|
3395
4971
|
routePrecision: areas.routePrecision,
|
|
@@ -3400,7 +4976,8 @@ var runCheck = async (cwd) => {
|
|
|
3400
4976
|
syncCompletenessIssueCount: diagnostics.filter(
|
|
3401
4977
|
(diagnostic) => diagnostic.category === "doc-structure" || diagnostic.category === "generated-surface"
|
|
3402
4978
|
).length,
|
|
3403
|
-
topologyPressureCount: areas.topologyPressureCount
|
|
4979
|
+
topologyPressureCount: areas.topologyPressureCount,
|
|
4980
|
+
freshnessDiagnosticCount: freshness?.diagnostics.length ?? 0
|
|
3404
4981
|
};
|
|
3405
4982
|
return {
|
|
3406
4983
|
command: "check",
|
|
@@ -3408,11 +4985,170 @@ var runCheck = async (cwd) => {
|
|
|
3408
4985
|
diagnostics,
|
|
3409
4986
|
data: {
|
|
3410
4987
|
branchScope,
|
|
3411
|
-
truthVisibility
|
|
4988
|
+
truthVisibility,
|
|
4989
|
+
...freshness ? { impactSet: freshness.impactSet } : {}
|
|
4990
|
+
}
|
|
4991
|
+
};
|
|
4992
|
+
};
|
|
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));
|
|
3412
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
|
|
3413
5125
|
};
|
|
3414
5126
|
};
|
|
3415
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
|
+
|
|
3416
5152
|
// src/cli/handlers.ts
|
|
3417
5153
|
var runConfig2 = async (options) => {
|
|
3418
5154
|
return runConfig(process.cwd(), options);
|
|
@@ -3420,8 +5156,96 @@ var runConfig2 = async (options) => {
|
|
|
3420
5156
|
var runInit2 = async () => {
|
|
3421
5157
|
return runInit(process.cwd());
|
|
3422
5158
|
};
|
|
3423
|
-
var runCheck2 = async () => {
|
|
3424
|
-
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
|
+
};
|
|
3425
5249
|
};
|
|
3426
5250
|
|
|
3427
5251
|
// src/cli/program.ts
|
|
@@ -3430,6 +5254,13 @@ var writeResult = (result, options) => {
|
|
|
3430
5254
|
process.stdout.write(`${output}
|
|
3431
5255
|
`);
|
|
3432
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
|
+
};
|
|
3433
5264
|
var addJsonOption = (command) => {
|
|
3434
5265
|
return command.option("--json", "Render command output as JSON");
|
|
3435
5266
|
};
|
|
@@ -3447,9 +5278,31 @@ var buildProgram = () => {
|
|
|
3447
5278
|
writeResult(await runInit2(), options);
|
|
3448
5279
|
});
|
|
3449
5280
|
addJsonOption(
|
|
3450
|
-
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")
|
|
5282
|
+
).action(async (options) => {
|
|
5283
|
+
writeResult(await runCheck2({ base: options.base }), options);
|
|
5284
|
+
});
|
|
5285
|
+
addJsonOption(
|
|
5286
|
+
program.command("index").description("Build the deterministic Truthmark repository index.")
|
|
3451
5287
|
).action(async (options) => {
|
|
3452
|
-
writeResult(await
|
|
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
|
+
);
|
|
3453
5306
|
});
|
|
3454
5307
|
return program;
|
|
3455
5308
|
};
|