truthmark 1.2.2 → 1.3.0

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