truthmark 1.2.3 → 1.3.0

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